[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:
@@ -15,16 +15,23 @@
|
||||
|
||||
// Package instance holds the instance's public identity: a small, self-
|
||||
// contained file at the data root that answers "who am I" for the hub app's
|
||||
// public profile (hub-app theme M1) and, later, federation attestation.
|
||||
// public profile (hub-app theme M1). Rev 9 removed federation, so the
|
||||
// fingerprint here is informational only (it is no longer a trust anchor).
|
||||
package instance
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// keyFileName is the persisted random instance key backing Fingerprint.
|
||||
const keyFileName = "instance.key"
|
||||
|
||||
// Identity is the instance's user-visible self-description. Name falls back to
|
||||
// DCNTRLIZED_INSTANCE_NAME and then the hostname when instance.json is absent;
|
||||
// Title and Description are optional and only ever come from the file.
|
||||
@@ -39,6 +46,55 @@ func Path(dataRoot string) string {
|
||||
return filepath.Join(dataRoot, "instance.json")
|
||||
}
|
||||
|
||||
// Fingerprint returns the instance's stable app-level identity fingerprint in
|
||||
// the same "sha256:ab:cd:…" shape the CA fingerprint used to have. Rev 9
|
||||
// removed the PKI layer, so the value is derived from a persisted 32-byte
|
||||
// random key at <dataRoot>/instance.key (created on first use, chmod 600) —
|
||||
// not from any certificate. It is unauthenticated by design; a consumer can
|
||||
// still compare it to detect a changed peer. Returns "" when the key cannot be
|
||||
// read or created (the public profile then omits the field).
|
||||
func Fingerprint(dataRoot string) string {
|
||||
key, err := loadOrCreateKey(dataRoot)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
sum := sha256.Sum256(key)
|
||||
hexStr := hex.EncodeToString(sum[:])
|
||||
b := make([]byte, 0, len(hexStr)+len(hexStr)/2+7)
|
||||
b = append(b, "sha256:"...)
|
||||
for i := 0; i < len(hexStr); i += 2 {
|
||||
if i > 0 {
|
||||
b = append(b, ':')
|
||||
}
|
||||
b = append(b, hexStr[i:i+2]...)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// loadOrCreateKey reads <dataRoot>/instance.key, generating it atomically on
|
||||
// first use.
|
||||
func loadOrCreateKey(dataRoot string) ([]byte, error) {
|
||||
path := filepath.Join(dataRoot, keyFileName)
|
||||
if data, err := os.ReadFile(path); err == nil && len(data) == 32 {
|
||||
return data, nil
|
||||
}
|
||||
key := make([]byte, 32)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return nil, fmt.Errorf("generate instance key: %w", err)
|
||||
}
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, key, 0o600); err != nil {
|
||||
return nil, fmt.Errorf("write instance key: %w", err)
|
||||
}
|
||||
if err := os.Chmod(tmp, 0o600); err != nil {
|
||||
return nil, fmt.Errorf("chmod instance key: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmp, path); err != nil {
|
||||
return nil, fmt.Errorf("replace instance key: %w", err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// Load resolves the instance identity: instance.json when present, else
|
||||
// DCNTRLIZED_INSTANCE_NAME, else os.Hostname(). Read-on-demand by design —
|
||||
// no daemon state, so an edited file shows up on the next request.
|
||||
@@ -61,9 +117,8 @@ func Load(dataRoot string) Identity {
|
||||
}
|
||||
|
||||
// Save atomically writes the identity file (tmp + rename), chmod 600 like the
|
||||
// rest of Maestro's file stores. The owner-gated edit endpoint that uses this
|
||||
// lands with the federation theme; the helper exists so both callers share one
|
||||
// write path.
|
||||
// rest of Maestro's file stores. The helper exists so the create and edit
|
||||
// paths share one write path.
|
||||
func Save(dataRoot string, id Identity) error {
|
||||
if id.Name == "" {
|
||||
return fmt.Errorf("instance name must not be empty")
|
||||
@@ -90,8 +145,7 @@ func defaultIdentity() Identity {
|
||||
if name := os.Getenv("DCNTRLIZED_INSTANCE_NAME"); name != "" {
|
||||
return Identity{Name: name}
|
||||
}
|
||||
// The domain the instance announces as is its natural public name;
|
||||
// federation attestation will reuse it.
|
||||
// The domain the instance announces as is its natural public name.
|
||||
if domain := os.Getenv("DCNTRLIZED_INSTANCE_DOMAIN"); domain != "" {
|
||||
return Identity{Name: domain}
|
||||
}
|
||||
|
||||
@@ -77,12 +77,12 @@ func TestDefaultIdentityFallbacks(t *testing.T) {
|
||||
|
||||
func TestLooksLikeContainerID(t *testing.T) {
|
||||
cases := map[string]bool{
|
||||
"6079761d6e4d": true,
|
||||
"ABCDEF0123456789abcdef0123456789": true,
|
||||
"archlinux": false,
|
||||
"maestro-1": false,
|
||||
"abc": false,
|
||||
"": false,
|
||||
"6079761d6e4d": true,
|
||||
"ABCDEF0123456789abcdef0123456789": true,
|
||||
"archlinux": false,
|
||||
"maestro-1": false,
|
||||
"abc": false,
|
||||
"": false,
|
||||
}
|
||||
for s, want := range cases {
|
||||
if got := looksLikeContainerID(s); got != want {
|
||||
|
||||
Reference in New Issue
Block a user