Skip to content

Documentation for idtoken var source #563

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lit/docs/operation/creds.lit
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ backend you want to use.
\include-section{creds/aws-secretsmanager.lit}
\include-section{creds/kubernetes.lit}
\include-section{creds/cyberark-conjur.lit}
\include-section{creds/idtoken.lit}
\include-section{creds/caching.lit}
\include-section{creds/redacting.lit}
\include-section{creds/retry.lit}
295 changes: 295 additions & 0 deletions lit/docs/operation/creds/idtoken.lit
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
\title{\aux{The }IDToken credential manager}{idtoken-credential-manager}

\use-plugin{concourse-docs}
\omit-children-from-table-of-contents

This idtoken credential manager is a bit special. It does not load any credentials from an external source, but instead generates \link{JWTs}{https://datatracker.ietf.org/doc/html/rfc7519} which are signed by concourse and contain information about the pipeline/job
that is currently running. It can NOT be used as a cluster-wide credential manager, but must instead be used as a \reference{var-sources}{var source}.

These JWTs can be used to authenticate with external services via "identity federation" with the identity of the pipeline.

Examples for services that support authentication via JWTs are:

\list{
\link{Vault}{https://vaultproject.io}
}{
\link{AWS}{https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers.html}
}{
\link{Azure}{https://learn.microsoft.com/en-us/graph/api/resources/federatedidentitycredentials-overview?view=graph-rest-1.0}
}


External Services can verify if JWTs are actually issued by your Concourse, by checking the signatures on the JWTs against the public keys published by your Concourse.

The public keys for verification are published as \link{JWKS}{https://datatracker.ietf.org/doc/html/rfc7517} at:
\codeblock{bash}{{{
https://your-concourse-server.com/.well-known/jwks.json
}}}

Concourse also has a minimal \link{OIDC Discovery Endpoint}{https://openid.net/specs/openid-connect-discovery-1_0.html}, which allows external services to auto-discover the JWKS-URL.

\section{
\title{Usage}

You create a var-source of type \code{idtoken} with the configuration you want (see \reference{idtoken-credential-manager-config}{next chapter}) in your pipeline.
That var-source then exposes a single variable \code{token}, which contains the JWT and can be used in any step of your pipeline.

You can also have multiple idtoken var-sources in the same pipeline, each with different audiences, lifetimes etc.

\codeblock{bash}{{{
var_sources:
- name: myidtoken
type: idtoken
config:
audience: ["sts.amazonaws.com"]

jobs:
- name: print-creds
plan:
- task: print
config:
platform: linux
image_resource:
type: mock
source: {mirror_self: true}
run:
path: bash
args:
- -c
- |
echo myidtoken: ((myidtoken:token))
}}}
}

\section{
\title{Configuration}{idtoken-credential-manager-config}

You can pass several config options to the var-source to customize the generated JWTs.
You can for example configure the aud-claim, expiration or granularity of the sub-claim.
See \reference{idtoken-var-source}{here} for details.

\section{
\title{Subject Scope}{idtoken-subject-scope}

Some external services (like for example AWS) only perform exact-matches on a token's sub-claim and ignore most other claims.
To enable use-cases like for example "all pipelines of a team should be able to assume an AWS-Role", Concourse offers the option to configure how fine-granular the sub-claim's value should be.

This is configured via the \code{subject_scope} setting of the \reference{idtoken-var-source}{var-source}.

Depending of the value of \code{subject_scope}, the content of the JWT's sub-claim will differ:
\list{
\code{team}: sub="<team_name>"
}{
\code{pipeline}: sub="<team_name>/<pipeline_name>"
}{
\code{instance}: sub="<team_name>/<pipeline_name>/<instance_vars>"
}{
\code{job}: sub="<team_name>/<pipeline_name>/<instance_vars>/<job_name>"
}

\smaller{Note: If a path element is empty (for example because you chose \code{job} on a pipeline with no instance-vars), the empty element is still added.}

This way all your pipelines can simply get a token with \code{subject_scope="team"} and use this token to assume an AWS-Role that matches on \code{sub="yourteamname"}.

}

}

\section{
\title{Example JWT}

The generated tokens usually look something like this:
\code{
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3lvdXItY29uY291cnNlLmV4YW1wbGUuY29tIiwiZXhwIjoxNzUxMDE1NzM0LCJhdWQiOlsiYXBpOi8vQXp1cmVBRFRva2VuRXhjaGFuZ2UiXSwic3ViIjoibWFpbi9leGFtcGxlLXBpcGVsaW5lIiwidGVhbSI6Im1haW4iLCJwaXBlbGluZSI6ImV4YW1wbGUtcGlwZWxpbmUiLCJqb2IiOiJleGFtcGxlLWpvYiJ9.my7l44tH0wfz8vc6z3fMmzTMxZ8_orhjcsOti3BKSNo
}

And after decoding like this:
\codeblock{bash}{{{
{
"iss": "https://your-concourse.example.com",
"exp": 1751015734,
"aud": ["api://AzureADTokenExchange"],
"sub": "main/example-pipeline",
"team": "main",
"pipeline": "example-pipeline",
"instance_vars": {
"branch": "dev"
}
"job": "example-job"
}
}}}

Here is a short explanation of the different claims:

\list{
\code{iss}: Who issued the token (always contains the external URL of your Concourse)
}{
\code{exp}: When the token will expire
}{
\code{aud}: Who the token is intended for. (In the above example it's for Azure's Identity Federation API)
}{
\code{team}: The team of the pipeline this token was generated for
}{
\code{pipeline}: The pipeline this token was generated for
}{
\code{job}: The name of the job (inside the pipeline) this token was generated for
}{
\code{instance_vars}: Any instance_vars for the pipeline (if it is an instanced pipeline).
}{
\code{sub}: A combination of team + pipeline + instance_vars + job. Which parts are used here is configurable, see \reference{idtoken-subject-scope}{here}.
}
}

\section{
\title{Automatic Key Rotation}{idtoken-key-rotation}

Concourse will automatically rotate the signing keys used for creating the JWTs. The default rotation period is \code{7 days}. The previously used keys are being kept around for a while (\code{24h})
so that verification of currently existing JWTs doesn't fail.

That behavior can be configured via the following ATC flags:

\list{
\code{signing-key.rotation-period}: How often to rotate the signing keys. Default: \code{7d}. 0 means don't rotate at all.
}{
\code{signing-key.grace-period}: How long to keep previously used signing keys published in the JWKs after they have been superceeded. Default: \code{24h}.
}{
\code{signing-key.check-interval}: How often to check if new keys are needed or if old ones should be removed. Default: \code{10m}
}

}

\right-side{Examples}{
\example{Vault}{
You can use JWTs to authenticate with \link{HashiCorp Vault}{https://developer.hashicorp.com/vault/docs/auth/jwt#jwt-authentication}. This way your pipelines can directly communicate with Vault and use all of it's features, beyond what Concourse's native vault-integration offers.

First enable the JWT auth method in your Vault Server:
\codeblock{bash}{{{
vault auth enable jwt
}}}

Now configure the JWT auth method to accept JWTs issued by your Concourse:
\codeblock{bash}{{{
vault write auth/jwt/config \
oidc_discovery_url="https://<external_url_of_your_concourse>" \
default_role="demo"
}}}

Lastly configure a "role" for JWT auth. Make sure to use the same value in your pipeline that you used for \code{bound_audiences}. \code{bound_subject} must be the sub-claim value of your JWT, if you use the \code{subject_scope} setting to change the contents of your sub-claim, adapt this accordingly!
\codeblock{bash}{{{
vault write auth/jwt/role/demo \
bound_subject="main/your-pipeline" \
bound_audiences="my-vault-server.com" \
policies=webapps \
ttl=1h
}}}

This role will allow the holder of a JWT with \code{aud=my-vault-server.com} and \code{sub=main/your-pipeline} to get a vault-token with the vault-policy "webapps".

Pipelines can now do the following:
\codeblock{bash}{{{
var_sources:
- name: vaulttoken
type: idtoken
config:
audience: ["my-vault-server.com"]

jobs:
- name: aws-deploy
plan:
- task: print
config:
platform: linux
image_resource:
type: registry-image
source: { repository: hashicorp/vault }
run:
path: bash
args:
- -c
- |
vault write auth/jwt/login role=demo jwt=((vaulttoken:token)) > vault-response.json
echo "Now do something with the token in vault-response.json"
}}}

You don't have to create a role and a policy for every single of your pipelines!
You can use claims from the JWT with Vault's \link{policy templating}{https://developer.hashicorp.com/vault/tutorials/policies/policy-templating} feature.
This way you can define a policy that allows a pipeline access to all secrets it would usually have access to using Concourse's native vault-integration:

\codeblock{bash}{{{
path "secret/metadata/concourse/{{ identity.entity.aliases.<JWT_ACCESSOR>.metadata.team }}" {
capabilities = ["list"]
}

path "secret/data/concourse/{{ identity.entity.aliases.<JWT_ACCESSOR>.metadata.team }}/+" {
capabilities = ["read"]
}

path "secret/metadata/concourse/{{ identity.entity.aliases.<JWT_ACCESSOR>.metadata.team }}/{{ identity.entity.aliases.<JWT_ACCESSOR>.metadata.pipeline }}" {
capabilities = ["list"]
}

path "secret/metadata/concourse/{{ identity.entity.aliases.<JWT_ACCESSOR>.metadata.team }}/{{ identity.entity.aliases.<JWT_ACCESSOR>.metadata.pipeline }}/*" {
capabilities = ["read", "list"]
}

path "secret/data/concourse/{{ identity.entity.aliases.<JWT_ACCESSOR>.metadata.team }}/{{ identity.entity.aliases.<JWT_ACCESSOR>.metadata.pipeline }}/*" {
capabilities = ["read", "list"]
}
}}}
\smaller{Make sure to set \code{<JWT_ACCESSOR>} to the actual mount-accessor value of your JWT Auth method!}

With a policy like this you don't need to configure \code{bound_subject} for your JWT auth role. Every single pipeline can simply use the same role and the policy will take care that they can only access secrets ment for them.

}

\example{AWS}{
AWS supports \link{federation with external identity providers}{https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers.html}.
Using this you can allow identities managed by an external identity provider to perform actions in your AWS account.

In this scenario the external identity provider is Concourse and the identities are teams/pipelines/jobs.
This means you are able to grant a specific pipeline or job permissions to perform actions in AWS (like deploying something). All without managing IAM-Users or dealing
with long-lived credentials.

First you need to \link{create an OpenID Connect identity provider}{https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html} in your AWS Account.
As \code{Provider URL} specify the external URL of your Concourse server.
For \code{Audience} you can choose any string you like. Using something like \code{sts.amazonaws.com} is recommended. You have to use the same string later in the configuration of your var-source.

Next you will need to \link{create an IAM-Role that can be assumed using your JWT}{https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html#idp_oidc_Create}.
As \code{Identity Provider} choose the one you created previously.
Add a condition on the sub-claim with type \code{StringEquals} and value \code{yourteam/yourpipeline}. This will allow ONLY that specific pipeline (and any instanced versions of it) to assume that IAM Role using a JWT. If you use the \code{subject_scope} setting to change the contents of your sub-claim, adapt this condition accordingly!
In the next step you will be able to choose which AWS permissions your role will get.

Now you can use the AWS \link{AssumeRoleWithWebIdentity API operation}{https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html} to assume your role via a JWT issued by concourse.
The easiest way is to do this is via the \link{assume-role-with-web-identity AWS CLI command}{https://docs.aws.amazon.com/cli/latest/reference/sts/assume-role-with-web-identity.html}:
\codeblock{bash}{{{
var_sources:
- name: awstoken
type: idtoken
config:
audience: ["sts.amazonaws.com"]

jobs:
- name: aws-deploy
plan:
- task: print
config:
platform: linux
image_resource:
type: registry-image
source: { repository: amazon/aws-cli }
run:
path: bash
args:
- -c
- |
aws sts assume-role-with-web-identity --role-arn arn:aws:iam::<your_account>:role/<your_role> --web-identity-token ((awstoken:token)) > creds.json
echo "Now do something with the temporary credentials in creds.json"
}}}
}

\example{Azure}{

TODO

}
}
37 changes: 37 additions & 0 deletions lit/docs/vars.lit
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,8 @@ configured multiple times with different parameters
\reference{aws-ssm-credential-manager}{\code{ssm}}
}{
\reference{aws-asm-credential-manager}{\code{secretmanager}} (since v7.7.0)
}{
\reference{idtoken-credential-manager}{\code{idtoken}} (since v7.14.0)
}

In the future we want to make use of something like the \link{Prototypes
Expand Down Expand Up @@ -524,6 +526,41 @@ configured multiple times with different parameters
}
}
}
}{
\schema-group{\code{idtoken} var source}{idtoken-var-source}{
\required-attribute{type}{`idtoken`}{
The \code{idtoken} type issues JWTs which are signed by concourse and contain information about the currently running pipeline/job.

These JWTs can be used to authenticate with external services.
}

\required-attribute{config}{idtoken_config}{
\schema{idtoken_config}{
\required-attribute{audience}{[string]}{
A list of audience-values to place into the token's aud-claim.
}
\optional-attribute{subject_scope}{string}{
Determines what is put into the token's sub-claim. See \reference{idtoken-subject-scope}{here} for e detailed explanation.

Supported values: \code{team}, \code{pipeline}, \code{instance}, \code{job}.

Default \code{pipeline}.
}
\optional-attribute{expires_in}{duration}{
How long the token should be valid.

Default \code{1h}. Can be at max \code{24h}.
}
\optional-attribute{algorithm}{string}{
The signature algorithm to use for the token.

Supported values: \code{RS256}, \code{ES256}.

Default \code{RS256}.
}
}
}
}
}
}
}
Expand Down