Portage Engine schema v23 separates authentication from authorization, makes project admission policy durable, and enforces federated session revocation across control-plane replicas:
- configured OIDC and GitHub providers prove an exact
(issuer, subject)identity; - upstream credentials are exchanged once for a random Portage Engine session;
- PostgreSQL
project_membershipsgrants that subject a project role; - every durable build records both
project_idandrequested_by_subject_id; - project selection is explicit through
X-Project-IDwhen an identity has more than one membership.
Mutable token claims such as username, email, groups and roles never grant access. A verified email may be retained only as display metadata.
| Role | Project capability |
|---|---|
viewer |
Read project builds, logs, overview and live events |
developer |
Viewer plus submit builds |
maintainer |
Developer plus cancel, retry and delete project jobs |
owner |
Maintainer plus member management and project admission policy |
system-admin is not a project role. It is reserved for exact subjects listed
in OIDC_ADMIN_SUBJECTS and, during migration, the legacy API key. It protects
cloud settings, builders/workers, scheduler internals, image factory, runtime
metadata, WebShell and global cleanup operations.
# Compatibility only
AUTH_MODE=legacy
API_KEY=<administrator-secret>
STEP_UP_API_KEY=<independent-administrator-secret>
# Recommended migration mode
AUTH_MODE=hybrid
API_KEY=<break-glass-administrator-secret>
OIDC_ISSUER_URL=https://idp.example.org/realms/portage
OIDC_AUDIENCE=portage-engine
OIDC_ADMIN_SUBJECTS=<exact-bootstrap-subject>
OIDC_SESSION_IDLE_MINUTES=60
OIDC_SESSION_MAX_MINUTES=720
OIDC_STEP_UP_MAX_AGE_MINUTES=10
# OIDC-only after automation and the WebUI no longer need the legacy key
AUTH_MODE=oidc
OIDC_ISSUER_URL=https://idp.example.org/realms/portage
OIDC_AUDIENCE=portage-engine
OIDC_ADMIN_SUBJECTS=<exact-bootstrap-subject>
OIDC_SESSION_IDLE_MINUTES=60
OIDC_SESSION_MAX_MINUTES=720
OIDC_STEP_UP_MAX_AGE_MINUTES=10The issuer must expose standard OIDC discovery and JWKS documents. Portage
Engine verifies issuer, signature, expiry and audience. Subject IDs in
OIDC_ADMIN_SUBJECTS are exact sub values, not usernames.
The server requires PostgreSQL in required mode for hybrid and oidc.
Start with hybrid, prove OIDC login and project isolation, then remove
legacy-key dependencies before switching to oidc.
Community deployments should use the multi-provider registry described in IDENTITY_PROVIDERS.md. Authentik, Google and GitHub are peer login choices; the single OIDC variables below remain a compatibility path for existing deployments.
Register this callback with the identity provider:
https://portage.example.org/auth/oidc/callback
Configure:
AUTH_ENABLED=true
JWT_SECRET=<at-least-32-random-characters>
OIDC_ENABLED=true
OIDC_ISSUER_URL=https://idp.example.org/realms/portage
OIDC_CLIENT_ID=portage-engine
OIDC_CLIENT_SECRET=<empty-for-a-public-client-or-provider-secret>
OIDC_REDIRECT_URL=https://portage.example.org/auth/oidc/callback
COOKIE_SECURE=trueThe Dashboard uses Authorization Code + PKCE S256 and OIDC nonce validation.
It exchanges the upstream credential once at the control plane, encrypts only
the returned pe1_ session in an HttpOnly, SameSite=Lax cookie, and forwards
that opaque platform session on backend requests. The selected project is
forwarded separately in X-Project-ID.
COOKIE_SECURE=true is required for an HTTPS OIDC callback, including the
common case where a reverse proxy terminates TLS and forwards HTTP to the
Dashboard process.
Local ADMIN_USER/ADMIN_PASSWORD may coexist in hybrid mode as a
break-glass system-administrator path. Remove them for an OIDC-only Dashboard.
Use a system-administrator identity to create a project:
curl --fail-with-body \
-H "Authorization: Bearer ${PORTAGE_ENGINE_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name":"gentoo-desktop","description":"Reviewed desktop packages"}' \
https://portage.example.org/api/v1/projectsPre-provision an exact issuer/subject as the first owner:
curl --fail-with-body -X PUT \
-H "Authorization: Bearer ${PORTAGE_ENGINE_TOKEN}" \
-H "X-Project-ID: gentoo-desktop" \
-H "Content-Type: application/json" \
-d '{
"issuer":"https://idp.example.org/realms/portage",
"subject":"exact-provider-subject",
"role":"owner"
}' \
https://portage.example.org/api/v1/projects/membersThe database serializes membership changes per project and refuses to demote or remove the last owner.
List the current identity and its projects:
portage-client whoami \
-server=https://portage.example.org \
-token="${PORTAGE_ENGINE_TOKEN}"Submit within one project:
export PORTAGE_ENGINE_PROJECT=gentoo-desktop
portage-client build \
-server=https://portage.example.org \
-token="${PORTAGE_ENGINE_TOKEN}" \
-package=app-misc/jq -waitEach project has a versioned PostgreSQL policy. View its limits and current usage:
portage-client project-policy \
-server=https://portage.example.org \
-token="${PORTAGE_ENGINE_TOKEN}" \
-project=gentoo-desktopAn owner (or system administrator) can replace it using the returned version:
portage-client project-policy-set \
-server=https://portage.example.org \
-token="${PORTAGE_ENGINE_TOKEN}" \
-project=gentoo-desktop \
-version=1 -max-queued=100 -max-active=4 -max-daily=500 \
-priority-weight=100 -starvation-seconds=300 \
-max-vcpus=32 -max-memory-mib=131072 -max-disk-gib=1000 \
-max-artifact-bytes=34359738368 \
-max-claimed=4 -max-provision=2 -max-build=4 \
-max-verify=2 -max-publish=1 \
-max-daily-build-minutes=1440 \
-max-daily-cloud-cost-microunits=1000000000 \
-max-failures-hour=20 -abuse-cooldown-seconds=3600PUT /api/v1/projects/policy uses optimistic versioning, so two owners cannot
silently overwrite each other. The Dashboard project context displays
queued/limit, active/limit, vCPU/memory/disk reservations, active
quarantine bytes, build-time/cloud-cost usage, active runtime reservations,
trailing-hour failures, UTC-today submissions and both manual/automatic
suspension. It also displays the scheduler weight and anti-starvation
threshold. See Scheduler fairness and autoscaling.
Submission and scheduler claim transactions lock the same project policy row
before checking usage. This is the correctness boundary for multiple API
replicas and workers; Redis is only a short-burst accelerator. A repeated
request with the same project-scoped Idempotency-Key is resolved before
admission, so replay returns the existing job even after a limit is reached or
the project is suspended.
| HTTP | code |
Meaning |
|---|---|---|
423 |
project_suspended |
New submission/retry disabled; queued jobs are not claimed |
423 |
project_abuse_suspended |
Failure-storm cooldown is active; an owner may clear it explicitly |
429 |
queued_limit_reached |
Project queued-job cap is full |
429 |
daily_submission_limit_reached |
UTC calendar-day cap is full |
429 |
resource_request_invalid |
Catalog resource class is missing a required accounting dimension |
429 |
vcpu_request_exceeds_limit |
One job requests more vCPUs than the project permits |
429 |
memory_request_exceeds_limit |
One job requests more memory than the project permits |
429 |
disk_request_exceeds_limit |
One job requests more disk than the project permits |
429 |
build_minute_request_exceeds_daily_limit |
One catalog class cannot fit the project's daily runtime budget |
429 |
cloud_cost_request_exceeds_daily_limit |
One catalog class cannot fit the project's daily estimated-cost budget |
429 |
runtime_budget_request_invalid |
Catalog runtime or cost accounting is missing/invalid |
429 |
authentication_rate_limit |
Pre-authentication source-IP/path short-burst limit |
429 |
request_rate_limit |
Authenticated identity/path write short-burst limit |
Policy changes and authenticated admission/rate-limit denials are written to
audit_events. The pre-authentication denial cannot have a durable actor and
is captured by structured access logs. Lowering max_active_jobs does not kill
existing work; it prevents further claims until usage falls below the new
limit. The same non-preemptive rule applies when a resource limit is lowered.
Schema v11 creates one PostgreSQL reservation per claimed attempt. Claiming a job locks its project policy, sums active reservations, and atomically checks job count, vCPU, memory MiB and disk GiB before inserting the attempt, reservation and worker lease. A temporarily full resource budget leaves the job queued with a short database-clock backoff so another runnable job can be considered.
The reservation phase follows the durable attempt through claimed,
provision, build, verify and publish. Terminal completion, failure,
cancellation and lease-expiry recovery release it in the same transaction as
the corresponding job/attempt transition. The policy API derives live usage
from this ledger; Redis and process-local worker counts are not quota
authorities.
Catalog resource classes must contain positive cores, memory and
disk_size. Pre-catalog compatibility jobs are charged the built-in medium
shape (4 vCPU, 8192 MiB, 50 GiB); new partially specified catalog jobs fail
closed. Drain active attempts before applying schema v11 so no pre-migration
executor can run without a reservation.
Schema v12 snapshots max_artifact_bytes_per_job when an attempt first opens
its private quarantine. Each retained generation is accounted independently:
the collected unsigned generation remains charged while the isolated signer
creates its signed generation, so the ledger captures the real dual-generation
peak. After the signed generation is verified and adopted, unsigned bytes are
released. Promotion, failure, cancellation, lease expiry and quarantine
cleanup release the remaining reservation.
Outbound worker uploads receive a one-shot limit equal to the durable
remaining budget. Legacy HTTP downloads reject oversized Content-Length and
also stop unknown-length streams after one sentinel byte beyond the remaining
limit. The signer receives a durable maximum output size, and promotion
recomputes the selected generation from regular files before acquiring the
publication path.
The project Dashboard reports aggregate active quarantine bytes and the per-job cap. Lowering the policy does not preempt attempts that already snapshotted a limit; new attempts use the new value. Drain active attempts before schema v12 so no older executor can write unaccounted quarantine bytes.
Schema v13 adds per-project hard caps for claimed, provision, build,
verify and publish. Claim admission and every phase change lock the same
project policy row before counting active project_resource_reservations, so
two control-plane replicas cannot both consume the final slot.
An attempt that reaches a full checkpoint keeps renewing its durable worker lease and waits without changing the job state or consuming the destination phase. Once another attempt leaves, the same fenced transition is retried and admitted atomically. Lowering a phase cap is non-preemptive: current attempts finish, while new transitions wait for usage to fall below the limit.
This is IAM-1B2b1's checkpoint scheduler. The executor still owns the whole pipeline while paused; a later scheduler iteration must split phases into independently claimable work items so waiting provision/publish capacity does not occupy a build executor slot. Drain active attempts before schema v13 so an older executor cannot bypass the checkpoint Gate.
Schema v14 creates four independently fenced work items for every attempt:
provision, build, verify and publish. Each item has its own owner,
monotonic claim fence and renewable lease. A PostgreSQL SKIP LOCKED claim
checks the destination project phase cap atomically; completion releases that
capacity, wakes deferred peers and makes only the next item ready. An expired
phase lease can be reclaimed, and the previous fence cannot complete it.
Attempt failure, cancellation or whole-attempt lease expiry cancels all
remaining items transactionally.
The rollout is deliberately two-mode. Existing attempts create a shadow
plan that cannot be claimed. A plan becomes active only through an explicit
fenced cutover while the attempt is still at its initial claimed boundary.
This prevents the legacy whole-pipeline executor and a new phase executor from
running the same build simultaneously.
IAM-1B2b2a stops at this database authority: production execution remains on the legacy path while plans are shadow-only.
Schema v15 moves worker sessions, commands, completions and upload capabilities out of the process-local Broker. Any control-plane replica can enqueue a stable command ID, another replica can deliver it, and a third can accept its completion. Delivery and upload claims have independent monotonic fences. An expired claim can be replayed, but a completion from the older fence is rejected. Terminal jobs, explicit cancellation and scheduler lease expiry revoke the session and cancel unfinished commands/uploads in the same database transaction.
The builder copies the durable command ID into LocalBuildRequest.ExecutionID.
Submitting the same build command again returns the original persisted
BuildJob, including an interrupted failure after builder restart, instead of
running emerge twice. Reusing that ID with a different decoded build request
is a hard conflict. Upload generations use fence-qualified temporary
files; PostgreSQL selects the winning digest/size generation before it can be
renamed into the quarantine destination. Recovery rechecks both size and
SHA-256. The gateway also normalizes every destination and confines it to the
configured quarantine/spool root both when the control plane creates the
capability and after a durable claim is read back from PostgreSQL. A malformed
or corrupted row therefore cannot redirect a worker upload elsewhere on the
server filesystem.
The schema-v15 Gate covered a drained v14 migration, two independent
repository/control-plane views, expired delivery and upload reclaim, stale
completion rejection, Broker restart/status recovery, cross-replica
dispatch/completion, builder restart idempotency, and the full local Compose
stack. Worker Gateway executor protocol 4 was introduced there. Schema v16 and
the IAM-1B2b2c Gate subsequently promoted phase execution to active; active
mode does not start the legacy whole-pipeline executor. Schema v20 capability
routing raises the current protocol to 5.
Schema v16 adds a non-secret, attempt-fenced execution context for the
provisioned instance, worker identity, quarantine generation and published
artifact projection. PHASE_EXECUTOR_MODE=active starts separate admission
and phase-worker loops: admission is the only shadow-to-active cutover path,
and the legacy whole-pipeline executor is not started. Capacity-blocked
ready work stays in PostgreSQL, so waiting for a provision, build, verify or
publish cap does not consume a Go executor slot.
Every phase renews its own 45-second lease and the owning attempt lease. Commands, collect uploads and verification commands derive stable UUIDs from the durable phase work ID. A replacement replica therefore waits for the same command result and upload generation. After any long-running worker result, and immediately before signing or publication, the executor must renew the exact phase owner/fence. A stale replica may observe the durable result but cannot append its logs, collect artifacts, sign, publish or commit context. Final publish, job/attempt completion, lease/resource/artifact-budget release and Worker Gateway revocation are one PostgreSQL transaction.
The first empty Worker Gateway long poll is also a committed session
heartbeat. Treating “no command yet” as a transaction error would roll back
connected_at and deadlock provisioning while it waited to enqueue the first
command. Completion delivery is exact-replay idempotent so an accepted
database write followed by a lost HTTP response can be retried safely;
different payloads or delivery fences remain rejected.
Schema v20 makes heterogeneous execution an explicit PostgreSQL contract.
Each queued job and each provision, build, verify, or publish work item
stores a canonical all-of requirement set. Labels bind the exact phase,
provider, execution zone, architecture, build mode, profile ID and immutable
image generation. When present, the resolved egress policy ID and digest are
also required. A worker is eligible only if its active, protocol-5 durable
record contains every label; a similar architecture, profile, image, provider,
or zone is never used as fallback.
EXECUTOR_ZONES declares the catalog zones reachable by one control-plane
replica. With an empty EXECUTOR_CAPABILITIES, that replica derives its labels
only from catalog profiles whose provider and zone match its configuration.
An explicit capability list is an exact override for specialized pools such
as a desktop verifier. The HA Compose overlay provides a separate secondary
override, so enabling a new VM or replica affects only the capabilities it
advertises rather than every VM in the cluster.
Admission requires at least one live matching capability worker before it
claims a queued job. The subsequent phase claim repeats the same match in the
transaction that registers/heartbeats the executor and increments the phase
fence. Scheduler, project-policy and Worker Gateway status distinguish
unschedulable from ordinary queued/ready work and expose the count of live
capability workers. Schema v20 is a drain-required migration: legacy rows
receive the fail-closed legacy:unroutable requirement instead of silently
running on a partially upgraded executor.
Schema v17 adds one project_attempt_usage row per claimed attempt. Claiming
locks the project policy and reserves the selected resource class's full
max_runtime_minutes plus
max_runtime_minutes * cloud_cost_microunits_per_minute. Active reservations
and settled charges share the same UTC-day budget, so concurrent replicas
cannot each spend the final allowance. A full daily budget defers queued work
until the next UTC day; a single request larger than the project limit is
rejected before a job is created.
The runtime deadline is enforced in attempt and phase lease renewal, claim validation and the final side-effect fence. Terminal settlement charges at least one second of actual wall time, capped by the attempt reservation, and releases unused reservation. The cap prevents lease-recovery latency after a deadline from manufacturing usage that was never admitted. Cloud cost starts when provisioning begins and is charged in ceiling minutes, also capped by its reservation; a request that never starts provisioning incurs no cloud charge. This is a stable scheduler/accounting estimate from the operator catalog, not an invoice or a dependency on an external billing API.
Failed and expired attempts are counted over a trailing hour. Reaching
max_failures_per_hour sets a separate abuse_suspended_until for
abuse_cooldown_seconds, records the reason/generation and emits a
project.abuse_suspended audit event. It never overwrites the owner's manual
suspended flag. An owner can clear only the automatic state with
-clear-abuse-suspension; this is also versioned and audited.
Drain all nonterminal jobs and active worker leases before applying schema v17. The migration raises the minimum executor protocol to 4 so an older replica cannot claim work without the runtime ledger/deadline fences.
Schema v18 stores a SHA-256 token fingerprint and lifecycle metadata for each
session; schema v19 adds issuer-scoped OIDC logout replay records. Neither
stores or returns bearer-token bytes, upstream credentials, or raw logout JWTs.
PostgreSQL is authoritative for expiry, maximum lifetime, idle timeout,
per-session revocation, and the per-subject tokens_valid_after watermark used
by “revoke all”. Revocation therefore takes effect on every control-plane
replica without relying on Redis or a sticky session.
OIDC_SESSION_IDLE_MINUTES=60
OIDC_SESSION_MAX_MINUTES=720
OIDC_STEP_UP_MAX_AGE_MINUTES=10
# Optional: require one accepted authentication method and context.
OIDC_STEP_UP_AMR_VALUES=otp,hwk
OIDC_STEP_UP_ACR_VALUES=urn:example:mfaAn OIDC high-risk write requires a recent auth_time. If AMR/ACR allowlists
are configured, the token must also match both policies. The Dashboard starts
a new Authorization Code + PKCE flow with prompt=login and max_age=0.
API clients may exchange a fresh provider credential and send the resulting
platform session separately as
X-Step-Up-Authorization: Bearer ...; it must have the same exact issuer and
subject as the primary bearer credential.
Legacy/hybrid break-glass administration uses a different secret:
API_KEY=<primary-api-key>
STEP_UP_API_KEY=<independent-step-up-key>The primary key cannot be reused as the step-up key. The local Dashboard asks
the administrator to re-enter local credentials, then grants a ten-minute
HttpOnly, SameSite=Strict elevation bound to that exact Dashboard session. It
forwards SERVER_STEP_UP_API_KEY only while that elevation is valid.
High-risk operations include creating projects, changing project members or admission policy, deleting jobs, global session revocation, and non-read system-administrator settings, worker, scheduler, image-factory, cache, artifact and runtime-metadata operations. Self-revoking one session does not require step-up; revoking another subject's session requires a stepped-up system administrator.
Manage sessions with the CLI:
portage-client sessions \
-server=https://portage.example.org \
-token="${PORTAGE_ENGINE_TOKEN}"
portage-client session-revoke \
-server=https://portage.example.org \
-token="${PORTAGE_ENGINE_TOKEN}" \
-session-id=<session-uuid>
portage-client sessions-revoke-all \
-server=https://portage.example.org \
-token="${PORTAGE_ENGINE_TOKEN}" \
-step-up-token="${PORTAGE_ENGINE_FRESH_TOKEN}"“Revoke all” advances a second-precision subject watermark and accepts only
tokens whose iat is strictly newer. This deliberately fails closed for a
token minted in the same second as revocation: re-authenticate after the
operation. OIDC providers that declare back-channel logout support can revoke
by issuer plus sid or sub; raw logout JWTs and jti values are not
retained. Providers without that protocol, including the GitHub OAuth App
flow, continue to rely on local logout, idle/max lifetime and revoke-all.
Server-side revocation remains authoritative for Portage Engine requests.
The normal control plane and Dashboard may use HTTP during an explicitly
trusted-LAN bring-up. An HTTP OIDC issuer or non-loopback HTTP callback also
requires OIDC_ALLOW_INSECURE_HTTP=true.
This is transport opt-in, not encryption: any network observer can copy bearer tokens and session traffic. Keep the service bound/firewalled to the trusted segment and move to HTTPS before untrusted users or networks are involved. The dedicated worker identity channel remains TLS 1.3 mutual TLS regardless of this control-plane choice.
Schema v21 makes PostgreSQL authoritative for the public lifecycle metadata of Worker Gateway credentials. Every issued leaf records:
- the SHA-256 fingerprint and serial of the leaf;
- the exact worker, job, attempt and attempt fence;
- its validity interval;
- the logical issuer ID, provider and CA-generation fingerprint.
Private keys and certificate PEM are never written to PostgreSQL. After the Go TLS stack verifies the client chain, every pull, completion and upload request recalculates the peer leaf fingerprint and checks the leaf, issuer generation, session, current attempt and fence. Revoking a leaf, issuer, session, attempt or job therefore takes effect on all Gateway replicas without a CRL cache or Redis.
The local reference provider is configured as:
WORKER_GATEWAY_ISSUER_ID=community-build-workers
WORKER_GATEWAY_ISSUER_PROVIDER=file
WORKER_GATEWAY_ISSUER_CERT=/run/secrets/workload-ca.crt
WORKER_GATEWAY_ISSUER_KEY=/run/secrets/workload-ca.keyThe key must be owner-only (0600). The file provider reloads the pair for
each issuance, so replacing both files switches new attempts to a new
generation. It does not retain the signing key in a package-level singleton.
For production, the implemented Vault PKI provider keeps the CA private key
outside every Portage Engine process. The control plane generates an Ed25519
worker key and CSR locally, submits only that CSR to the configured Vault role,
then validates the returned public chain, exact URI identity, client-auth EKU,
requested TTL and current Gateway listener trust roots before returning the
one-shot key to the worker:
WORKER_GATEWAY_ISSUER_ID=community-vault-workers
WORKER_GATEWAY_ISSUER_PROVIDER=vault
WORKER_GATEWAY_VAULT_ADDRESS=https://vault.internal.example
WORKER_GATEWAY_VAULT_MOUNT=pki_workers
WORKER_GATEWAY_VAULT_ROLE=portage-worker
WORKER_GATEWAY_VAULT_TOKEN_FILE=/run/secrets/vault-token
WORKER_GATEWAY_VAULT_SERVER_CA=/run/secrets/vault-server-ca.pem
WORKER_GATEWAY_VAULT_TIMEOUT_SECONDS=15The Vault address must be an HTTPS origin; this does not require the normal Portage Engine control-plane listener to use HTTPS on a trusted LAN. Redirects and environment proxies are disabled for the signing request so the Vault token cannot be forwarded to another host. The token file must be a regular, non-symlink, owner-only file. A Vault Agent renewable file sink is preferred over a static long-lived token.
Create a role that can issue only attempt-bound URI identities and client certificates:
vault secrets enable -path=pki_workers pki
vault secrets tune -max-lease-ttl=8760h pki_workers
# Import/use an existing CA or generate the integration CA according to the
# site's PKI policy before creating this role.
vault write pki_workers/roles/portage-worker \
allow_any_name=true \
enforce_hostnames=false \
allowed_uri_sans='spiffe://portage-engine/*' \
use_csr_common_name=true \
use_csr_sans=true \
server_flag=false \
client_flag=true \
code_signing_flag=false \
key_type=any \
ttl=3h \
max_ttl=24hThe application token needs only one capability:
path "pki_workers/sign/portage-worker" {
capabilities = ["update"]
}Do not grant issue, role mutation, issuer mutation, root generation, token
creation or policy administration to the Portage Engine token. In production,
prefer a short-period batch token delivered and renewed by Vault Agent. The
server rereads the token file for each issuance, so agent rotation and recovery
do not require restarting the control plane.
Use a staged rotation for either provider:
- Drain active Worker Gateway sessions before applying schema v21.
- Add the new CA certificate to
WORKER_GATEWAY_CLIENT_CAwhile keeping the old CA, then roll the Gateway listeners. TLS trust bundles are loaded when listeners start. - For
file, atomically replaceWORKER_GATEWAY_ISSUER_CERTandWORKER_GATEWAY_ISSUER_KEY. Forvault, switch the role/default issuer only after every Gateway replica serves the dual-CA trust bundle. The first new issuance registers the new generation asactiveand changes the previous generation todraining. - Wait at least the configured leaf TTL and confirm the old generation has no active certificates. Then remove the old CA from the trust bundle and roll listeners again.
- Use issuer revocation only for compromise response. Revocation is irreversible for that fingerprint and terminates all of its active worker sessions, commands and uploads.
System administrators can inspect the redacted inventory:
curl -fsS \
-H "Authorization: Bearer ${PORTAGE_ENGINE_TOKEN}" \
https://portage.example.org/api/v1/worker-gateway/identities | jqLeaf and issuer revocation are high-risk writes and require the same fresh step-up policy as other system-administrator mutations:
curl -fsS -X POST \
-H "Authorization: Bearer ${PORTAGE_ENGINE_TOKEN}" \
-H "X-Step-Up-Authorization: Bearer ${PORTAGE_ENGINE_FRESH_TOKEN}" \
-H "Content-Type: application/json" \
--data '{"fingerprint":"<64-lowercase-hex>","reason":"incident-123"}' \
https://portage.example.org/api/v1/worker-gateway/certificates/revoke
curl -fsS -X POST \
-H "Authorization: Bearer ${PORTAGE_ENGINE_TOKEN}" \
-H "X-Step-Up-Authorization: Bearer ${PORTAGE_ENGINE_FRESH_TOKEN}" \
-H "Content-Type: application/json" \
--data '{"fingerprint":"<64-lowercase-hex>","reason":"CA compromise"}' \
https://portage.example.org/api/v1/worker-gateway/issuers/revokeThe Monitor shows active, draining and revoked issuer counts, active/revoked leaf counts, leaves expiring within 30 minutes, and the redacted generation inventory. Provider health additionally exposes consecutive failures and the last success/failure timestamp with a bounded, credential-free error. Alert when the provider is unhealthy, there is no active issuer, an unexpected second active generation, a revoked generation still appears in a listener trust bundle, or certificate authorization failures rise above the normal stale-attempt rate.
At startup, an enabled Gateway performs a real sign-and-verify probe and refuses
to start if Vault is unreachable, the token is unauthorized, or the returned
generation is not in that instance's listener trust bundle. At runtime, a Vault
failure blocks only new certificate issuance and degrades /health; already
issued, unexpired and non-revoked worker sessions continue to be authorized
against PostgreSQL. Restore/renew the owner-only token file or Vault service,
then the next successful issuance clears the failure counter. Never bypass the
trust check to recover from an incorrectly ordered CA rotation—restore the old
Vault default issuer or deploy the dual trust bundle first.
Run the repeatable real-Vault Gate locally:
scripts/test-vault-issuer.shIt pins hashicorp/vault:2.0.3, creates a temporary TLS dev server and
sign-only token, proves token failure/recovery, proves that switching Vault
before listener trust rollout fails closed, then proves old → dual → new CA
rollover. Dev mode and its generated CA are test-only and are destroyed when
the script exits.
Operational references:
- Vault PKI sign API and role constraints
- Vault PKI setup
- AppRole authentication
- Vault Agent file sink
- TLS dev server flags
IAM-0 through IAM-1G now establish identity, isolation, manual and automatic suspension, atomic queued/active limits, UTC-daily submission/runtime/cost caps, dual-layer short-burst limiting, durable per-attempt accounting, cross-replica token/session revocation, administrator step-up, and exact heterogeneous executor routing, plus a durable workload leaf/issuer revocation boundary plus a real external Vault PKI issuer, listener-bound trust rollover, and failure recovery Gate. They do not make the service ready for unrestricted public submissions. Remaining work is a production-site Vault HA/unseal/backup runbook and long-duration drill, real Authentik/Google/GitHub community-domain callback validation, explicit account-linking policy, fair scheduling, and production SLO/abuse operations.