Identity of Runs
Rather than storing long-lived keys as secrets, a run can prove which organization it belongs to. Cadence.CI is an OpenID Connect issuer: each run gets short-lived identity tokens, which AWS, Google Cloud, Azure, Vault and OpenBao accept once you set up a trust, as they accept the tokens of GitHub Actions and GitLab CI jobs.
Getting a Token
On a Cadence.CI worker, the api-id:<audience> locator gives the identity token of the run for an audience, as a Secret<string>: api-id:sts.amazonaws.com for AWS, api-id:vault for a Vault role bound to the vault audience. Pass it to the element exchanging it for access, as you would any secret.
A token is valid for 10 minutes; the engine requests a new one once it has expired. Like api:, api-id: only works on Cadence.CI workers, and the token is never shown.
Issuer
| Issuer | https://api.melodium.tech |
| Discovery | https://api.melodium.tech/.well-known/openid-configuration |
| Keys | https://api.melodium.tech/.well-known/jwks.json |
| Signature | RS256 |
| Lifetime | 10 minutes |
Claims
| Claim | Value |
|---|---|
iss | https://api.melodium.tech |
aud | the audience requested |
sub | organization:<organization-id>, followed by :public for public runs and the runs they start |
organization_id | the organization of the run |
run_id | the run |
group_id | its execution group |
parent_id | the run that started it, if any |
tags | the tags of the run |
public | whether the run, or a run that started it, is public |
melodium_version | the Mélodium version of the engine |
iat, nbf, exp | when it was issued, from when it is valid, when it expires |
jti | its unique identifier |
The subject is the same for every private run of an organization, so that providers comparing it exactly, as AWS and Azure do, trust the organization as a whole. Conditions on tags or runs go through the other claims, where the provider can test them, as Google Cloud, Vault and OpenBao do. Public runs, and the runs they start, have a subject of their own: a trust set up for organization:<organization-id> never accepts them. Keep it that way for anything sensitive, as public runs execute code anyone can propose.
The identifier of your organization is on the Organization page of the dashboard.
AWS
- In IAM, under Identity providers, add an OpenID Connect provider with the URL
https://api.melodium.techand the audiencests.amazonaws.com. - Create a role for this provider, with a trust policy limited to your organization:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::<account-id>:oidc-provider/api.melodium.tech"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"api.melodium.tech:aud": "sts.amazonaws.com",
"api.melodium.tech:sub": "organization:<organization-id>"
}
}
}
]
}- In the program, give
api-id:sts.amazonaws.comto what assumes the role withAssumeRoleWithWebIdentity, with the ARN of the role.
Google Cloud
- Create a workload identity pool, and a provider trusting the runs of your organization:
gcloud iam workload-identity-pools create cadence-ci --location=global
gcloud iam workload-identity-pools providers create-oidc cadence-ci \
--location=global \
--workload-identity-pool=cadence-ci \
--issuer-uri="https://api.melodium.tech" \
--allowed-audiences="gcp" \
--attribute-mapping="google.subject=assertion.sub,attribute.organization_id=assertion.organization_id" \
--attribute-condition="assertion.organization_id == '<organization-id>' && !assertion.public"- Grant the roles the runs need to the principal set of the organization,
principalSet://iam.googleapis.com/projects/<project-number>/locations/global/workloadIdentityPools/cadence-ci/attribute.organization_id/<organization-id>, or let it impersonate a service account withroles/iam.workloadIdentityUser. - In the program, request the token for the allowed audience:
api-id:gcp.
Azure
- On a user-assigned managed identity, or on an app registration, add a federated credential for the organization:
az identity federated-credential create \
--name cadence-ci \
--identity-name <identity> \
--resource-group <resource-group> \
--issuer "https://api.melodium.tech" \
--subject "organization:<organization-id>" \
--audiences "api://AzureADTokenExchange"- Give the identity the roles the runs need.
- In the program, request the token for the audience Azure expects:
api-id:api://AzureADTokenExchange.
Vault and OpenBao
- Enable the
jwtauthentication method, trusting Cadence.CI, and a role for the runs of your organization (with OpenBao, usebaoinstead ofvault):
vault auth enable jwt
vault write auth/jwt/config \
oidc_discovery_url="https://api.melodium.tech" \
bound_issuer="https://api.melodium.tech"
vault write auth/jwt/role/cadence-ci - <<EOF
{
"role_type": "jwt",
"user_claim": "sub",
"bound_audiences": ["vault"],
"bound_subject": "organization:<organization-id>",
"token_policies": ["ci"],
"token_ttl": "10m"
}
EOF- In the program, the
vault::Vaultmodel logs in with the identity token, and secrets locatedvault:<path>#<field>are read with the policies of the role:
use sql::SqlPool
use std/ops/option::|wrap
use vault::Vault
treatment database(const password: Secret<string> = "vault:kv/data/app/db#password")
model vault: Vault(address="https://vault.example.com:8200", auth="jwt", role="cadence-ci", jwt="api-id:vault")
model pool: SqlPool(url="postgresql://app@db.example.com/app", password=|wrap<Secret<string>>(password))
{
}bound_claims narrows the role further, on the other claims, such as run_id or tags.
Going Further
- Secrets: storing secrets on Cadence.CI.
- Keys.
- Vault package in the Mélodium reference.