[refactor](maestro): retire the PKI and federation identity layer

Contract rev 9 removes the instance CA, client certificates, the peer listener and the trust store fleet-wide. Internal traffic is plaintext HTTP on each service single :8080 listener, so there is no peer identity left to authorize: delete lib/pki, lib/federation, devboot, the per-service database role model and the /api/peers* and /api/instances surfaces, and drop the console Trusted services UI. Config delivery now rides a dedicated ES256 token from lib/configsign rather than mTLS, and the instance fingerprint is derived from a persisted random key and is informational only. main.go also wires the surfaces introduced by the later commits, which this change is what makes possible.
This commit is contained in:
2026-10-19 02:39:15 +01:00
parent 3220944b12
commit 17d5307110
29 changed files with 735 additions and 2512 deletions
+47 -187
View File
@@ -23,8 +23,6 @@ package authmgmt
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"net/http"
@@ -32,26 +30,27 @@ import (
"path/filepath"
"time"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/configsign"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/env"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/logger"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/pki"
)
// AuthServiceURL is the default DNS name of the shared auth service on the
// dcntrlized network. The auth service listens with ClientAuth=RequireAndVerify
// trusting the instance CA (M2 §1).
const AuthServiceURL = "https://dcntrlized-auth:8080"
// dcntrlized network. After rev 9 the fleet-wide internal transport is
// plaintext HTTP on the single :8080 listener; there is no mTLS anywhere.
const AuthServiceURL = "http://dcntrlized-auth:8080"
// Client talks to the auth service over mTLS using Maestro's own identity
// certificate (CN=maestro), which the auth service's RequireMaestro gate
// recognizes as the orchestrator.
// Client talks to the auth service over plaintext HTTP. It no longer presents
// any client certificate: the peer gate was removed fleet-wide in rev 9.
type Client struct {
baseURL string
http *http.Client
}
// Attestation is the auth service's public signing key statement (D3): the key
// a peer should verify access tokens with, plus its TOFU-pinnable fingerprint.
// a peer should verify access tokens with, plus the fingerprint Maestro pins
// as an app-level check (M2 §5). Rev 9 removed the peer trust store, the
// pending/approved authorization flow and every management call that used to
// live in this package.
type Attestation struct {
PublicKey string `json:"public_key"`
Fingerprint string `json:"fingerprint"`
@@ -59,44 +58,9 @@ type Attestation struct {
IssuedAt string `json:"issued_at"`
}
// AuthorizedKey is an approved peer row of the auth service's trust store.
type AuthorizedKey struct {
Service string `json:"service"`
Fingerprint string `json:"fingerprint"`
ApprovedBy string `json:"approved_by"`
ApprovedAt string `json:"approved_at"`
RevokedAt *string `json:"revoked_at"`
HiddenAt *string `json:"hidden_at"`
}
// PendingAuthorization is a first-contact request awaiting the owner's
// decision.
type PendingAuthorization struct {
Service string `json:"service"`
Fingerprint string `json:"fingerprint"`
FirstSeen string `json:"first_seen"`
LastSeen string `json:"last_seen"`
}
// TrustState is the combined pending + approved view the dashboard shows.
type TrustState struct {
Pending []PendingAuthorization `json:"pending"`
Authorized []AuthorizedKey `json:"authorized"`
}
type authzRequest struct {
Service string `json:"service"`
Fingerprint string `json:"fingerprint"`
}
// New builds the mTLS management client, provisioning Maestro's own identity
// from the instance CA (the CA holder issues its own orchestrator cert).
// New builds the plaintext management client. There is no identity to
// provision and no transport to configure after the PKI removal.
func New() (*Client, error) {
transport, err := maestroTransport()
if err != nil {
return nil, err
}
baseURL := ""
if env.IsDev() {
baseURL = os.Getenv("DCNTRLIZED_AUTH_URL")
@@ -111,84 +75,52 @@ func New() (*Client, error) {
return &Client{
baseURL: baseURL,
http: &http.Client{
Timeout: 10 * time.Second,
Transport: transport,
Timeout: 10 * time.Second,
},
}, nil
}
// maestroTransport builds the mutual-TLS transport Maestro uses as a peer: its
// own identity certificate (CN=maestro) signed by the instance CA, trusting the
// CA as root. This is the client side of every Maestro peer edge.
func maestroTransport() (*http.Transport, error) {
dataRoot, err := dataRoot()
if err != nil {
return nil, fmt.Errorf("resolve data root: %w", err)
}
ca, err := pki.EnsureCA(dataRoot)
if err != nil {
return nil, fmt.Errorf("ensure instance CA: %w", err)
}
identity, err := pki.EnsureServiceIdentity(ca, "maestro")
if err != nil {
return nil, fmt.Errorf("provision maestro identity: %w", err)
}
cert, err := tls.LoadX509KeyPair(identity.CertPath, identity.KeyPath)
if err != nil {
return nil, fmt.Errorf("load maestro identity: %w", err)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(ca.CertPEM) {
return nil, fmt.Errorf("no certificates parsed from instance CA")
}
return &http.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: pool,
MinVersion: tls.VersionTLS12,
},
}, nil
}
// Scheme returns "http" in dev mode and "https" otherwise, for building
// satellite peer URLs (RunAction, manifest fetch). Dev satellites keep a
// plaintext listener so the RN clients (which cannot do mTLS) can reach them;
// production satellites bind mTLS listeners.
// Scheme returns the scheme every satellite peer edge uses after rev 9:
// always "http" (plaintext on the single :8080 listener). Kept as a function
// so the call sites (manifest fetch, action routing) do not hard-code it.
func Scheme() string {
if env.IsDev() {
return "http"
}
return "https"
return "http"
}
// PeerTransport returns the transport Maestro uses for the satellite peer
// edges (M2 §2): plaintext in dev mode (matching the RN-facing listeners),
// mutual TLS with Maestro's identity in production.
func PeerTransport() (*http.Transport, error) {
if env.IsDev() {
return &http.Transport{}, nil
// ConfigToken mints a fresh, single-use ES256 config token for one service
// (contract rev 9 §17.3.6). It loads (or creates) Maestro's DEDICATED
// config-signing keypair from the data root and returns the compact JWS. The
// service holds only the public half (DCNTRLIZED_CONFIG_PUBKEY) and verifies;
// Maestro only ever mints. Wire format is frozen in §17.3.3.
func ConfigToken(serviceName string) (string, error) {
root, err := dataRoot()
if err != nil {
return "", err
}
return maestroTransport()
signer, err := configsign.LoadOrCreate(root)
if err != nil {
return "", err
}
return signer.Mint(serviceName, time.Now())
}
// MaestroTransport returns Maestro's own mutual-TLS transport (CN=maestro)
// unconditionally — dev mode does NOT downgrade it. Orchestrator-only edges
// that must prove the caller is the orchestrator (e.g. /config delivery, which
// the service gates to CN=maestro) use this regardless of FORCE_JSON/DEV,
// since those services keep an always-mTLS peer listener.
func MaestroTransport() (*http.Transport, error) {
return maestroTransport()
}
// TrustState returns the pending authorizations and the approved trust store.
func (c *Client) TrustState(ctx context.Context) (*TrustState, error) {
var state TrustState
if err := c.do(ctx, http.MethodGet, "/internal/pending", nil, &state); err != nil {
return nil, err
// dataRoot resolves the directory holding Maestro's private key material
// (DCNTRLIZED_INSTALL_DIR → XDG → HOME), mirroring the containers package's
// resolution. Kept local to avoid an import cycle: containers imports authmgmt.
func dataRoot() (string, error) {
if dir := os.Getenv("DCNTRLIZED_INSTALL_DIR"); dir != "" {
return dir, nil
}
return &state, nil
if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" {
return filepath.Join(xdg, ".dcntrlized"), nil
}
if xdgHome := os.Getenv("XDG_HOME_DIR"); xdgHome != "" {
return filepath.Join(xdgHome, ".dcntrlized"), nil
}
if home := os.Getenv("HOME"); home != "" {
return filepath.Join(home, ".dcntrlized"), nil
}
return filepath.Join("/data", ".dcntrlized"), nil
}
// Attestation returns the auth service's public key statement.
@@ -200,35 +132,6 @@ func (c *Client) Attestation(ctx context.Context) (*Attestation, error) {
return &att, nil
}
// Approve forwards the owner's approval for a peer's fingerprint to the auth
// service's own trust store.
func (c *Client) Approve(ctx context.Context, service, fingerprint string) error {
return c.do(ctx, http.MethodPost, "/internal/authorizations",
authzRequest{Service: service, Fingerprint: fingerprint}, nil)
}
// Revoke forwards the owner's revocation for a peer's fingerprint (returns the
// peer to the no-op path immediately).
func (c *Client) Revoke(ctx context.Context, service, fingerprint string) error {
return c.do(ctx, http.MethodPost, "/internal/revoke",
authzRequest{Service: service, Fingerprint: fingerprint}, nil)
}
// Reject denies a pending first-contact request: the request is dropped and
// audited; the peer stays on the no-op path until the owner approves it.
func (c *Client) Reject(ctx context.Context, service, fingerprint string) error {
return c.do(ctx, http.MethodPost, "/internal/reject",
authzRequest{Service: service, Fingerprint: fingerprint}, nil)
}
// Hide permanently hides a revoked peer from the dashboard: the auth service
// keeps the row (audit trail survives) but marks it hidden_at; a re-approval
// clears the flag and makes the key visible again.
func (c *Client) Hide(ctx context.Context, service, fingerprint string) error {
return c.do(ctx, http.MethodPost, "/internal/hide",
authzRequest{Service: service, Fingerprint: fingerprint}, nil)
}
func (c *Client) do(ctx context.Context, method, path string, body, out any) error {
var payload []byte
if body != nil {
@@ -258,46 +161,3 @@ func (c *Client) do(ctx context.Context, method, path string, body, out any) err
}
return nil
}
// RevokeServiceBestEffort revokes a peer's authorization in the auth service
// trust store, logging and swallowing failures: uninstall must proceed even
// when the auth service is down (M2 §3 step 5 — "or automatically on
// uninstall").
func RevokeServiceBestEffort(ctx context.Context, service, fingerprint string) {
client, err := New()
if err != nil {
bestEffort(err, "build client for revoking "+service)
return
}
if err := client.Revoke(ctx, service, fingerprint); err != nil {
bestEffort(err, "revoke "+service)
}
}
// dataRoot resolves the directory storing the instance PKI, mirroring the
// containers package's resolution (DCNTRLIZED_INSTALL_DIR → XDG → HOME). Kept
// local to avoid an import cycle: containers imports authmgmt for the
// uninstall-revoke hook.
func dataRoot() (string, error) {
if dir := os.Getenv("DCNTRLIZED_INSTALL_DIR"); dir != "" {
return dir, nil
}
if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" {
return filepath.Join(xdg, ".dcntrlized"), nil
}
if xdgHome := os.Getenv("XDG_HOME_DIR"); xdgHome != "" {
return filepath.Join(xdgHome, ".dcntrlized"), nil
}
if home := os.Getenv("HOME"); home != "" {
return filepath.Join(home, ".dcntrlized"), nil
}
return filepath.Join("/data", ".dcntrlized"), nil
}
// bestEffort logs-and-continues: management calls must never take down the
// orchestrator (e.g. uninstall revoking while the auth service is down).
func bestEffort(err error, what string) {
if err != nil {
logger.Warn("authmgmt: %s failed (best-effort): %v", what, err)
}
}