Skip to main content

AAC tenant-side trust-anchor publisher daemon: pushes root-key JWK Sets to the AAC control plane.

Project description

aac-trust-anchor-publisher

The tenant-side trust-anchor publisher daemon (Eng Spec §XVI component map). It runs inside the tenant boundary, one instance per tenant (single-writer; replicas: 1 per §X — HA via leader election is a V2 concern), and keeps the AAC control plane's copy of the tenant's trust anchors current:

  1. Read the tenant's root signing public keys from a RootKeyPublisherSource (filesystem in Stage 2; the §IX.5 KMS adapters — Vault Transit, AWS KMS, Azure Key Vault, GCP KMS — are backlogged pilot work).
  2. Package them as a canonical RFC 7517 JWK Set.
  3. Sign an AAC-Tenant-Admin JWS (compact, typ aac-tenant-admin+jwt, EdDSA) with the tenant-admin private key.
  4. POST /v1/root-keys/ingest on the control plane — at startup and whenever the key set changes.

Since Week 11 the SAME daemon also carries the spiffe-bundle role (the §IX.5 TrustBundleSource role — there is no separate SPIFFE publisher): it reads the tenant's SPIFFE CA anchors from a SpiffeBundlePublisherSource (filesystem in Stage 2; SPIRE/KMS adapters are backlogged pilot work), packages them as the unified trust-anchor bundle JSON (Workload Identity Bootstrap §7), signs the same AAC-Tenant-Admin JWS with artifact_class: spiffe_bundle, and POST /v1/spiffe-bundle/ingests at startup + on change.

The two roles are PEERS (B105): each activates via its own both-or-neither env-var pair, and a tenant configures only the roles it plays — root-keys for a tenant that MINTS chain roots, spiffe-bundle for a tenant whose workloads PRESENT. A forwarding-only tenant (e.g. JPMorgan in Wedge A) runs a spiffe-bundle-only publisher. Configuring zero roles is a boot error.

Verifiers never talk to the publisher: sidecars fetch the published artifacts from the control plane's public GET /.well-known/aac-root-keys/{tenant_id} and GET /.well-known/spiffe-bundle/{tenant_id}.

Terminology: JWSs are signed/created — "mint" is reserved for macaroon/token operations in AAC vocabulary.

