route-proxy: a second authority for internal names, and a target a route names the scheme of
Internal aliases were served over plain HTTP only — correctly refused a public certificate (no public CA can validate a private name), and then left with nothing. The mesh has two authorities for its two name spaces (08-connectivity §2), so the proxy now takes an optional internal ACME directory and dispatches at the handshake by the same question HostPolicy already answers: which authority may certify this name at all. A route may also say its target speaks https, with insecure for a backend whose own certificate nothing would trust — the shape Mailu's webmail front needs, and the exception: everything else the mesh hands this proxy stays plain http on the private network.
This commit is contained in:
@@ -130,6 +130,68 @@ func TestAContributionMissingWhatARouteNeedsIsSkipped(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A route may name a target reached over https, for a backend that terminates its own TLS — the
|
||||
// shape Mailu's webmail front needs, which this proxy reaches as a plain workload otherwise.
|
||||
func TestARouteMayTargetHttps(t *testing.T) {
|
||||
routes, _, err := routesFrom(write(t, `{"given":[
|
||||
{"from":"mail","node":"anchor","at":"anchor.internal",
|
||||
"values":{"name":"mail.example","port":7443,"scheme":"https","insecure":true}}
|
||||
]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if targetOf(routes, "mail.example") != "https://anchor.internal:7443" {
|
||||
t.Fatalf("an https target was not built as one: %v", routes)
|
||||
}
|
||||
if !routes["mail.example"][0].insecure {
|
||||
t.Fatal("insecure was declared and not carried onto the rule")
|
||||
}
|
||||
}
|
||||
|
||||
// A scheme that is neither http nor https is refused rather than guessed at.
|
||||
func TestARouteWithAnUnknownSchemeIsSkipped(t *testing.T) {
|
||||
routes, _, err := routesFrom(write(t, `{"given":[
|
||||
{"from":"a","node":"n","at":"n.internal","values":{"name":"bad.example","port":80,"scheme":"ftp"}}
|
||||
]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(routes) != 0 {
|
||||
t.Fatalf("a route with an unusable scheme was served: %v", routes)
|
||||
}
|
||||
}
|
||||
|
||||
// End to end: a backend terminating TLS with a certificate nothing would ordinarily trust is still
|
||||
// reached when the route declared `insecure`, and the response comes back through unmodified.
|
||||
func TestTheProxyReachesAnInsecureHttpsBackend(t *testing.T) {
|
||||
workload := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte("the workload, over its own TLS"))
|
||||
}))
|
||||
defer workload.Close()
|
||||
target := strings.TrimPrefix(workload.URL, "https://")
|
||||
|
||||
held := newTable()
|
||||
routes := map[string][]rule{"mail.example": {{target: "https://" + target, insecure: true}}}
|
||||
held.set(routes, allPublic(routes))
|
||||
|
||||
proxy := httptest.NewServer(handler(held))
|
||||
defer proxy.Close()
|
||||
|
||||
asked, err := http.NewRequest(http.MethodGet, proxy.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
asked.Host = "mail.example"
|
||||
answer, err := http.DefaultClient.Do(asked)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer answer.Body.Close()
|
||||
if answer.StatusCode != http.StatusOK {
|
||||
t.Fatalf("an insecure https backend was not reached: %d", answer.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// End to end through the proxy itself: a request for the name reaches the workload, and a name
|
||||
// nobody asked for is refused in a way that says what IS served.
|
||||
func TestTheProxyReachesTheWorkloadAndNamesWhatItServes(t *testing.T) {
|
||||
@@ -271,6 +333,31 @@ func TestNoCertificateIsAskedForOnAnInternalAlias(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A certificate is asked of the *internal* authority only for a name that is routed here and is
|
||||
// not a route's own public name — the internal-network alias, never the route it accompanies.
|
||||
func TestTheInternalAuthorityOnlyCertifiesInternalOnlyAliases(t *testing.T) {
|
||||
routes, public, err := routesFrom(write(t, `{"given":[
|
||||
{"from":"app","node":"anchor","at":"anchor.internal",
|
||||
"values":{"name":"app.example","internal-name":"app.anchor.internal","port":8080}}
|
||||
]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
held := newTable()
|
||||
held.set(routes, public)
|
||||
policy := onlyInternalNamesTheMeshSaid(held)
|
||||
|
||||
if err := policy(context.Background(), "app.anchor.internal"); err != nil {
|
||||
t.Errorf("the internal alias was refused by its own authority: %v", err)
|
||||
}
|
||||
if err := policy(context.Background(), "app.example"); err == nil {
|
||||
t.Error("the internal authority certified a route's public name, which the public authority already covers")
|
||||
}
|
||||
if err := policy(context.Background(), "unrouted.internal"); err == nil {
|
||||
t.Error("the internal authority certified a name nobody routed here")
|
||||
}
|
||||
}
|
||||
|
||||
// A route withdrawn stops being certifiable, without the proxy restarting.
|
||||
func TestWithdrawingARouteWithdrawsItsCertificate(t *testing.T) {
|
||||
held := newTable()
|
||||
|
||||
Reference in New Issue
Block a user