Skip to content

Authenticate Status Dashboard reporting with a Zitadel service identity - #28

Merged
Aloento merged 3 commits into
mainfrom
feature/zitadel-service-identity
Sep 23, 2026
Merged

Aloento merged 3 commits into
mainfrom
feature/zitadel-service-identity

Conversation

@Aloento

@Aloento Aloento commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

What

The Status Dashboard backend no longer accepts the local HMAC-SHA256 JWT (stackmon/StatusDashboard-Backend#18), so the reporter authenticates with a Zitadel service identity token obtained through the machine user JWT Profile flow, delegated to the zitadel crate.

  • src/oidc.rs: the machine user key file is loaded once at startup (only type: serviceaccount is accepted); OIDC discovery, assertion signing and the token request are left to zitadel::credentials. Startup fails closed, naming the offending MP_STATUS_DASHBOARD__OIDC_* key when a value is missing, unreadable, invalid or of an unsupported type, and the key material never reaches a log line or an error message.
  • src/config.rs: status_dashboard.secret is replaced by oidc_issuer, oidc_key_file and oidc_scopes, all three required. The scope list is validated at startup and must carry a project role scope (urn:zitadel:iam:org:project:role:<role>) and the project audience scope (urn:zitadel:iam:org:project:id:<projectId>:aud) that makes the token audience the project id the backend verifies.
  • src/sd.rs: build_auth_headers is async and returns anyhow::Result<HeaderMap>; the HMAC signing path, LocalHmacClaims and the "no secret, report anonymously" fallback are removed.
  • src/bin/reporter.rs: a fresh token is requested right before every authenticated call (startup component fetch, component cache refresh, incident creation) and is never kept across loop iterations, so a long-running reporter cannot keep using an expired token. A token failure during startup exits non-zero; a failure inside the loop is logged and the loop continues; a fatal metric_watcher error now propagates out of main instead of exiting with code 0.
  • src/sd.rs and src/bin/reporter.rs: the unused V1 ComponentStatus structures are dropped.
  • Cargo.toml: jwt, hmac, sha2 and jsonwebtoken are replaced by the zitadel crate (default-features = false), and reqwest moves to 0.12 so that a single HTTP/TLS stack remains.

The documentation of the new keys, the regenerated config schema and the removal of specs/ and doc/modules/ live in #32, so this PR stays code only.

Verification: CI build (Docker musl image), test (fmt-check, clippy, cargo test) and coverage (tarpaulin 95% gate) on f332d07.

@Aloento Aloento self-assigned this Sep 23, 2026
@Aloento
Aloento force-pushed the feature/zitadel-service-identity branch from 7952165 to cf83487 Compare September 23, 2026 18:50
…identity

Replace the self-signed HMAC JWT with a Zitadel OIDC service identity token obtained through the machine user JWT Profile flow, delegated to the community `zitadel` crate.

- drop status_dashboard.secret and add oidc_issuer, oidc_key_file and oidc_scopes
- load the Zitadel machine user key file once at startup, failing closed with the configuration key named when it is missing, unreadable, invalid or of an unsupported type
- delegate OIDC discovery, assertion signing and the token request to zitadel::credentials, signing a fresh assertion and requesting a fresh token for every report
- send the configured scopes space-joined as one scope parameter (the role scope plus the project audience scope that makes the token audience the project id)
- present the assertion instead of HTTP Basic credentials and never cache a token in-process
- exit non-zero when a fatal reporter error occurs
- replace the jwt, hmac, sha2 and jsonwebtoken dependencies with the zitadel crate and upgrade reqwest to 0.12 so that a single HTTP/TLS stack remains
- update the test suite
The default list only carried the role scope, and the audience scope cannot have a default because it carries the project id of the deployment, so a deployment that configured issuer and key file started up and then failed every report with 401.

- drop the default of status_dashboard.oidc_scopes and make the key required
- reject a scope list without a urn:zitadel:iam:org:project:role: scope or without a urn:zitadel:iam:org:project:id:<projectId>:aud scope in StatusDashboardConfig::oidc_identity(), naming MP_STATUS_DASHBOARD__OIDC_SCOPES and showing a correct list
- cover both missing scope classes and the accepted list in the unit and integration tests
Nothing constructs or reads them since the reporter moved to the V2 incident API, and their public visibility kept the dead code lint quiet.

- remove the V1 request structure from src/sd.rs and the reporter
- drop the serde import the reporter only needed for it
@Aloento
Aloento force-pushed the feature/zitadel-service-identity branch from 7ce4cad to f332d07 Compare September 23, 2026 20:50
@Aloento
Aloento merged commit ddd0c03 into main Sep 23, 2026
9 checks passed
@Aloento
Aloento deleted the feature/zitadel-service-identity branch September 23, 2026 21:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant