Authority identity-provider bundle operations
Audience: operators provisioning LDAP, OIDC, or SAML authentication in an online or air-gapped Stella Ops installation.
Prerequisites
- Authority’s built-in
standardprovider remains available as the recovery login path. - The chosen provider configuration is saved through
/console/admin/identity-providersusingenv:,file:,vault:, orsecret:references for sensitive fields. Never place a bind password or client secret in the repository, bundle, command line, or evidence. - The relevant projects have already been restored from the approved offline NuGet mirror when
-NoRestoreis used. - Cosign and the site-owned private/public signing-key pair are available. Keep the private key outside the repository and outside the mounted trust-root directory.
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.
Package and sign the recommended providers
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
- Confirm the Authority container is healthy and its recent logs contain no signature, checksum, catalog-admission, assembly-load, or native-library failure.
- Read
GET https://authority.stella-ops.local/internal/plugins/status. For the intended provider, requiremounted=true,discovered=true,admitted=true, andloaded=true. Then callPOST /internal/plugins/probeand requireprobed=truewithstatus=respondedfor the deterministic protocol probe. - Read
GET /console/admin/identity-providers/{name}/statuswith a token that carriesui.adminandauthority:idp.read. The configured provider name and type must match the mounted descriptor exactly. - Run
POST /console/admin/identity-providers/{name}/applywith fresh authentication andauthority:idp.write. A non-2xx response is a real apply failure; do not treat it as success. - Exercise the forcing function: authenticate a real directory/provider user through
/connect/tokenwithauthority_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
not_mounted: the configuration exists but the exact signed provider bundle is absent from the mounted recommended root. Package, sign, render, and restart Authority.rejected: inspect the manifest type/assembly identity, detached signature, checksum, and mounted public key. Authority admission is catalog-bound and fail-closed; do not enable unsigned fallback.- LDAP load failure on Linux: rebuild the Authority image with the approved OpenLDAP runtime package (
AUTHORITY_LDAP_NATIVE_PACKAGE, normallylibldap-2.5-0) available from the offline image source. - Configuration apply failure: keep the bundle mounted, correct the reference-backed provider configuration, and retry Apply with fresh authentication. Never copy an inline secret into the global store.
- Sign-in regression: use the retained
standardprovider, disable the external provider through Authority, and inspect its status/audit record. Removing the overlay requires another explicitly approved Authority-only recreate.
See Authority architecture for the descriptor merge and admission contract, and plugin cross-ALC troubleshooting for assembly identity failures.
