M2 §5: pin auth attestation into InstalledMetadata and inject AUTH_PINNED_FINGERPRINT

Fetch the auth service's key attestation over mTLS at install time and
persist the fingerprint (plus kid/issued-at) in the install record. At
start, inject AUTH_PINNED_FINGERPRINT into every non-oneoff service so
the shared authclient refuses a substituted key instead of silently
trusting it (TOFU/D3). Best-effort: installs that cannot reach the auth
service backfill the pin on the next start. Skip pinning for the auth
service itself (it is the attester).
This commit is contained in:
2026-08-15 23:43:02 +01:00
parent 950b6e5827
commit 77bd21aea2
3 changed files with 96 additions and 2 deletions
+51 -2
View File
@@ -18,6 +18,7 @@ package containers
import (
"context"
"fmt"
"path/filepath"
"strings"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/docker"
@@ -64,7 +65,7 @@ func StartContainer(ctx context.Context, item types.RegistryContainer) error {
// downloaded from git never declares these mounts — Maestro injects them
// (security theme M1 §2). Failing to provision identity is fatal: a peer
// without a cert cannot authenticate on the mTLS edges.
if err := injectServiceIdentity(project, serviceName); err != nil {
if err := injectServiceIdentity(ctx, project, serviceName); err != nil {
return err
}
@@ -108,7 +109,12 @@ func StartContainer(ctx context.Context, item types.RegistryContainer) error {
// cert/key, and the mTLS env vars into every non-oneoff compose service. The
// env vars follow the shared-module contract (AUTH_CA_BUNDLE/AUTH_CLIENT_CERT/
// AUTH_CLIENT_KEY) so consumers need no per-service config.
func injectServiceIdentity(project *composetypes.Project, serviceName string) error {
//
// It also injects AUTH_PINNED_FINGERPRINT (M2 §5 TOFU): the auth signing key's
// fingerprint recorded at install. A service that already has the pin skips the
// fetch; one installed before this feature (or whose install could not reach
// the auth service) backfills it here, best-effort.
func injectServiceIdentity(ctx context.Context, project *composetypes.Project, serviceName string) error {
dataRoot, err := getDataRoot()
if err != nil {
return fmt.Errorf("resolve data root: %w", err)
@@ -135,6 +141,14 @@ func injectServiceIdentity(project *composetypes.Project, serviceName string) er
"AUTH_CLIENT_KEY": identityMountDir + "/tls.key",
}
// The auth service is the attester itself — nothing to pin there. For every
// other peer, pin its signing key (fetch + persist once).
if serviceName != AuthServiceName {
if pinned := pinAuthFingerprint(ctx, serviceName); pinned != "" {
env["AUTH_PINNED_FINGERPRINT"] = pinned
}
}
for name, s := range project.Services {
if isOneoff(s) {
continue
@@ -147,6 +161,41 @@ func injectServiceIdentity(project *composetypes.Project, serviceName string) er
return nil
}
// pinAuthFingerprint returns the service's pinned auth fingerprint, fetching
// and persisting it when the install record does not have one yet (M2 §5).
// Best-effort: when the auth service is unreachable, it returns "" and the
// next start retries.
func pinAuthFingerprint(ctx context.Context, serviceName string) string {
dir, err := getContainerDataDir(serviceName)
if err != nil {
return ""
}
metadata, err := readInstalledMetadata(dir)
if err != nil {
return ""
}
if metadata.AuthFingerprint != "" {
return metadata.AuthFingerprint
}
att, err := fetchAuthAttestation(ctx)
if err != nil {
logger.Warn("Auth pin for %s: attestation fetch failed (will retry at next start): %v", serviceName, err)
return ""
}
if att.Fingerprint == "" {
return ""
}
metadata.AuthFingerprint = att.Fingerprint
metadata.AuthKid = att.Kid
metadata.AuthAttestedAt = att.IssuedAt
if err := writeMetadata(filepath.Join(dir, "metadata.json"), *metadata); err != nil {
logger.Warn("Auth pin for %s: persist failed: %v", serviceName, err)
}
logger.Info("Pinned auth fingerprint %s for %s", att.Fingerprint, serviceName)
return att.Fingerprint
}
// composeVolumes converts "src:dst:ro" mount strings to compose volume configs.
func composeVolumes(mounts []string) []composetypes.ServiceVolumeConfig {
out := make([]composetypes.ServiceVolumeConfig, 0, len(mounts))