Authority identity-provider bundle operations

Audience: operators provisioning LDAP, OIDC, or SAML authentication in an online or air-gapped Stella Ops installation.

Prerequisites

The saved configuration and the executable provider are separate controls. A saved and enabled provider without its signed bundle correctly reports configApplied=true, bundleMounted=false, and outcome=not_mounted. A bundle cannot become a registrar in Authority’s already-built root service provider; mounting a new bundle requires an Authority restart.

From the repository root, run:

pwsh ./devops/build/package-runtime-plugins.ps1 `
  -Module authority `
  -Profile recommended `
  -SignAuthorityBundles `
  -AuthorityCosignKeyPath C:\secure\stellaops\authority-plugins.key `
  -AuthorityCosignPublicKeyPath C:\secure\stellaops\authority-plugins.pub `
  -NoRestore

The producer publishes the ldap, oidc, and saml bundles under devops/plugins/authority/recommended/<provider>/. Each bundle contains its entry assembly, manifest.json, checksums.sha256, and an adjacent <assembly>.dll.sig. The public key is copied to devops/etc/certificates/trust-roots/plugins/authority/cosign.pub; the private key is never copied. -UseOfflineDevSigner is for deterministic local tests, not production or customer bundles.

For the local P3-8 fixture only, the exact development-signing variant is:

pwsh ./devops/build/package-runtime-plugins.ps1 `
  -Module authority `
  -Profile recommended `
  -SignAuthorityBundles `
  -UseOfflineDevSigner `
  -NoRestore

It writes the development private key beneath the ignored devops/plugins/_signing/ tree and refreshes the ignored Authority trust root. Do not use that signer for production or customer bundles.

Check the output before touching a running service:

$providers = 'ldap', 'oidc', 'saml'
foreach ($provider in $providers) {
  $root = "devops/plugins/authority/recommended/$provider"
  if (-not (Test-Path "$root/manifest.json") -or
      -not (Test-Path "$root/checksums.sha256") -or
      -not (Get-ChildItem $root -Filter '*.dll.sig' -File)) {
    throw "Incomplete signed Authority bundle: $provider"
  }
}

if (-not (Test-Path 'devops/etc/certificates/trust-roots/plugins/authority/cosign.pub')) {
  throw 'Authority plugin trust root is missing.'
}

Render and activate the mount

The recommended overlay mounts the bundle and trust-root directories read-only at /app/plugins/authority/recommended and /app/trust-roots/plugins/authority. Render the exact compose chain before the service mutation:

Push-Location devops/compose
docker compose `
  -f docker-compose.stella-ops.yml `
  -f docker-compose.plugins.recommended.yml `
  config --quiet
Pop-Location

After the operator approves the Authority-only restart boundary, activate it:

