The web front kept nothing on the bus: whether grafana or drive was routed, where to, with which certificate, and what it last answered could only be learned with curl or the container's log. The proxy now keeps `routes` and `unrouted` (announced readable, keyed <node>.<name>), written on change by one writer: route set changes, certificates stored by autocert's cache or failing at a handshake, upstream answers that differ from the last written (or are a minute old), and names refused in a request or a handshake. The provider helper's bundle serves route_proxy_routes, route_proxy_route and route_proxy_unrouted from those states. Quick: log the routes at start and grep the journal. Thorough: the state on the bus and the tools that read it, mesh-wide, in one call. Chose thorough: the operator's question is mesh-wide and recurs, and the log says nothing about certificates or answers. Tests: two table tests added (the ledger, the tools), both failing without the change; four authority tests and four skipped-route tests merged into two, so the module has four fewer.
76 lines
3.7 KiB
Go
76 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The lab's first root, and the one a re-initialised CA generates in its place.
|
|
const (
|
|
firstRoot = "-----BEGIN CERTIFICATE-----\nMIIBeFIRST\n-----END CERTIFICATE-----\n"
|
|
secondRoot = "-----BEGIN CERTIFICATE-----\nMIIBeSECOND\n-----END CERTIFICATE-----\n"
|
|
)
|
|
|
|
// A re-initialised CA does not need somebody to delete the cache by hand.
|
|
//
|
|
// autocert keeps its account key at one fixed name and reuses it for ever. When an internal CA is
|
|
// re-initialised it has never heard of that account, rejects every use of it, and autocert has no
|
|
// path back: nothing is re-registered, no order reaches the CA, and issuance stops with nothing
|
|
// saying why. Naming the cache after the authority means the account is only ever found where it is
|
|
// still valid — the new root lands in a directory with no account in it, and autocert registers.
|
|
func TestTheAccountCacheIsNamedAfterItsAuthority(t *testing.T) {
|
|
t.Run("a new CA root means a new account cache", func(t *testing.T) {
|
|
const directory = "https://anchor.internal/acme/acme/directory"
|
|
before := forThisAuthority("/var/lib/route-proxy/acme", directory, []byte(firstRoot))
|
|
after := forThisAuthority("/var/lib/route-proxy/acme", directory, []byte(secondRoot))
|
|
if before == after {
|
|
t.Fatalf("a re-initialised CA reuses the account it was rejected for: %s", before)
|
|
}
|
|
})
|
|
|
|
// And the SAME authority keeps the account it registered, restart after restart.
|
|
//
|
|
// This is the whole reason ACME_CACHE is required in the first place: an account and its
|
|
// certificates that did not persist would be re-ordered on every restart, which works silently until
|
|
// a rate limit says it does not. Whitespace around the delivered root is not a new authority — the
|
|
// mesh writes that file, and a trailing newline coming or going must not throw away an account.
|
|
t.Run("the same authority keeps its account", func(t *testing.T) {
|
|
const directory = "https://anchor.internal/acme/acme/directory"
|
|
first := forThisAuthority("/var/lib/route-proxy/acme", directory, []byte(firstRoot))
|
|
again := forThisAuthority("/var/lib/route-proxy/acme", directory, []byte("\n"+firstRoot+"\n\n"))
|
|
if first != again {
|
|
t.Errorf("the same authority was given two caches, so every restart orders again:\n%s\n%s",
|
|
first, again)
|
|
}
|
|
})
|
|
|
|
// Staging and production are different authorities, and were sharing one account.
|
|
//
|
|
// The latent fault of the same shape: pointing ACME_DIRECTORY at production after testing against
|
|
// staging reused the staging account, because the cache had no idea they were different.
|
|
t.Run("staging and production do not share an account", func(t *testing.T) {
|
|
staging := forThisAuthority("/acme", stagingDirectory, nil)
|
|
production := forThisAuthority("/acme", "https://acme-v02.api.letsencrypt.org/directory", nil)
|
|
if staging == production {
|
|
t.Errorf("two issuers share one account: %s", staging)
|
|
}
|
|
})
|
|
|
|
// It stays inside the directory the mesh gave it, and is a plain name.
|
|
//
|
|
// The mesh owns ACME_CACHE and mounts it; a name derived from a certificate that escaped it — or
|
|
// that carried a separator out of the PEM — would put an account somewhere nothing persists.
|
|
t.Run("the account cache stays where the mesh put it", func(t *testing.T) {
|
|
const cache = "/var/lib/route-proxy/acme"
|
|
got := forThisAuthority(cache, "https://anchor.internal/acme/acme/directory", []byte(firstRoot))
|
|
if !strings.HasPrefix(got, cache+"/") {
|
|
t.Fatalf("the account cache is not under %s: %s", cache, got)
|
|
}
|
|
name := strings.TrimPrefix(got, cache+"/")
|
|
if name != filepath.Base(got) || strings.ContainsAny(name, "/.") {
|
|
t.Errorf("the account cache is not a plain name: %q", name)
|
|
}
|
|
})
|
|
}
|