From dad0a153ffc5f00d5f48721f5dd8f35499cd6604 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 26 Sep 2026 14:26:01 +0200 Subject: [PATCH] route-proxy: a policy refusal is also not-my-token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit autocert checks the host policy before the token and answers 403 — the internal authority does this for every public name, so mail.novox.be's challenge died on the internal manager's probe one commit after it stopped dying on the public one's 404. Both shapes of refusal now fall through to routing; a fifth test pins the 403 case with a refusing policy. --- examples/route-proxy/challenge_test.go | 23 +++++++++++++++++++++++ examples/route-proxy/main.go | 7 +++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/examples/route-proxy/challenge_test.go b/examples/route-proxy/challenge_test.go index 6919f24..3afd141 100644 --- a/examples/route-proxy/challenge_test.go +++ b/examples/route-proxy/challenge_test.go @@ -7,6 +7,8 @@ package main // three behaviours tokenOrRoute exists for. import ( + "context" + "fmt" "net/http" "net/http/httptest" "os" @@ -73,6 +75,27 @@ func TestASecondAuthorityIsProbedBeforeRouting(t *testing.T) { } } +func TestAnAuthorityWhosePolicyRefusesTheNameIsProbedPast(t *testing.T) { + // autocert checks the host policy before the token and answers 403 — the internal authority + // does this for every public name. A policy refusal is as much "not mine" as a missing token: + // the request must still reach plain routing, where the workload's own ACME client answers. + refusing := &autocert.Manager{ + Prompt: autocert.AcceptTOS, + Cache: autocert.DirCache(t.TempDir()), + HostPolicy: func(ctx context.Context, host string) error { + return fmt.Errorf("no internal-only route for %q in this mesh", host) + }, + } + h := tokenOrRoute(routedTo(t, "the workload answered"), refusing) + + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest("GET", "http://mail.example/.well-known/acme-challenge/mailus-token", nil)) + + if rec.Code != http.StatusOK || rec.Body.String() != "the workload answered" { + t.Fatalf("a policy refusal must fall through to routing; got %d %q", rec.Code, rec.Body.String()) + } +} + func TestAnOrdinaryPathNeverTouchesTheChallengeMachinery(t *testing.T) { m := &autocert.Manager{Prompt: autocert.AcceptTOS, Cache: autocert.DirCache(t.TempDir())} h := tokenOrRoute(routedTo(t, "routed"), m) diff --git a/examples/route-proxy/main.go b/examples/route-proxy/main.go index 1e775f0..38d9308 100644 --- a/examples/route-proxy/main.go +++ b/examples/route-proxy/main.go @@ -458,8 +458,11 @@ func tokenOrRoute(routes http.Handler, managers ...*autocert.Manager) http.Handl for _, probe := range probes { buffered := &probedResponse{header: make(http.Header)} probe.ServeHTTP(buffered, r) - if buffered.status == http.StatusNotFound { - continue // not this manager's token + // Two shapes of "not mine": 404, a token this manager is not holding — and 403, a + // name its host policy would never certify at all (autocert checks the policy before + // the token, so the internal authority answers 403 for every public name). + if buffered.status == http.StatusNotFound || buffered.status == http.StatusForbidden { + continue } buffered.replayTo(w) return