Files
Maestro/src/lib/authmgmt/client.go
T
Admin 17d5307110 [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.
2026-10-19 02:39:15 +01:00

164 lines
5.3 KiB
Go

// Copyright (C) 2026 Dcntrlized
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// Package authmgmt is Maestro's client for the shared auth service's
// management endpoints (security theme M2 §3). Maestro is only the CA and the
// UI here: it lists pending first-contact requests, forwards the owner's
// approve/revoke decisions to the auth service, and reads its key
// attestation. Enforcement always stays inside the auth service (D5).
package authmgmt
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"time"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/configsign"
"git.dcntrlized.org/Dcntrlized/Maestro/lib/env"
)
// AuthServiceURL is the default DNS name of the shared auth service on the
// 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 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 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"`
Kid string `json:"kid"`
IssuedAt string `json:"issued_at"`
}
// 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) {
baseURL := ""
if env.IsDev() {
baseURL = os.Getenv("DCNTRLIZED_AUTH_URL")
}
if baseURL == "" {
baseURL = os.Getenv("AUTH_SERVICE_URL")
}
if baseURL == "" {
baseURL = AuthServiceURL
}
return &Client{
baseURL: baseURL,
http: &http.Client{
Timeout: 10 * time.Second,
},
}, nil
}
// 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 {
return "http"
}
// 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
}
signer, err := configsign.LoadOrCreate(root)
if err != nil {
return "", err
}
return signer.Mint(serviceName, time.Now())
}
// 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
}
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.
func (c *Client) Attestation(ctx context.Context) (*Attestation, error) {
var att Attestation
if err := c.do(ctx, http.MethodGet, "/internal/attestation", nil, &att); err != nil {
return nil, err
}
return &att, nil
}
func (c *Client) do(ctx context.Context, method, path string, body, out any) error {
var payload []byte
if body != nil {
payload, _ = json.Marshal(body)
}
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bytes.NewReader(payload))
if err != nil {
return err
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.http.Do(req)
if err != nil {
return fmt.Errorf("auth service %s %s: %w", method, path, err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("auth service %s %s: status %d", method, path, resp.StatusCode)
}
if out != nil {
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
return fmt.Errorf("decode auth service response: %w", err)
}
}
return nil
}