Runbook: Scanner — Registry Authentication Failures
Use this runbook when the Stella Ops scanner worker cannot authenticate to a container registry — for example, private images failing with 401 Unauthorized or producing zero non-empty layers while public images scan fine. It is written for the Platform and Security teams.
The essential mental model: the scanner worker resolves registry credentials from one of two places — the Integrations module (per-tenant registry integrations) or static worker pipeline config (environment variables) — and supports only Anonymous, Basic, and Bearer auth. There is no cloud-IAM/OIDC federation and no operator CLI for registry credentials; see How credential resolution actually works before diagnosing.
Metadata
| Field | Value |
|---|---|
| Component | Scanner (scanner-worker) |
| Severity | High |
| On-call scope | Platform team, Security team |
| Last updated | 2026-05-30 |
| Doctor check | None dedicated. The Scanner Doctor plugin ships check.scanner.queue, check.scanner.sbom, check.scanner.vuln, check.scanner.reachability, check.scanner.resources, check.scanner.slice.cache, and check.scanner.witness.graph — there is no check.scanner.registry-auth check. (Source: src/Doctor/__Plugins/StellaOps.Doctor.Plugin.Scanner/Checks/.) |
Reconciliation note (2026-05-30): This runbook was reconciled against
src/Scannerand the CLI. The original draft described astella registry ...credential-management workflow and cloud-IAM/OIDC federation flows that do not exist in the codebase. The real model is: the scanner-worker resolves registry credentials either from the Integrations module (integrations-web HTTP catalog) or from static worker pipeline config (env vars). Supported auth types are Anonymous, Basic, and Bearer only. Sections that describe unimplemented surfaces are flagged inline.
Symptoms
- [ ] Scans fail with the registry returning
401 Unauthorized(the registry client surfaces this viaEnsureSuccessStatusCode()after exhausting the bearer-token challenge handshake inRegistryClient.SendWithAuthAsync). - [ ] Scans produce zero non-empty layers for a private image even though the image exists (fail-closed credential resolution returned
null— see “Fail-closed behaviour” below). - [ ] Worker log warning:
Skipping registry probe for {Registry}: integration {IntegrationId} is flagged requires_credentials_resolution=true(legacy anonymous registry that needs credentials added). - [ ] Worker log warning:
IntegrationAwareRegistryCredentialProvider: resolve-credentials failed for {Registry} integration {IntegrationId}(integrations-web reachable but credential resolution failed). - [ ] Worker log warning:
Bearer token request failed for {Registry}: {StatusCode}(Docker v2 token endpoint rejected the basic credentials). - [ ] Scans work for public/anonymous images but fail for private images.
Flag — alert name unverified. The original draft referenced an alert
ScannerRegistryAuthFailed. No such alert rule was found insrc/Scanneror the telemetry assets during reconciliation; treat it as unverified rather than authoritative.
Impact
| Impact Type | Description |
|---|---|
| User-facing | Cannot scan private images; release pipeline blocked |
| Data integrity | No data loss; authentication issue only |
| SLA impact | All scans for affected registry blocked |
How credential resolution actually works
Before diagnosing, understand the resolution chain the scanner-worker runs for every distinct registry hostname it encounters during a scan (source: src/Scanner/StellaOps.Scanner.Worker/Processing/IntegrationAwareRegistryCredentialProvider.cs):
- If the Integrations path is enabled (
Scanner:Worker:Integrations:Enabled=trueand aBaseAddressis set), the worker calls integrations-web:GET /api/v1/integrations?type=Registry(paged) to list registry integrations.- Matches the requested registry hostname against each integration’s
endpoint(case-insensitive, scheme-agnostic, viaNormalizeHostname). - On a match that is not flagged
requiresCredentialsResolution=true, it callsPOST /api/v1/integrations/{id}/resolve-credentialsto fetch the plaintext secret. - The secret is translated to a credential:
user:password→ Basic; an opaque value → Bearer (TranslateSecret).
- If no integration matches the hostname, the worker falls back to
PipelineRegistryCredentialProvider, which uses static env-var credentials (Scanner:Worker:Pipeline:RegistryUsername/RegistryPassword) when set, otherwise Anonymous. - The chosen credential is applied by
RegistryClient(source:src/Scanner/__Libraries/StellaOps.Scanner.Registry/RegistryClient.cs): Basic/Bearer header on the first request, and if the registry replies401with aWWW-Authenticate: Bearerchallenge, the client performs the Docker v2 token handshake (fetches a token from the challengerealm, caches it, retries).
Supported auth types are only
Anonymous,Basic, andBearer(RegistryAuthTypeinIRegistryCredentialProvider.cs). There is no AWS ECR IAM-role, GCP workload-identity/OIDC-federation, or Azure ACR managed-identity flow anywhere in Scanner — see the flagged section under “Root cause fix”.
Fail-closed behaviour (default)
When a registry integration matches the hostname but credential resolution returns nothing (resolve-credentials errored, returned empty, or the integration is flagged requiresCredentialsResolution=true), the worker returns null instead of falling back to anonymous, because the operator intent is that registries must be authenticated. This is controlled by Scanner:Worker:Integrations:FailClosed (default true). The downstream registry client then surfaces this as an auth failure / zero non-empty layers rather than silently scanning nothing. Note: a lookup failure (transport/parse/config error before any match) still falls back to the static provider so a transient integrations-web outage does not take the worker down.
Diagnosis
Quick checks
Run the Scanner Doctor checks (there is no registry-auth-specific check; use the general Scanner queue/vuln/sbom checks plus the worker logs below):
stella doctor runFlag — NOT IMPLEMENTED. The original draft invoked
stella doctor --check check.scanner.registry-auth. No such check exists (see Metadata). Usestella doctorwithout that filter and inspect scanner-worker logs.Confirm whether the Integrations credential path is enabled. Inspect the scanner-worker config (
Scanner:Worker:Integrations):Enabled,BaseAddress,Tenant,FailClosed. IfEnabled=false, only the static pipeline env-var credentials are in play (resolution step 2 above).List configured registry integrations (this is the data the worker matches against). Use the Integrations module surface — the scanner-worker itself calls
GET /api/v1/integrations?type=Registry. There is nostella registry list --show-statuscommand.Flag — NOT IMPLEMENTED.
stella registry list --show-statusandstella registry test <url>do not exist as working commands. Astella registrygroup exists only as dead, un-wired stub code (RegistryCommandGroup.cs— prints simulated output, never wired into the CLI root). The only wired registry CLI surface isstella config registry list/stella config registry configure, and both are mock stubs that print hardcoded output and do not persist or test anything.
Deep diagnosis
Read scanner-worker logs for the resolution-stage warnings. Filter the scanner-worker container/service logs for the literal messages emitted by
IntegrationAwareRegistryCredentialProvider:Skipping registry probe for {Registry}: integration {IntegrationId} is flagged requires_credentials_resolution=true→ the integration is a legacy/anonymous row that needs credentials added.IntegrationAwareRegistryCredentialProvider: resolve-credentials returned {StatusCode}/... resolve-credentials failed ...→ integrations-web reachable but credential resolution failed.IntegrationAwareRegistryCredentialProvider: list-integrations returned {StatusCode}→ the worker could not list integrations (often auth/tenant: the resolver HttpClient must carry a valid Authority client-credentials JWT whosestellaops:tenantclaim matches the integration’s tenant).Bearer token request failed for {Registry}: {StatusCode}(fromRegistryClient) → the Docker v2 token endpoint rejected the basic credentials during the challenge handshake.
Flag — NOT IMPLEMENTED.
stella scanner logs --filter ... --last 30mdoes not exist. Read the scanner-worker service/container logs directly (e.g.docker logs).Verify the resolver’s auth + tenant binding. The HttpClient named
scanner-worker-integrations-resolvercarries an Authority client-credentials token; integrations-web enforces a claim-onlyRequireTenant()filter and strips rawX-Tenant-Id-style headers at ingress. A401/403on list-integrations or resolve-credentials almost always means the worker’s token tenant claim does not match the integration’s tenant, or the token lacks the configured scope (Scanner:Worker:Integrations:Scope, defaultintegration:read).Confirm the hostname actually matches an integration. Matching is on the normalized host (lowercased authority, port-aware). A mismatch (e.g.
registry.example.test:5000vs an integration endpoint ofhttps://registry.example.test) causes the worker to fall through to static/anonymous instead of using the integration credential.Flag — NOT IMPLEMENTED.
stella registry credentials show,stella registry test --verbose, andstella registry iam-statusdo not exist. Diagnose via integrations-web data + worker logs instead.
Resolution
There is no operator CLI for managing scanner registry credentials. Remediation happens in one of two places: the Integrations module (the per-tenant registry integration + its stored secret) or the scanner-worker configuration (static env vars). Choose the path that matches how the failing hostname is being resolved (see “How credential resolution actually works”).
Immediate mitigation
Integrations path — update the registry integration secret. Update the credential stored on the matching registry integration so the next
resolve-credentialscall returns valid material. The secret must be in one of the shapes the worker understands:user:password(→ Basic) or an opaque token (→ Bearer). Use the Integrations module’s own management surface to update the secret; the scanner-worker caches the hostname→integration mapping for the lifetime of a worker process, so a worker restart guarantees the new secret is picked up.Static path — set worker pipeline credentials. If the hostname does not match any integration, set the static env vars on the scanner-worker and restart it:
SCANNER__SCANNER__WORKER__PIPELINE__REGISTRYUSERNAME=<user> SCANNER__SCANNER__WORKER__PIPELINE__REGISTRYPASSWORD=<token-or-password>These map to
ScannerWorkerOptions.ScanPipelineOptions.RegistryUsername/RegistryPassword. When set, the worker sends HTTP Basic; when unset it falls back to Anonymous.Clear a stuck fail-closed state. If the integration is flagged
requiresCredentialsResolution=true(legacy anonymous row), the worker fail-closes and logsSkipping registry probe .... Add real credentials to that integration (clearing the flag), or — only as a temporary, non-production workaround — setScanner:Worker:Integrations:FailClosed=falseso a missing credential falls back to anonymous/static. Document any such change.Flag — NOT IMPLEMENTED.
stella registry refresh-credentials,stella registry update-credentials, andstella registry configure docker-hub --access-tokendo not exist. Docker Hub rate-limiting is not specially handled by Scanner; provide an authenticated Docker Hub credential via the integration or the static env vars above to raise the rate limit.
Root cause fix
If credentials expired: Rotate the secret at the source registry, then update it on the matching registry integration (Integrations path) or in the scanner-worker env vars (static path) and restart the worker. There is no auto-refresh/refresh_interval setting for scanner registry credentials; the only token refresh that happens automatically is the in-memory Docker v2 bearer-token cache inside RegistryClient (refreshed expires_in - 30s before expiry, default 5 min when the token endpoint omits expires_in). That cache is per worker process and is not operator-configurable beyond RegistryClientOptions.TokenExpirationBufferSeconds.
Flag — NOT IMPLEMENTED (cloud IAM/OIDC). The original draft documented AWS ECR IAM-role assumption, GCP Artifact Registry workload-identity/OIDC federation, and Azure ACR flows (
stella registry iam verify,stella registry oidc verify,stella registry configure ecr --role-arn,... gcr --workload-identity-provider). None of these exist. Scanner authenticates to registries only with static Basic/Bearer credentials (resolved from an integration or env vars) plus the standard Docker v2 bearer-token challenge handshake. The--type ecr|gcr|acroption onstella config registry configureis a label on a mock stub that persists nothing and triggers no cloud-IAM behaviour. For cloud registries, supply a username/password or token credential (e.g. an ECRdocker loginpassword, a GCR JSON-key or access token) as a normal Basic/Bearer secret. This also aligns with the platform’s on-prem-first / no-cloud-managed-defaults posture.
If a self-hosted registry uses a private CA or non-TLS: TLS handling is configuration on the registry client, not a per-registry CLI flag:
Registry:Client:SkipTlsVerify(RegistryClientOptions.SkipTlsVerify, defaultfalse) — skips certificate validation. Development/testing only.Registry:Client:AllowInsecure/Registry:Client:InsecureRegistries— force plain HTTP for the internal/in-cluster registry or a named host list.The scan pipeline also has
Scanner:Worker:Pipeline:AllowInsecure(defaulttrue) for the internalstellaops-registry:5000.Flag — NOT IMPLEMENTED.
stella registry configure <name> --ca-cert ...and--insecure-skip-verifydo not exist. There is no per-registry custom-CA option in the registry client; the runtime relies on the host trust store plus the insecure/plain-HTTP switches above. If a custom CA is required, install it into the scanner-worker container’s trust store.
Verification
There is no stella registry test / stella scan image --dry-run verification command for this flow. Verify by re-running a real scan of a private image and confirming the worker logs no longer emit the resolution-stage warnings listed under “Deep diagnosis”, and that the scan produces non-empty layers / components.
- Restart the scanner-worker (clears the per-process hostname→integration cache and the bearer-token cache).
- Trigger a scan of a private image on the affected registry.
- Tail the scanner-worker logs and confirm: no
Skipping registry probe, noresolve-credentials failed, noBearer token request failed, and the scan completes with layers/components emitted.
Flag — NOT IMPLEMENTED. The original verification block (
stella registry test,stella scan image --dry-run,stella scanner logs) referenced commands that do not exist; replaced with the log-based real-scan verification above.
Prevention
- [ ] Credentials: Store registry credentials as per-tenant registry integrations so they are managed centrally and the fail-closed guard applies, rather than scattering static env vars across worker instances.
- [ ] Rotation: Rotate the integration/env-var secret on the registry’s own schedule and restart the worker to invalidate the per-process hostname cache. (There is no built-in scanner-side auto-refresh; do not rely on one.)
- [ ] Fail-closed: Keep
Scanner:Worker:Integrations:FailClosed=truein production so a missing/expired credential surfaces as an auth failure instead of silently scanning nothing. - [ ] Tenant/scope hygiene: Ensure the scanner-worker’s Authority client-credentials token carries the correct
stellaops:tenantclaim and the configured integrations scope (defaultintegration:read), so list/resolve calls do not 401/403. - [ ] Documentation: Document, per environment, which registries use the Integrations path vs. static env vars.
Related Resources
- Scanner architecture:
docs/modules/scanner/architecture.md(registry/pipeline config table; the originally-linkeddocs/modules/scanner/registry-auth.mddoes not exist). - Source of truth:
src/Scanner/StellaOps.Scanner.Worker/Processing/IntegrationAwareRegistryCredentialProvider.cs(integration-based resolution + fail-closed)src/Scanner/StellaOps.Scanner.Worker/Processing/PipelineRegistryCredentialProvider.cs(static env-var fallback)src/Scanner/__Libraries/StellaOps.Scanner.Registry/RegistryClient.cs(Docker v2 bearer-token handshake, retry, rate limit)src/Scanner/__Libraries/StellaOps.Scanner.Registry/IRegistryCredentialProvider.cs(RegistryAuthType: Anonymous/Basic/Bearer)
- Related runbooks:
scanner-worker-stuck.md,scanner-timeout.md - Flag — link not found:
docs/operations/registry-configuration.md(referenced by the original draft) does not exist in the repo; remove or create before relying on it.