What this daemon holds (and doesn't)

  • It holds the tenant-admin private key — the credential that authenticates publishes to the control plane. That is the ONLY private key it touches.
  • It reads public root-key material only. Root signing private keys never pass through it (and with KMS sources, even the admin JWS should eventually be signed by the KMS's sign API — see Backlog).
  • The control plane never sees any tenant private key, ever.

Configuration (env vars — control-plane idiom, §XVI posture (f))

Always required:

Variable Meaning
AAC_TAP_TENANT_ID This tenant's id (the JWS iss claim).
AAC_TAP_ADMIN_KEY_FILE Path to the tenant-admin Ed25519 private key PEM.

Root-keys role (both-or-neither; setting exactly one is a boot error):

Variable Meaning
AAC_TAP_ROOT_KEYS_DIR Directory of <key_id>.pub.pem root signing public keys.
AAC_TAP_ROOT_KEYS_INGEST_URL Full root-keys ingest URL, e.g. https://aac-cloud.example/v1/root-keys/ingest.

Spiffe-bundle role (both-or-neither; setting exactly one is a boot error):

Variable Meaning
AAC_TAP_SPIFFE_BUNDLE_DIR Directory of <anchor_id>.ca.pem SPIFFE CA certificates.
AAC_TAP_SPIFFE_BUNDLE_INGEST_URL Full spiffe-bundle ingest URL, e.g. https://aac-cloud.example/v1/spiffe-bundle/ingest.

At least one role must be configured (zero roles = boot error, exit 2). The env-var shape is AAC_TAP_<ROLE>_<THING>, with the role prefix matching the wire artifact_class (root_keys / spiffe_bundle).

Optional: AAC_TAP_POLL_INTERVAL_SECONDS (60), AAC_TAP_REQUEST_TIMEOUT_SECONDS (10), AAC_TAP_LOG_LEVEL (info).

The admin public key must be registered with the control plane first (tenant_admin_pubkey_pem on POST /v1/tenants, rotate via PATCH /v1/tenants/{tenant_id}); the daemon derives its own kid (tadm_ + SPKI fingerprint) offline — no registration round-trip.

Key directory convention

<key_id>.pub.pem, one file per root signing public key (Ed25519, or EC/P-256 for the §2.1 KMS-fallback case). The whole stem is the key_id — dots allowed. This differs deliberately from the VERIFIER-side sidecar convention <tenant_id>.<key_id>.pub.pem: the publisher's directory holds one tenant's own keys, so filenames don't carry a tenant id (config does).

Rotation workflow: drop the new <key_id>.pub.pem alongside the old one (transition set — §2.1 rule 4 requires retaining at least one previously-active key), wait a poll interval, then delete the old file; the control plane serves the removed key as DEPRECATED for the 24h grace window.

Bundle directory convention (spiffe-bundle role)

<anchor_id>.ca.pem, one file per SPIFFE CA certificate (must parse, carry BasicConstraints CA=TRUE, and be unexpired — invalid files are skipped with a warning). The whole stem is the anchor_id. The filesystem source reports source: "filesystem" and derives spiffe_id_pattern as spiffe://<tenant_id>/* — per-anchor patterns arrive with the real SPIRE/KMS adapters. CA rotation is whole-document: drop the new CA alongside the old (one bundle carries both), then delete the old file once downstream SVIDs have rolled — no server-side grace window (the bundle replaces atomically; overlap is expressed IN the document).

Installing (two channels, same daemon — B115)

Step-by-step tenant-operator runbook (both channels, systemd setup, success checks): INSTALL.md.

The daemon releases in dual mode; pick the channel that matches your deployment:

  • Container (Kubernetes / Docker Compose tenants):

    docker pull ghcr.io/cascadeauth/aac-trust-anchor-publisher:latest
    
  • pip (non-container hosts — the daemon then runs under systemd, see below):

    pip install aac-trust-anchor-publisher
    

    Installing the wheel also generates the aac-trust-anchor-publisher console command (equivalent to python -m trust_anchor_publisher).

Repo developers use neither: uv run python -m trust_anchor_publisher from the workspace.

Running

python -m trust_anchor_publisher        # repo / module form
aac-trust-anchor-publisher              # pip-installed console command

Either form runs the same long-lived foreground process: forced publish at startup, then poll-and-republish-on-change forever. It does NOT run-once-and-exit — in a terminal it sits until Ctrl-C; in production a supervisor (systemd or a container runtime) owns its lifecycle.

Container: bind-mount the key directory and admin key read-only and set the env vars. The daemon needs outbound HTTPS to the control plane and no inbound ports.

Boot posture: local misconfiguration (bad env, unreadable/non-Ed25519 admin key) exits 2 immediately; an unreachable or rejecting control plane is survivable — the poll loop is the retry mechanism.

Deploying as a systemd service (non-container hosts)

The publisher is a "new-style daemon": it deliberately stays a plain foreground process (no self-forking, no pidfile, stdout/stderr logging) and lets systemd supply the daemonization — start at boot, restart on failure, journal capture. A complete, commented example unit ships at systemd/aac-trust-anchor-publisher.service; the short version:

# one-time setup (as root)
python -m venv /opt/aac/venv
/opt/aac/venv/bin/pip install aac-trust-anchor-publisher
useradd --system --home /var/lib/aac aac
# write /etc/aac/trust-anchor-publisher.env with the AAC_TAP_* vars (chmod 600)
# the DAEMON user (not systemd) opens the admin key — make it readable:
chown root:aac /etc/aac/keys/tenant-admin.pem
chmod 640 /etc/aac/keys/tenant-admin.pem
cp trust_anchor_publisher/systemd/aac-trust-anchor-publisher.service /etc/systemd/system/  # from the repo root (or copy from GitHub)
systemctl daemon-reload
systemctl enable --now aac-trust-anchor-publisher
journalctl -u aac-trust-anchor-publisher -f

Unit-file details worth keeping: Restart=on-failure with RestartPreventExitStatus=2 (exit 2 = local misconfiguration — a restart loop cannot fix a missing env var or an unreadable admin key; fail visibly instead of flapping), EnvironmentFile= for the AAC_TAP_* config, a dedicated non-root User=aac, and read-only sandboxing (ProtectSystem=strict — the daemon never writes the filesystem).

Sequence strategy (recorded trade-off)

Sequences are timestamp-derived (max(unix_now, last_sent + 1)) — stateless across restarts, no control-plane read surface (Week 10 PR-B design Q1, option c). Known limits, accepted for Stage 2 and backlogged (medium-high, pre-pilot): a clock stepped backward past the last accepted value stalls publishing with ERR_INGEST_SEQUENCE_STALE until wall time catches up; concurrent publisher instances for one tenant have no coordination (single-writer deployment is the §X contract). The upgrade path — an authoritative last-sequence read + increment — needs no wire-contract change.

Parity copy notice

ingest_jws.py deliberately duplicates the signing half of control_plane/ingest_auth.py: the two deployables never import each other (§XVI). tests/test_ingest_jws_parity.py enforces byte-identical output; change one side and the test names the drift.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aac_trust_anchor_publisher-0.1.1.tar.gz (35.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aac_trust_anchor_publisher-0.1.1-py3-none-any.whl (30.9 kB view details)

Uploaded Python 3

File details

Details for the file aac_trust_anchor_publisher-0.1.1.tar.gz.

File metadata

File hashes

Hashes for aac_trust_anchor_publisher-0.1.1.tar.gz
Algorithm Hash digest
SHA256 db9a91a543de577ca4a37a5ef8a115e6102d4b15cac17bf11a766be1fb848bc3
MD5 b0384c805c5a65bb559b84f81cda82eb
BLAKE2b-256 637fcefa7f6bd498411380a0eb91719892da1b6893250b2dc0a354ca539687d6

See more details on using hashes here.

Provenance

The following attestation bundles were made for aac_trust_anchor_publisher-0.1.1.tar.gz:

Publisher: release-trust-anchor-publisher.yml on CascadeAuth/aac-prototype

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file aac_trust_anchor_publisher-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for aac_trust_anchor_publisher-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 dc0dd9df165a7fc3be9eae13faab7885662804e904b72c73008d6a3ad9c6ca0d
MD5 70692c43f9edd9fc08a1753ce4cec87a
BLAKE2b-256 554edf9a6af15cec6d6d88267ee1659fff41a49a561002f4334a050bd666e94d

See more details on using hashes here.

Provenance

The following attestation bundles were made for aac_trust_anchor_publisher-0.1.1-py3-none-any.whl:

Publisher: release-trust-anchor-publisher.yml on CascadeAuth/aac-prototype

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page