Push-Location devops/compose
docker compose `
  -f docker-compose.stella-ops.yml `
  -f docker-compose.plugins.recommended.yml `
  up -d --force-recreate --no-deps authority
Pop-Location

--force-recreate is required when the service currently runs without this overlay or must swap a same-tag image. Do not recreate unrelated services.

Verify admission and status

  1. Confirm the Authority container is healthy and its recent logs contain no signature, checksum, catalog-admission, assembly-load, or native-library failure.
  2. Read GET https://authority.stella-ops.local/internal/plugins/status. For the intended provider, require mounted=true, discovered=true, admitted=true, and loaded=true. Then call POST /internal/plugins/probe and require probed=true with status=responded for the deterministic protocol probe.
  3. Read GET /console/admin/identity-providers/{name}/status with a token that carries ui.admin and authority:idp.read. The configured provider name and type must match the mounted descriptor exactly.
  4. Run POST /console/admin/identity-providers/{name}/apply with fresh authentication and authority:idp.write. A non-2xx response is a real apply failure; do not treat it as success.
  5. Exercise the forcing function: authenticate a real directory/provider user through /connect/token with authority_provider=<name>, then prove wrong credentials are rejected. Container health alone is insufficient.

For the local fixture drill, add docker-compose.idp-testing.yml and the idp compose profile. It supplies Keycloak and OpenLDAP fixtures but still expects signed recommended bundles and keeps the LDAP bind password in AUTHORITY_IDP_LDAP_BIND_PASSWORD.

Run the P3-8 two-phase token acceptance

IdentityProviderLiveContainerTests is an env-gated, Authority-owned black-box test. It deliberately does not recreate containers. A newly mounted registrar cannot join Authority’s already-built root service provider, so an operator runs the same test seam in two phases around the approved Authority-only restart.

Keep all credentials and the fresh global-admin token in the process environment; do not add them to the repository, JSON configuration, shell command arguments, or test evidence. The admin token must carry ui.admin, authority:idp.read, and authority:idp.write, must identify a global admin, and must have an auth_time no older than five minutes.

Prepare an ignored JSON object and point STELLAOPS_IDP_LIVE_LDAP_CONFIG_PATH to it. The local OpenLDAP shape is:

{
  "connection": {
    "host": "openldap.stellaops.test",
    "port": 389,
    "timeoutSeconds": 10,
    "useStartTls": false,
    "validateCertificates": false,
    "searchBase": "ou=users,dc=stellaops,dc=test",
    "usernameAttribute": "uid",
    "bindDn": "cn=readonly,dc=stellaops,dc=test",
    "bindPasswordSecret": "env:AUTHORITY_IDP_LDAP_BIND_PASSWORD"
  },
  "security": {
    "requireTls": false,
    "allowInsecureWithEnvToggle": true
  },
  "queries": {
    "userFilter": "(uid={username})",
    "attributes": ["uid", "cn", "mail", "memberOf"]
  },
  "claims": {
    "groupAttribute": "memberOf"
  }
}

The compose overlay sets STELLAOPS_LDAP_ALLOW_INSECURE=true only for this local fixture. Production LDAP remains TLS-required. Authority reaches OpenLDAP over openldap.stellaops.test:389; the host-only diagnostic port defaults to 3390 because Windows RDP owns 3389, and can be changed with STELLAOPS_IDP_LDAP_HOST_PORT.

Set these values from the operator’s secure session before both phases:

AUTHORITY_IDP_LDAP_BIND_PASSWORD=<fixture read-only bind password; injected into Authority>
STELLAOPS_IDP_LIVE_TESTS=true
STELLAOPS_IDP_LIVE_AUTHORITY_URL=https://stella-ops.local
STELLAOPS_IDP_LIVE_ADMIN_TOKEN=<fresh token; do not persist>
STELLAOPS_IDP_LIVE_PROVIDER_NAME=ldap
STELLAOPS_IDP_LIVE_LDAP_CONFIG_PATH=<absolute ignored JSON path>
STELLAOPS_IDP_LIVE_LDAP_USERNAME=<fixture directory user>
STELLAOPS_IDP_LIVE_LDAP_PASSWORD=<fixture directory password>
STELLAOPS_IDP_LIVE_LDAP_WRONG_PASSWORD=<different non-secret negative value>
STELLAOPS_IDP_LIVE_CLIENT_ID=stellaops-cli
STELLAOPS_IDP_LIVE_ALLOW_SELF_SIGNED_TLS=true

AUTHORITY_IDP_LDAP_BIND_PASSWORD is consumed by the Authority container via the compose overlay and resolves the JSON env: reference; it is not consumed by the test process. An empty value leaves the mounted-phase LDAP probe and password grant unable to bind. The black-box test uses the published front door because the Compose stack does not expose authority.stella-ops.local:443 directly on the host; the gateway routes /console/admin/* and /connect/token to Authority.

For phase 1, set STELLAOPS_IDP_LIVE_PHASE=not-mounted and point STELLAOPS_AUTHORITY_IDP_BUNDLE_ROOT at an empty ignored directory before rendering and starting the overlay. Run only the not-mounted method:

pwsh ./tools/scripts/test-targeted-xunit.ps1 `
  -Project ./src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/StellaOps.Authority.Tests.csproj `
  -Method "*NotMountedPhase_SaveEnableApply*"

It saves and enables the global LDAP row through Authority, applies it, and requires configApplied=true, bundleMounted=false, loaded=false, and outcome=not_mounted.

For phase 2, package and sign the bundle as above, restore STELLAOPS_AUTHORITY_IDP_BUNDLE_ROOT to the signed recommended directory, and perform the explicitly approved Authority-only force-recreate. Set STELLAOPS_IDP_LIVE_PHASE=mounted, acquire a new fresh admin token, and run:

pwsh ./tools/scripts/test-targeted-xunit.ps1 `
  -Project ./src/Authority/StellaOps.Authority/StellaOps.Authority.Tests/StellaOps.Authority.Tests.csproj `
  -Method "*MountedPhase_ApplyThenDirectoryPasswordGrant*"

The second phase applies the same global row, requires the signed bundle to be mounted, admitted, loaded, and healthy, then calls /connect/token. It proves a directory password grant returns a JWT whose stellaops:idp claim is ldap and that the same user with a wrong password returns invalid_grant with no access token. Capture the sanitized responses and provider name under docs/qa/feature-checks/runs/**; never capture tokens or passwords.

Failure modes and recovery

See Authority architecture for the descriptor merge and admission contract, and plugin cross-ALC troubleshooting for assembly identity failures.