diff --git a/.vale/styles/config/vocabularies/DependencyTrack/accept.txt b/.vale/styles/config/vocabularies/DependencyTrack/accept.txt index d297cf5e..c8c452e7 100644 --- a/.vale/styles/config/vocabularies/DependencyTrack/accept.txt +++ b/.vale/styles/config/vocabularies/DependencyTrack/accept.txt @@ -138,6 +138,7 @@ Webex [Mm]isconfiguration [Mm]isconfigured [Mm]ixeway +[Mm]ulticast [Nn]amespaced? [Nn]amespaces [Nn]amespacing @@ -181,6 +182,8 @@ cutover eDirectory keysets? keytool +kubeadm +kubelet npm px sAMAccountName diff --git a/docs/assets/images/guides/administration/configuring-workload-identity-federation/create-binding.png b/docs/assets/images/guides/administration/configuring-workload-identity-federation/create-binding.png new file mode 100644 index 00000000..0e107aab Binary files /dev/null and b/docs/assets/images/guides/administration/configuring-workload-identity-federation/create-binding.png differ diff --git a/docs/assets/images/guides/administration/configuring-workload-identity-federation/create-provider.png b/docs/assets/images/guides/administration/configuring-workload-identity-federation/create-provider.png new file mode 100644 index 00000000..375eda56 Binary files /dev/null and b/docs/assets/images/guides/administration/configuring-workload-identity-federation/create-provider.png differ diff --git a/docs/concepts/.pages b/docs/concepts/.pages index 91150b96..846b3335 100644 --- a/docs/concepts/.pages +++ b/docs/concepts/.pages @@ -4,6 +4,7 @@ nav: - About projects: projects.md - About tags: tags.md - About access control: access-control.md + - About workload identity federation: workload-identity-federation.md - About vulnerability data sources: about-vulnerability-data-sources.md - About KEV data sources: about-kev-data-sources.md - About vulnerability findings: vulnerability-findings.md diff --git a/docs/concepts/access-control.md b/docs/concepts/access-control.md index 0c7ff6cf..e3481ad0 100644 --- a/docs/concepts/access-control.md +++ b/docs/concepts/access-control.md @@ -18,7 +18,7 @@ Four types of users exist: | Managed User | A local account created and managed within Dependency-Track. | | LDAP User | Authenticated via an external LDAP directory. See [Configuring LDAP](../guides/administration/configuring-ldap.md). | | OIDC User | Authenticated via an OpenID Connect identity provider. See [Configuring OIDC](../guides/administration/configuring-oidc.md). | -| Service Account | A non-human account for automation. Authenticates with its own API keys. Available in 5.2.0 and later. See [Service accounts](#service-accounts). | +| Service Account | A non-human account for automation. Authenticates with its own API keys, or through workload identity federation. Available in 5.2.0 and later. See [Service accounts](#service-accounts). | All user types share the same permission model. The authentication mechanism determines how the system verifies a user's identity, not what they can access. @@ -33,10 +33,19 @@ A service account is the identity of a CI pipeline, scanner, script, or other automated tool. Like any other user, it holds permissions directly or through team membership, and appears in team member lists. -A service account can't log in. It authenticates with API keys that it owns. -Each request made with such a key acts as the service account, so audit records -show its username. Suspending a service account stops all its API keys from working -until you lift the suspension. Deleting it also deletes its API keys. +A service account can't log in. It authenticates in one of two ways: + +* **Workload identity federation.** The workload exchanges a short-lived token + issued by its own platform, such as GitHub Actions, GitLab CI, Kubernetes, or + SPIFFE, for a session of the service account. See + [About workload identity federation](workload-identity-federation.md). +* **API keys** that the service account owns. Each request made with such a key + acts as the service account. + +Either way, audit records show the service account's username. Suspending a service +account stops all its API keys and blocks new token exchanges, until you lift the +suspension. It also ends sessions created through federation at once. Deleting the +account also deletes its API keys and its workload identity bindings. Service account usernames always start with `svc:`, for example `svc:ci-pipeline`. No other user can have a username with that prefix. This prevents a service account @@ -49,6 +58,13 @@ Compared to team API keys, service accounts: * Give each automated tool a distinct identity, even when tools share a team. * Take permissions directly, so narrowing one tool's permissions doesn't need a dedicated team. * Support suspension, which blocks access without deleting API keys. +* Can authenticate without any stored credential, through workload identity federation. + +!!! tip "The intended path for automation" + + Service accounts combined with [workload identity federation](workload-identity-federation.md) + are the strategic replacement for teams that exist only to hold a long-lived API key. + Team API keys remain supported. Start new integrations on service accounts. ## Teams @@ -65,8 +81,9 @@ exceptions that don't justify a dedicated team. Team API keys belong to a team and carry the same permissions as that team. They authenticate automated access (CI/CD pipelines, integrations) without associating requests with a -specific identity. In 5.2.0 and later, [service accounts](#service-accounts) offer an -alternative with their own identity and permissions. +specific identity. In 5.2.0 and later, [service accounts](#service-accounts) supersede them: +they carry their own identity and permissions, and can authenticate without a stored +credential at all. Deleting a team also deletes its API keys and any project access assignments made through it. It keeps projects, user accounts, and API keys owned by service @@ -205,5 +222,7 @@ and holders of `PORTFOLIO_ACCESS_CONTROL_BYPASS` until one is assigned. * [Permissions reference](../reference/permissions.md) for the full permissions table and default teams. +* [About workload identity federation](workload-identity-federation.md) for how + service accounts authenticate without stored credentials. * [About projects](projects.md) for how Dependency-Track models projects, hierarchies, and collection projects. diff --git a/docs/concepts/workload-identity-federation.md b/docs/concepts/workload-identity-federation.md new file mode 100644 index 00000000..bb7d1d53 --- /dev/null +++ b/docs/concepts/workload-identity-federation.md @@ -0,0 +1,101 @@ +# About workload identity federation + +!!! note "Available in 5.2.0 and later" + + Earlier versions authenticate automation with team API keys only. + +Workload identity federation lets a workload authenticate to Dependency-Track with a token +that its own platform issued, instead of an API key that someone created and stored. +A GitHub Actions job, a GitLab CI job, a Kubernetes pod, or a SPIFFE workload already holds a short-lived, +signed token that states its identity. Dependency-Track verifies that token and, +if it matches a rule an administrator configured, hands back a session for a [service account](access-control.md#service-accounts). + +The workload stores no Dependency-Track credential. + +## Why it exists + +An API key is a static secret with a long life. Someone creates it, copies it into a CI system, +and has to remember to delete it when it leaks and to rotate it before it expires. +Nothing about the key says which pipeline uses it, and a key that escapes the pipeline keeps +working from anywhere until a human notices. + +A platform token has none of those properties. It exists for minutes, it names the exact repository, +branch, namespace, or workload that received it, and nobody can mint one outside that context. + +Service accounts and workload identity federation together are the intended replacement for +automation that runs on team API keys: + +* The **service account** gives the automation an identity of its own, with its own permissions and its own audit trail. +* **Workload identity federation** gives that identity a credential nobody has to store. + +## How an exchange works + +```mermaid +sequenceDiagram + participant W as Workload
(CI job, k8s pod) + participant P as Platform
(GitHub, GitLab, Kubernetes, SPIRE) + participant D as Dependency-Track + + W->>P: Request a token for audience X + P-->>W: Signed token (sub, aud, exp, ...) + W->>D: Exchange token, naming a provider and a service account + D->>P: Fetch the issuer's public signing keys + D->>D: Verify signature, issuer, audience, expiry + D->>D: Match subject and condition against the bindings
of that service account + D-->>W: Session token for the service account + W->>D: Upload a BOM with the session token +``` + +The workload names both the provider and the service account it wants to act as. + +## Providers and bindings + +A **workload identity provider** says which issuer to trust. It holds the issuer, +the audience that tokens must carry, where the issuer's public signing keys come from, +and how long the sessions it creates last. Providers are instance-wide and only administrators manage them. + +A **workload identity binding** says which subject of that provider may act as which service account. +It belongs to a service account, names a provider, and matches a subject, either exactly or by prefix. +An optional condition, written in [CEL](../reference/cel-expressions.md), +narrows the match further by looking at any claim in the token. + +Bindings can't point at a human user. + +### Subjects and conditions + +A binding needs a subject even when it carries a condition, and the reason is the shape of shared issuers. +GitHub and GitLab.com issue tokens to anyone with an account, and let the requester choose the audience. +The audience isolates nothing there. Only the subject names the organization, +so a binding that matched on the condition alone would be open to every other tenant of that platform. + +Conditions exist because platforms don't put everything into the subject. +[GitLab CI](https://docs.gitlab.com/ci/secrets/id_token_authentication/), for example, +keeps the deployment environment in a separate claim, out of reach of a subject prefix. + +A condition is part of the security boundary, in that one that matches more tokens than intended +lets more workloads act as the service account. + +## The resulting session + +The exchange returns the same kind of opaque session token that the login endpoints issue, +carrying all permissions of the service account. + +The session lasts for the provider's configured lifetime, which can't exceed 24 hours. +The subject token proves the workload's identity at the moment of the exchange, +and short platform tokens make that gap deliberate. + +[GitLab CI](https://docs.gitlab.com/ci/secrets/id_token_authentication/) and +[SPIRE](https://spiffe.io/docs/latest/deploying/spire_server/) issue tokens that expire after five minutes by default, +and a CI job that uploads a BOM and waits for its analysis takes longer than that. + +A session outlives the token it came from, so deleting a binding or a provider doesn't end +sessions already created. Suspending the service account does, at once. + +## Further reading + +* [Configuring workload identity federation](../guides/administration/configuring-workload-identity-federation.md) + for the procedure and per-platform configuration. +* [Workload identity reference](../reference/workload-identity.md) for subject matching + rules, the condition environment, and token requirements. +* [About access control](access-control.md#service-accounts) for how service accounts relate + to users, teams, and permissions. diff --git a/docs/guides/administration/.pages b/docs/guides/administration/.pages index bed2ae90..4e004853 100644 --- a/docs/guides/administration/.pages +++ b/docs/guides/administration/.pages @@ -16,6 +16,7 @@ nav: - configuring-observability.md - configuring-ldap.md - configuring-oidc.md + - configuring-workload-identity-federation.md - managing-notification-templates.md - debugging-notifications.md - configuring-project-retention.md diff --git a/docs/guides/administration/configuring-workload-identity-federation.md b/docs/guides/administration/configuring-workload-identity-federation.md new file mode 100644 index 00000000..699bd771 --- /dev/null +++ b/docs/guides/administration/configuring-workload-identity-federation.md @@ -0,0 +1,364 @@ +# Configuring workload identity federation + +!!! note "Available in 5.2.0 and later" + + Earlier versions authenticate automation with team API keys only. + +Workload identity federation lets a CI job, a Kubernetes pod, or any other workload exchange a token +issued by its own platform for a Dependency-Track session, so that the workload stores no +API key. For the model behind it, see +[About workload identity federation](../../concepts/workload-identity-federation.md). + +Creating providers and bindings requires the `ACCESS_MANAGEMENT` permission, or its fine-grained variants. + +## Before you start + +Collect the following from your platform: + +* The **issuer**, for OpenID Connect providers. For SPIFFE, the **trust domain** instead. +* Whether Dependency-Track can reach the issuer's discovery document over HTTPS. + If it can't, you need the issuer's public signing keys as a JSON Web Key Set. +* The exact `sub` claim of the tokens the workload receives. Print one during a trial run + rather than guessing, because platforms change the format. + +Pick an **audience** that's unique to this Dependency-Track instance, such as its URL. +The same audience goes into every provider and into every token request the workload makes. + +## Create a service account + +Under **Administration > Access Management > Service Accounts**, create the account the +workload acts as and grant it only the permissions the workload needs, +such as `BOM_UPLOAD` and `PROJECT_CREATION_UPLOAD`. + +Give the workload its own account rather than reusing one. +The username appears in audit records, and suspending it cuts off exactly one workload. + +Don't create an API key for it. + +## Register a workload identity provider + +Go to **Administration > Access Management > Workload Identity Providers** and select **Create**. + +![Create provider modal](../../assets/images/guides/administration/configuring-workload-identity-federation/create-provider.png) + +Fill in the form: + +| Field | What to enter | +|:---------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------| +| **Name** | A short name such as `github-actions`. Workloads send this name when they exchange a token, and it can't change later. | +| **Type** | **OpenID Connect** for CI systems, cloud platforms, and Kubernetes. **SPIFFE** for SPIRE and other SPIFFE implementations. It can't change later. | +| **Issuer** | The exact value of the token's `iss` claim. For SPIFFE, the bare trust domain, such as `example.org`. | +| **Audience** | The value the token's `aud` claim must contain. | +| **Signing Keys** | Where the public keys come from. See [Choose a key source](#choose-a-key-source). | +| **Session Lifetime** | How long sessions from this provider last, in seconds. Between 60 and 86400, and 3600 by default. | + +Dependency-Track fetches the keys while saving, and refuses the provider if that fails. + +Register the same issuer twice if two groups of workloads need different session lifetimes or different audiences. +A binding has no lifetime of its own. + +### Choose a key source + +**Discover from the issuer's OpenID Connect configuration** is the default for OpenID Connect +providers, and the right choice whenever Dependency-Track can reach the issuer. +It resolves the key set from the issuer's [discovery document](../../reference/workload-identity.md#key-fetching). + +**Fetch from a JWKS URL** skips discovery. Use it for SPIFFE bundle endpoints, +and for issuers whose discovery document isn't reachable but whose key set is. + +**Provide a JWK Set** stores the keys in Dependency-Track and fetches nothing. +Use it for issuers Dependency-Track can't reach at all, mainly self-managed Kubernetes clusters. +Inline keys are never refreshed, so update the provider whenever the issuer rotates its keys. + +Fetched URLs must use `https`. An issuer inside your network works, subject to the +[outbound fetch constraints](../../reference/workload-identity.md#outbound-fetch-constraints). + +## Bind a subject to the service account + +Go back to **Administration > Access Management > Service Accounts**, +expand the service account, and select the plus icon under **Workload Identity Bindings**. + +![Create binding modal](../../assets/images/guides/administration/configuring-workload-identity-federation/create-binding.png) + +| Field | What to enter | +|:--------------|:------------------------------------------------------------------------------------------------------------------------------------| +| **Provider** | The provider you registered. | +| **Subject** | The exact `sub` claim to accept, or a prefix of one. See [subject matching](../../reference/workload-identity.md#subject-matching). | +| **Condition** | Optional [CEL](../../reference/cel-expressions.md) expression over the token's claims, available as the `claims` map. | + +On a shared issuer, make the subject prefix include at least your organization. +See [Subjects and conditions](../../concepts/workload-identity-federation.md#subjects-and-conditions). + +A service account can hold more than one binding. Dependency-Track uses the first that +matches, in [matching order](../../reference/workload-identity.md#matching-order). + +To revoke access, delete the binding. Sessions already issued stay valid until they expire. +To cut those off too, suspend the service account. + +## Exchange the token in the workload + +The workload posts its platform token to `/api/v2/oauth/token`, a [RFC 8693](https://www.rfc-editor.org/rfc/rfc8693) +token exchange endpoint. The request needs no authentication of its own. + +```bash +ACCESS_TOKEN=$(curl -sSf -X POST \ + "https://dependency-track.example.com/api/v2/oauth/token" \ + -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ + -d "subject_token_type=urn:ietf:params:oauth:token-type:jwt" \ + -d "subject_token=${PLATFORM_TOKEN}" \ + -d "workload_identity_provider=github-actions" \ + -d "service_account=ci-pipeline" \ + | jq -r .access_token) +``` + +Use the result as a bearer token: + +```bash +curl -sSf -X POST "https://dependency-track.example.com/api/v1/bom" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -F "project=${PROJECT_UUID}" \ + -F "bom=@bom.json" +``` + +Exchange once per job and reuse the session for the rest of it. The session lives for the provider's session lifetime, +and no endpoint renews it. For the full parameter list and the error responses, +see the operations tagged `OAuth` in the [REST API v2 reference](../../reference/api/v2.md). + +## Platform examples + +Replace `https://dependency-track.example.com` with your instance URL, and `acme-inc` with your organization. + + +### GitHub Actions + + +GitHub Actions issues OpenID Connect ID tokens from a single issuer shared by every account +on GitHub.com, so the subject prefix has to carry your organization. +GitHub's [OIDC reference](https://docs.github.com/en/actions/reference/security/oidc) lists +the claims and their current formats. + +Provider: + +| Field | Value | +|:-------------|:--------------------------------------------------------| +| Name | `github-actions` | +| Type | OpenID Connect | +| Issuer | `https://token.actions.githubusercontent.com` | +| Audience | `https://dependency-track.example.com` | +| Signing Keys | Discover from the issuer's OpenID Connect configuration | + +Binding, for every repository in the organization: + +| Field | Value | +|:----------|:------------------| +| Subject | `repo:acme-inc/*` | +| Condition | (none) | + +!!! warning "Two subject formats exist" + + Repositories created after 2026-07-15, and older ones that opt in, + use an [immutable subject](https://docs.github.com/en/actions/reference/security/oidc#immutable-subject-claims) + that carries numeric IDs, such as `repo:acme-inc@123456/app@456789:ref:refs/heads/main`. + A prefix of `repo:acme-inc/*` doesn't match it. Use `repo:acme-inc@123456/*` for those repositories, + and add a second binding if your organization has both formats in use. + +The job needs the `id-token: write` permission, and requests the token for your audience: + +```yaml title=".github/workflows/upload-bom.yml" +jobs: + upload-bom: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: Upload BOM + env: + DT_URL: https://dependency-track.example.com + run: | + PLATFORM_TOKEN=$(curl -sSf \ + -H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ + "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${DT_URL}" | jq -r .value) + # Exchange PLATFORM_TOKEN as shown above. +``` + +`ACTIONS_ID_TOKEN_REQUEST_URL` already carries a query string, which is why the audience is preceded by `&`. + +### GitLab CI + +GitLab CI issues ID tokens from the URL of the GitLab instance. +On GitLab.com every account shares that issuer, so the subject prefix has to carry your group. +GitLab's [ID token authentication](https://docs.gitlab.com/ci/secrets/id_token_authentication/) guide +lists the claims and their current formats. + +Provider: + +| Field | Value | +|:-------------|:--------------------------------------------------------| +| Name | `gitlab` | +| Type | OpenID Connect | +| Issuer | `https://gitlab.com`, or the URL of your instance | +| Audience | `https://dependency-track.example.com` | +| Signing Keys | Discover from the issuer's OpenID Connect configuration | + +The job declares the token and the audience it wants: + +```yaml title=".gitlab-ci.yml" +upload-bom: + id_tokens: + DT_TOKEN: + aud: https://dependency-track.example.com + script: + - ./upload-bom.sh # Exchanges $DT_TOKEN as shown in "Exchange the token in the workload". +``` + +#### Matching on the environment requires a condition + +The GitLab subject is `project_path:{group}/{project}:ref_type:{type}:ref:{branch}`. +It only names the project and the git ref. The deployment environment lives in separate claims, +so a rule such as "only the job that deploys to production" **doesn't fit into a subject at all**. +Write it as a condition: + +| Field | Value | +|:----------|:-------------------------------------------------------------------------------| +| Subject | `project_path:acme-inc/*` | +| Condition | `claims.environment == "production" && claims.environment_protected == "true"` | + +!!! note + + GitLab encodes the boolean-looking claims `environment_protected` and `ref_protected` as the + strings `"true"` and `"false"`, so `claims.environment_protected == true` never matches. + +The `environment` claim only appears on jobs that declare an `environment:`. A condition that +reads a missing claim raises an error, and a binding whose condition errors doesn't match, +so this binding rejects every job that isn't a deployment. + +### Kubernetes + +A pod authenticates with a [projected service account token][k8s-sa]. +The subject is `system:serviceaccount:{namespace}:{name}`. + +A cluster set up with `kubeadm` issues tokens from the cluster-internal issuer +`https://kubernetes.default.svc.cluster.local`. That name only resolves inside the cluster, +so Dependency-Track can't run discovery against it. Store the keys inline instead: + +```shell +kubectl get --raw /openid/v1/jwks +``` + +Provider: + +| Field | Value | +|:-------------|:------------------------------------------------------| +| Name | `k8s-prod` | +| Type | OpenID Connect | +| Issuer | The cluster's issuer, exactly as it appears in `iss` | +| Audience | `https://dependency-track.example.com` | +| Signing Keys | Provide a JWK Set, pasting the output of that command | + +Where Dependency-Track can reach the cluster's discovery document, as with many managed offerings, +use the cluster's issuer URL with discovery and skip the manual key set. + +Binding, for one service account: + +| Field | Value | +|:--------|:----------------------------------------| +| Subject | `system:serviceaccount:ci:bom-uploader` | + +The pod asks for a token with the right audience through a projected volume: + +```yaml +spec: + serviceAccountName: bom-uploader + containers: + - name: upload-bom + image: acme-inc/upload-bom:1.0.0 + volumeMounts: + - name: dt-token + mountPath: /var/run/secrets/dependency-track + readOnly: true + volumes: + - name: dt-token + projected: + sources: + - serviceAccountToken: + audience: https://dependency-track.example.com + expirationSeconds: 3600 + path: token +``` + +The container reads the token from `/var/run/secrets/dependency-track/token` and sends it as `subject_token`. +The kubelet refreshes the file, so read it right before each exchange. + +### SPIFFE and SPIRE + +SPIFFE workloads present [JWT-SVIDs][spiffe-jwt-svid], which may omit the `iss` claim and +carry their trust domain in the subject instead. Register them with type **SPIFFE**. + +Provider: + +| Field | Value | +|:-------------|:----------------------------------------------------------------------| +| Name | `spire` | +| Type | SPIFFE | +| Issuer | `example.org`, the bare trust domain without `spiffe://` | +| Audience | `https://dependency-track.example.com` | +| Signing Keys | Fetch from a JWKS URL, pointing at the trust domain's bundle endpoint | + +Only bundle endpoints with a publicly trusted certificate work, +which is the `https_web` profile of [SPIFFE federation][spiffe-federation]. +For a trust domain without such an endpoint, export the bundle and store it inline: + +```shell +spire-server bundle show -format spiffe +``` + +Binding: + +| Field | Value | +|:----------|:----------------------------| +| Subject | `spiffe://example.org/ci/*` | +| Condition | (none) | + +A SPIFFE prefix must end with `/`. Changing the trust domain of the provider stops every one of +its bindings from matching, so update their subjects afterwards. + +The workload fetches its JWT-SVID for the audience from the [SPIRE agent][spire-agent]: + +```shell +spire-agent api fetch jwt -audience https://dependency-track.example.com +``` + +## Verify the setup + +Run the exchange once from the workload, then confirm the identity that its session carries: + +```bash +curl -sSf "https://dependency-track.example.com/api/v1/user/self" \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" +``` + +The `username` in the response is the service account, prefixed with `svc:`. + +The binding's **Last used** timestamp updates on every successful exchange, +which is the quickest way to tell whether a binding is the one that matched. + +## Troubleshooting + +The exchange endpoint returns the same `invalid_request` error whatever went wrong, +so it never names the failing check. The API server log records every refused exchange as a security event, +including the token's claims when the token verified but no binding matched. + +## See also + +* [About workload identity federation](../../concepts/workload-identity-federation.md) for + the model behind providers, bindings, and sessions. +* [Workload identity reference](../../reference/workload-identity.md) for subject matching + rules, the condition environment, and token requirements. +* [REST API v2 reference](../../reference/api/v2.md) for request and response schemas. +* [Permissions](../../reference/permissions.md) for what to grant the service account. + +[k8s-sa]: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/ +[spiffe-federation]: https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE_Federation.md +[spiffe-jwt-svid]: https://github.com/spiffe/spiffe/blob/main/standards/JWT-SVID.md +[spire-agent]: https://spiffe.io/docs/latest/deploying/spire_agent/ diff --git a/docs/guides/upgrading/v5.2.0.md b/docs/guides/upgrading/v5.2.0.md index 6d51e476..1b30dacb 100644 --- a/docs/guides/upgrading/v5.2.0.md +++ b/docs/guides/upgrading/v5.2.0.md @@ -1,8 +1,8 @@ # Upgrading to v5.2.0 -* **Service accounts reserve the `svc:` username prefix**. This release adds [service accounts][access-control-svc], - a user type for CI pipelines, scripts, and other automation. Their usernames start with `svc:`, - and no other user can have a username with that prefix, in any letter case. +* **Service accounts reserve the `svc:` username prefix**. This release adds + [service accounts][access-control-svc]. Existing users whose username starts with `svc:`, + in any letter case, collide with that reservation. If such a user exists, the database migration fails and its error message lists the affected users. To find them before you upgrade, run this query against the database: diff --git a/docs/includes/abbreviations.md b/docs/includes/abbreviations.md index 88dcf7da..6d1f23d3 100644 --- a/docs/includes/abbreviations.md +++ b/docs/includes/abbreviations.md @@ -10,6 +10,8 @@ *[GHSA]: GitHub Security Advisory *[IdP]: Identity Provider *[JVM]: Java Virtual Machine +*[JWKS]: JSON Web Key Set +*[JWT]: JSON Web Token *[KEK]: Key Encryption Key *[KEV]: Known Exploited Vulnerabilities *[LDAP]: Lightweight Directory Access Protocol @@ -23,5 +25,7 @@ *[SARIF]: Static Analysis Results Interchange Format *[SBOM]: Software Bill of Materials *[SLO]: Service Level Objectives +*[SPIFFE]: Secure Production Identity Framework for Everyone +*[SVID]: SPIFFE Verifiable Identity Document *[VDR]: Vulnerability Disclosure Report *[VEX]: Vulnerability Exploitability eXchange diff --git a/docs/reference/.pages b/docs/reference/.pages index deeb1273..4d34fe76 100644 --- a/docs/reference/.pages +++ b/docs/reference/.pages @@ -14,6 +14,7 @@ nav: - CEL expressions: cel-expressions.md - Access control: - Permissions: permissions.md + - Workload identity: workload-identity.md - Integrations: - Badges: badges.md - File formats: file-formats.md diff --git a/docs/reference/cel-expressions.md b/docs/reference/cel-expressions.md index 2b89caa7..58d98044 100644 --- a/docs/reference/cel-expressions.md +++ b/docs/reference/cel-expressions.md @@ -1,6 +1,6 @@ # CEL Expressions -Dependency-Track uses the [Common Expression Language] (CEL) in two places: +Dependency-Track uses the [Common Expression Language] (CEL) in three places: * **Policy conditions**, evaluated against components or vulnerabilities to drive policy violations and analyses. See @@ -8,12 +8,16 @@ Dependency-Track uses the [Common Expression Language] (CEL) in two places: * **Notification filters**, evaluated against notification subjects to control which notifications are dispatched. See [Filter expressions](notifications/filter-expressions.md). +* **Workload identity binding conditions**, evaluated against the claims of a + verified platform token to decide whether a workload may act as a service + account. Available in 5.2.0 and later. See + [Workload identity](workload-identity.md#condition-environment). -The two contexts share the syntax described on this page, but expose **different +The contexts share the syntax described on this page, but expose **different variables, types, and custom functions**. The custom functions documented for policies (for example `depends_on`, `spdx_expr_allows`) are not available to -notification filters, and the variables differ entirely. Refer to the -context-specific page for the available inputs and functions. +notification filters or binding conditions, and the variables differ entirely. +Refer to the context-specific page for the available inputs and functions. ## Syntax @@ -33,7 +37,8 @@ Both contexts have access to the [standard definitions] of the CEL specification The policy context registers additional custom functions (`depends_on`, `spdx_expr_allows`, and so on); see [Condition expressions](policies/condition-expressions.md#function-reference). -Notification filters do not register any custom functions. +Notification filters and workload identity binding conditions do not register any custom +functions. ## Optional field checking @@ -46,8 +51,8 @@ before accessing it: has(obj.field) && obj.field == "value" ``` -The pattern applies in both contexts. The exact root variables (`component`, `subject`, -and so on) are documented on the context-specific reference pages linked above. +The `has()` macro works in all three contexts. The exact root variables (`component`, +`subject`, `claims`, and so on) are documented on the context-specific reference pages. [C-style languages]: https://en.wikipedia.org/wiki/List_of_C-family_programming_languages [CEL strings extension]: https://github.com/google/cel-spec/blob/master/doc/extensions/strings.md diff --git a/docs/reference/permissions.md b/docs/reference/permissions.md index 189db1d7..73eafdcd 100644 --- a/docs/reference/permissions.md +++ b/docs/reference/permissions.md @@ -64,11 +64,11 @@ and [Managing project versions](../guides/user/managing-project-versions.md). | Permission | Description | |:------------------------------|:--------------------------------------------------------------------| -| `ACCESS_MANAGEMENT` | Manage users, service accounts, teams, permissions, and API keys. | -| `ACCESS_MANAGEMENT_CREATE` | Create users, service accounts, teams, and API keys. | -| `ACCESS_MANAGEMENT_READ` | Read users, service accounts, teams, and API keys. | -| `ACCESS_MANAGEMENT_UPDATE` | Update users, service accounts, teams, and API keys. | -| `ACCESS_MANAGEMENT_DELETE` | Delete users, service accounts, teams, and API keys. | +| `ACCESS_MANAGEMENT` | Manage users, service accounts, teams, permissions, API keys, and workload identity. | +| `ACCESS_MANAGEMENT_CREATE` | Create users, service accounts, teams, API keys, workload identity providers, and bindings. | +| `ACCESS_MANAGEMENT_READ` | Read users, service accounts, teams, API keys, workload identity providers, and bindings. | +| `ACCESS_MANAGEMENT_UPDATE` | Update users, service accounts, teams, API keys, and workload identity providers. | +| `ACCESS_MANAGEMENT_DELETE` | Delete users, service accounts, teams, API keys, workload identity providers, and bindings. | ### System configuration diff --git a/docs/reference/workload-identity.md b/docs/reference/workload-identity.md new file mode 100644 index 00000000..232bcde0 --- /dev/null +++ b/docs/reference/workload-identity.md @@ -0,0 +1,125 @@ +# Workload identity + +!!! note "Available in 5.2.0 and later" + + Earlier versions authenticate automation with team API keys only. + +For the model, see [About workload identity federation](../concepts/workload-identity-federation.md). +For the procedure, see [Configuring workload identity federation](../guides/administration/configuring-workload-identity-federation.md). + +Field constraints, request bodies, responses, and the permission each operation requires are +in the [REST API v2 reference](api/v2.md), under the operations tagged `Workload Identity Providers`, `OAuth`, +and the workload identity binding operations tagged `Service Accounts`. +This page describes the behavior behind those operations. + +## Provider types + +| Type | `iss` claim | `sub` claim | Key source | +|:---------|:---------------------------------|:-------------------------------------------|:------------------------------------| +| `OIDC` | Must equal the provider's issuer | Any value | Discovery, a key set URL, or inline | +| `SPIFFE` | Ignored | Must start with `spiffe:///` | A bundle endpoint URL, or inline | + +The issuer of a `SPIFFE` provider is a bare trust domain, such as `example.org`. +A key set URL on a `SPIFFE` provider is the trust domain's bundle endpoint, +which must use the `https_web` profile of SPIFFE federation. + +## Key fetching + +Discovery reads `/.well-known/openid-configuration`, requires the document to name the same issuer, +and stores the `jwks_uri` it names. Changing the issuer of an `OIDC` provider that fetches its keys from +a URL re-runs discovery and replaces the stored URL, unless the update names a key source itself. + +Dependency-Track caches fetched key sets and rate-limits how often it refreshes them per provider. +When a refresh fails, it keeps using the last fetched key set for up to one hour. + +### Outbound fetch constraints + +Dependency-Track fetches remote documents when you save a provider, +and during an exchange when a key is missing from the cache. Every fetch obeys the following: + +* Only `https` URLs, without user information. +* After resolving the hostname, Dependency-Track refuses loopback, link-local, wildcard, + and multicast addresses. Private ranges stay allowed. When a configured HTTP proxy handles the destination, + Dependency-Track skips the address check, because the proxy resolves the name. +* Dependency-Track doesn't follow redirects. +* A response must arrive complete within 10 seconds, and must not exceed 1 MiB. +* Response bodies never appear in API responses or logs. + +## Subject matching + +A subject is either an exact value or a prefix followed by `*`. +The prefix must be non-empty and must end with `:` or `/`. + +| Subject | Matches | +|:----------------------------------------|:--------------------------------------------------| +| `repo:acme-inc/app:ref:refs/heads/main` | That exact `sub` claim. | +| `repo:acme-inc/*` | Every `sub` starting with `repo:acme-inc/`. | +| `repo:acme-inc/app*` | Rejected. The prefix doesn't end with `:` or `/`. | +| `spiffe://example.org/ci/*` | Every SPIFFE ID under `spiffe://example.org/ci/`. | +| `spiffe://example.org/ci*` | Rejected. A SPIFFE prefix must end with `/`. | + +For `SPIFFE` providers, the subject must lie within the provider's trust domain. + +To match a prefix that ends mid-segment, use an exact subject prefix plus a condition, +such as `claims.sub.startsWith("repo:acme-inc/app-")`. + +## Condition environment + +Conditions are [CEL](cel-expressions.md) expressions. +Dependency-Track compiles and type-checks them when you save the binding. +They see one variable: + +| Variable | Type | Contents | +|:---------|:-------------------|:------------------------------------------| +| `claims` | `map(string, dyn)` | The claims of the verified subject token. | + +The [CEL strings extension](https://github.com/google/cel-spec/blob/master/doc/extensions/strings.md) is available. +Dependency-Track registers no custom functions here. + +An expression must return a boolean. Because claim values are dynamic, +`claims.admin` alone doesn't compile. Write `claims.admin == true` instead. + +At exchange time, a condition that raises an error, for example because a claim is absent, doesn't match. +Guard optional claims with `has(claims.name)` where the binding should still match tokens that omit them. +Claim values keep their JSON types, and platforms that encode booleans as strings need string comparisons. + +```js +claims.ref == "refs/heads/main" + +claims.environment == "production" && claims.environment_protected == "true" + +claims.sub.startsWith("repo:acme-inc/app-") +``` + +## Matching order + +Dependency-Track evaluates only the bindings of the service account that the request names, +and only those belonging to the named provider. It tries them from oldest to newest and uses +the first whose subject and condition both match. Duplicate and overlapping bindings do no harm, +because the resulting session is the same either way. + +## Subject token requirements + +A subject token must be a signed JWT that meets these requirements: + +| Property | Value | +|:--------------------|:---------------------------------------------------------------------------------------| +| Header `typ` | `JWT`, `JOSE`, `at+jwt`, or absent. | +| Signature algorithm | `RS256`, `RS384`, `RS512`, `PS256`, `PS384`, `PS512`, `ES256`, `ES384`, or `ES512`. | +| Required claims | `exp` and `sub`. | +| `aud` | Must contain the provider's audience. | +| `iss` | Must equal the provider's issuer for `OIDC` providers. Ignored for `SPIFFE` providers. | + +Dependency-Track refuses symmetric and unsigned tokens. It accepts keys marked for `jwt-svid` use, +as SPIFFE bundles mark them, alongside keys marked for signature use. + +## Audit events + +The API server logs the following as security events: + +* Creating, updating, and deleting providers and bindings. +* Successful exchanges, naming the provider, the service account, the matched binding, + the token's subject, and its `jti` if it has one. +* Refused exchanges. When the token verified but no binding matched, the log also carries the token's claims. + +Subject tokens are never logged.