[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:
+47
-187
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user