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.
164 lines
5.3 KiB
Go
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
|
|
}
|