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:
- 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). - Package them as a canonical RFC 7517 JWK Set.
- Sign an
AAC-Tenant-AdminJWS (compact,typ aac-tenant-admin+jwt, EdDSA) with the tenant-admin private key. POST /v1/root-keys/ingeston 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)
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-publisherconsole command (equivalent topython -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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aac_trust_anchor_publisher-0.1.0.tar.gz.
File metadata
- Download URL: aac_trust_anchor_publisher-0.1.0.tar.gz
- Upload date:
- Size: 35.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7f366fa303239242df83b8a6777fd289057949fb5187216a26c80a68081a27e7
|
|
| MD5 |
60da82f1251b9dbfaf987eeb373b87c2
|
|
| BLAKE2b-256 |
8d7a96cbeb2b6293502d4a4003b239d37e71dcaa399095b6ae9831e8f0f0c099
|
Provenance
The following attestation bundles were made for aac_trust_anchor_publisher-0.1.0.tar.gz:
Publisher:
release-trust-anchor-publisher.yml on CascadeAuth/aac-prototype
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aac_trust_anchor_publisher-0.1.0.tar.gz -
Subject digest:
7f366fa303239242df83b8a6777fd289057949fb5187216a26c80a68081a27e7 - Sigstore transparency entry: 2203413539
- Sigstore integration time:
-
Permalink:
CascadeAuth/aac-prototype@1ab9b9f328107bc07820af24e793e985524c969c -
Branch / Tag:
refs/tags/aac-trust-anchor-publisher-v0.1.0 - Owner: https://github.com/CascadeAuth
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-trust-anchor-publisher.yml@1ab9b9f328107bc07820af24e793e985524c969c -
Trigger Event:
push
-
Statement type:
File details
Details for the file aac_trust_anchor_publisher-0.1.0-py3-none-any.whl.
File metadata
- Download URL: aac_trust_anchor_publisher-0.1.0-py3-none-any.whl
- Upload date:
- Size: 30.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f9bdf32932d2096977b48660bf0771ca1219abdba2b780627215e77a06d9dafb
|
|
| MD5 |
d421b16a6f902e73802778dfe98a2e34
|
|
| BLAKE2b-256 |
9a4e92f549b7aca0f64ac6fc7c31ac08eec94477cd6af13319adef913bc80972
|
Provenance
The following attestation bundles were made for aac_trust_anchor_publisher-0.1.0-py3-none-any.whl:
Publisher:
release-trust-anchor-publisher.yml on CascadeAuth/aac-prototype
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aac_trust_anchor_publisher-0.1.0-py3-none-any.whl -
Subject digest:
f9bdf32932d2096977b48660bf0771ca1219abdba2b780627215e77a06d9dafb - Sigstore transparency entry: 2203413553
- Sigstore integration time:
-
Permalink:
CascadeAuth/aac-prototype@1ab9b9f328107bc07820af24e793e985524c969c -
Branch / Tag:
refs/tags/aac-trust-anchor-publisher-v0.1.0 - Owner: https://github.com/CascadeAuth
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-trust-anchor-publisher.yml@1ab9b9f328107bc07820af24e793e985524c969c -
Trigger Event:
push
-
Statement type: