diff --git a/content/blog/native-oidc-token-exchange/index.md b/content/blog/native-oidc-token-exchange/index.md new file mode 100644 index 000000000000..c1b94a3d0d4f --- /dev/null +++ b/content/blog/native-oidc-token-exchange/index.md @@ -0,0 +1,151 @@ +--- +title: "Native OIDC Token Exchange for Pulumi CLI" +date: 2025-12-16 +draft: false +meta_desc: The Pulumi CLI now supports native OIDC token exchange, enabling secure authentication without long-lived credentials in automated workflows. +meta_image: meta.png +authors: + - boris-schlosser +tags: + - features + - security + - oidc + - authentication + - ci-cd +--- + +Managing credentials in CI/CD pipelines has always involved tradeoffs. Long-lived access tokens are convenient but +create security risks when they leak or fall into the wrong hands. Short-lived credentials are more secure but require +additional tooling to obtain and manage. Today, we're eliminating this tradeoff with native OIDC token exchange support +in the Pulumi CLI. + + + +The Pulumi CLI now includes built-in support for exchanging OIDC tokens from your identity provider for short-lived +Pulumi Cloud access tokens. This means you can authenticate to Pulumi Cloud directly from CI/CD environments like GitHub +Actions, GitLab CI, or Kubernetes without storing any long-lived Pulumi credentials as secrets. + +## Why OIDC token exchange matters + +Most CI/CD workflows authenticate to Pulumi Cloud using personal access tokens or organization tokens stored as secrets. +While this approach works, it comes with significant security concerns: + +- **Credential exposure**: If a token is accidentally committed to a repository or logged in CI output, attackers gain + long-term access to your infrastructure +- **Rotation complexity**: Rotating tokens requires updating secrets across multiple CI/CD systems +- **Over-privileged access**: Tokens often have broader permissions than needed for specific workflows +- **Audit trail gaps**: Difficult to trace which workflow run used which credentials + +With OIDC token exchange, you eliminate these risks by leveraging short-lived tokens that your CI/CD platform or +identity provider already issues. No long-lived secrets to manage, rotate, or secure. + +## How it works + +The `pulumi login` command now accepts OIDC tokens directly: + +```bash +pulumi login --oidc-token --oidc-org +``` + +The CLI exchanges your OIDC token for a short-lived Pulumi Cloud access token, which is then used for all subsequent +operations. Tokens expire after 2 hours by default, though you can customize this with the `--oidc-expiration` flag. + +You can scope tokens to specific teams or users: + +```bash +# Scope to a team +pulumi login --oidc-token --oidc-org my-org --oidc-team platform-team + +# Scope to a user +pulumi login --oidc-token --oidc-org my-org --oidc-user alice +``` + +The `--oidc-token` flag accepts either a raw token string or a file path prefixed with `file://`, making it easy to +integrate with various token delivery mechanisms. + +## Example: Kubernetes (EKS) + +For workloads running in Kubernetes, you can use service account tokens and exchange them for Pulumi access tokens. The +following example uses a Pulumi program to define a Kubernetes Job resource. + +```typescript +import * as kubernetes from "@pulumi/kubernetes"; + +const script = new kubernetes.core.v1.ConfigMap("script", { + data: { + "entrypoint.sh": `#!/bin/bash +EKS_ID_TOKEN=$(cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token) +pulumi login --oidc-token $EKS_ID_TOKEN --oidc-org MY_ORG_NAME +pulumi whoami +` + } +}); + +const job = new kubernetes.batch.v1.Job("runner", { + metadata: {}, + spec: { + template: { + spec: { + serviceAccountName: "pulumi-service-account", + containers: [{ + name: "runner", + image: "pulumi/pulumi:latest", + command: ["/bin/entrypoint.sh"], + volumeMounts: [ + { + name: "script", + mountPath: "/bin/entrypoint.sh", + readOnly: true, + subPath: "entrypoint.sh", + }, + ], + }], + restartPolicy: "Never", + volumes: [ + { + name: "script", + configMap: { + defaultMode: 0o700, + name: script.metadata.name, + }, + }, + ], + }, + }, + backoffLimit: 0, + }, +}); + +export const jobName = job.metadata.name; +``` + +This approach works with any Kubernetes cluster that supports service account token projection, including EKS, GKE, and +AKS. The example uses EKS's default token location at `/var/run/secrets/eks.amazonaws.com/serviceaccount/token`, but you +can adapt the token path for other Kubernetes distributions. + +## Prerequisites + +Before using OIDC token exchange with the Pulumi CLI, you need to: + +1. [Register your OIDC provider as a trusted issuer in your Pulumi organization settings](/docs/administration/access-identity/oidc-client/#configuring-trust-relationships) +1. Configure authorization policies that specify which tokens can be exchanged and what permissions they receive +1. Ensure your CI/CD system or identity provider is configured to issue OIDC tokens with the appropriate audience claim + +## Get started + +Native OIDC token exchange is available now in the latest version of the Pulumi CLI. To get started: + +1. Update to the latest Pulumi CLI version +1. Configure your OIDC provider and authorization policies in [Pulumi Cloud](https://app.pulumi.com/) +1. Update your CI/CD workflows to use `pulumi login --oidc-token` + +For complete documentation, including setup guides for specific identity providers, see: + +- [`pulumi login` command reference](/docs/iac/cli/commands/pulumi_login/#oidc-token-exchange) +- [OIDC client integration guide](/docs/administration/access-identity/oidc-client/) +- [GitHub OIDC setup](/docs/administration/access-identity/oidc-client/github/) +- [GKE OIDC setup](/docs/administration/access-identity/oidc-client/kubernetes-gke/) +- [EKS OIDC setup](/docs/administration/access-identity/oidc-client/kubernetes-eks/) + +We're excited to see how this feature helps you build more secure infrastructure automation workflows. If you have +questions or feedback, join us in the [Pulumi Community Slack](https://slack.pulumi.com). diff --git a/content/blog/native-oidc-token-exchange/meta.png b/content/blog/native-oidc-token-exchange/meta.png new file mode 100644 index 000000000000..cf2ee6756b10 Binary files /dev/null and b/content/blog/native-oidc-token-exchange/meta.png differ diff --git a/content/docs/administration/access-identity/oidc-client/_index.md b/content/docs/administration/access-identity/oidc-client/_index.md index 03fd3fe52b22..e46533336552 100644 --- a/content/docs/administration/access-identity/oidc-client/_index.md +++ b/content/docs/administration/access-identity/oidc-client/_index.md @@ -119,7 +119,19 @@ In this example, it can be configured as `runner-*` to match any pod name with t ## Exchanging OIDC tokens -To exchange OIDC tokens for Pulumi access tokens, the oauth2 token endpoint and token exchange grant type are used. +### Using the Pulumi CLI + +The Pulumi CLI provides native support for OIDC token exchange via the `pulumi login` command. This is the recommended approach for most use cases: + +```bash +pulumi login --oidc-token --oidc-org +``` + +The `--oidc-token` flag accepts either a raw token string or a file path prefixed with `file://`. You can also specify optional parameters like `--oidc-team`, `--oidc-user`, or `--oidc-expiration`. For more details, see the [`pulumi login` documentation](/docs/iac/cli/commands/pulumi_login/#oidc-token-exchange). + +### Using the REST API directly + +For advanced scenarios where you need direct control over the token exchange process, you can use the oauth2 token endpoint and token exchange grant type directly. This endpoint supports both `application/json` and `application/x-www-form-urlencoded` content types are supported. diff --git a/content/docs/administration/access-identity/oidc-client/github.md b/content/docs/administration/access-identity/oidc-client/github.md index de787dee11dc..86bcdc08d556 100644 --- a/content/docs/administration/access-identity/oidc-client/github.md +++ b/content/docs/administration/access-identity/oidc-client/github.md @@ -18,7 +18,7 @@ aliases: This document outlines the steps required to configure Pulumi to accept Github id_tokens to be exchanged by Organization access tokens. {{< notes type="info" >}} -This guide demonstrates using `organization` tokens. Depending on your [Pulumi edition](/docs/pulumi-cloud/access-management/oidc-client/#token-types-by-edition), you may also use `personal` or `team` tokens by adjusting the token type in the authorization policies and the `requested-token-type` parameter. +This guide demonstrates using `organization` tokens. Depending on your [Pulumi edition](/docs/administration/access-identity/oidc-client/#token-types-by-edition), you may also use `personal` or `team` tokens by adjusting the token type in the authorization policies and the `requested-token-type` parameter. {{< /notes >}} ## Prerequisites diff --git a/content/docs/administration/access-identity/oidc-client/kubernetes-eks.md b/content/docs/administration/access-identity/oidc-client/kubernetes-eks.md index 1955706e1da9..c0ca76b74f7a 100644 --- a/content/docs/administration/access-identity/oidc-client/kubernetes-eks.md +++ b/content/docs/administration/access-identity/oidc-client/kubernetes-eks.md @@ -22,7 +22,7 @@ aliases: This document outlines the steps required to configure Pulumi to accept Elastic Kubernetes Service (EKS) id_tokens to be exchanged for a personal access token. With this configuration, Kubernetes pods authenticate to Pulumi Cloud using OIDC tokens issued by EKS. {{< notes type="info" >}} -This guide demonstrates using `personal` tokens. Depending on your [Pulumi edition](/docs/pulumi-cloud/access-management/oidc-client/#token-types-by-edition), you may also use `organization` or `team` tokens by adjusting the token type in the authorization policies and the `requested-token-type` parameter. +This guide demonstrates using `personal` tokens. Depending on your [Pulumi edition](/docs/administration/access-identity/oidc-client/#token-types-by-edition), you may also use `organization` or `team` tokens by adjusting the token type in the authorization policies and the `pulumi login` parameters. {{< /notes >}} ## Prerequisites @@ -148,35 +148,20 @@ For example to reference the pod name, you would use `"kubernetes.io".pod.name` import * as kubernetes from "@pulumi/kubernetes"; -const tokenParams = { - "audience": "urn:pulumi:org:ORG_NAME", - "token_type": "urn:pulumi:token-type:access_token:personal", - "expiration": 2 * 60 * 60, - "scope": "user:USER_LOGIN" +const loginParams = { + "org_name": "MY_ORG_NAME", + "user_login": "MY_USER_LOGIN" } const script = new kubernetes.core.v1.ConfigMap("script", { data: { "entrypoint.sh": `#!/bin/bash -apt -qq install -y jq - # This is the location of the EKS id token EKS_ID_TOKEN=$(cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token) echo "OIDC Token:" echo $EKS_ID_TOKEN -export PULUMI_ACCESS_TOKEN=$(curl -sS -X POST \ - -H 'Content-Type: application/x-www-form-urlencoded' \ - -d 'audience=${tokenParams.audience}' \ - -d 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \ - -d 'subject_token_type=urn:ietf:params:oauth:token-type:id_token' \ - -d 'requested_token_type=${tokenParams.token_type}' \ - -d 'expiration=${tokenParams.expiration}' \ - -d 'scope=${tokenParams.scope}' \ - -d "subject_token=$EKS_ID_TOKEN" \ - https://api.pulumi.com/api/oauth/token | jq -r '.access_token') -echo "Access Token:" -echo $PULUMI_ACCESS_TOKEN +pulumi login --oidc-token $EKS_ID_TOKEN --oidc-org ${loginParams.org_name} --oidc-user ${loginParams.user_login} pulumi whoami ` } diff --git a/content/docs/administration/access-identity/oidc-client/kubernetes-gke.md b/content/docs/administration/access-identity/oidc-client/kubernetes-gke.md index 8b9a040a36c4..aa328623817a 100644 --- a/content/docs/administration/access-identity/oidc-client/kubernetes-gke.md +++ b/content/docs/administration/access-identity/oidc-client/kubernetes-gke.md @@ -20,7 +20,7 @@ This document outlines the steps required to configure Pulumi to accept Google K See ["Bound Tokens"](https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-bound-service-account-tokens) for more background. {{< notes type="info" >}} -This guide demonstrates using `organization` tokens. Depending on your [Pulumi edition](/docs/pulumi-cloud/access-management/oidc-client/#token-types-by-edition), you may also use `personal` or `team` tokens by adjusting the token type in the authorization policies and the `requested-token-type` parameter. +This guide demonstrates using `organization` tokens. Depending on your [Pulumi edition](/docs/administration/access-identity/oidc-client/#token-types-by-edition), you may also use `personal` or `team` tokens by adjusting the token type in the authorization policies and the `pulumi login` parameters. {{< /notes >}} ## Prerequisites @@ -58,31 +58,17 @@ Please note that this guide provides step-by-step instructions based on the offi import * as kubernetes from "@pulumi/kubernetes"; -const tokenParams = { - "audience": "urn:pulumi:org:ORG_NAME", - "token_type": "urn:pulumi:token-type:access_token:organization", - "expiration": 2 * 60 * 60, +const loginParams = { + "org_name": "MY_ORG_NAME", } const script = new kubernetes.core.v1.ConfigMap("script", { data: { "entrypoint.sh": `#!/bin/bash -apt -qq install -y jq OIDC_GKE_TOKEN=$( --oidc-org + +The `--oidc-token` flag accepts either a raw token string or a file path prefixed with `file://`: + + $ esc login --oidc-token file:///path/to/token.txt --oidc-org my-org + +By default, the exchanged token is scoped to your organization. You can optionally scope it to a specific team or user: + + $ esc login --oidc-token --oidc-org my-org --oidc-team my-team + +The exchanged access token expires after 2 hours by default. You can customize the expiration using the `--oidc-expiration` flag: + + $ esc login --oidc-token --oidc-org my-org --oidc-expiration 4h + +This approach is particularly useful in environments like GitHub Actions, GitLab CI, or any CI/CD system that provides OIDC tokens, as it eliminates the need to store long-lived Pulumi access tokens as secrets. + +## Command ``` esc login [] [flags] @@ -38,11 +61,16 @@ esc login [] [flags] ## Options ``` - -c, --cloud-url string A cloud URL to log in to - --default-org string A default org to associate with the login. - -h, --help help for login - --insecure Allow insecure server connections when using SSL - --shared Log in to the account in use by the pulumi CLI + -c, --cloud-url string A cloud URL to log in to + --default-org string A default org to associate with the login. + -h, --help help for login + --insecure Allow insecure server connections when using SSL + --shared Log in to the account in use by the pulumi CLI + --oidc-expiration string The expiration for the cloud backend access token in duration format (e.g. '15m', '24h') + --oidc-org string The organization to use for OIDC token exchange audience + --oidc-team string The team when exchanging for a team token + --oidc-token string An OIDC token to exchange for a cloud backend access token. Can be either a raw token or a file path prefixed with 'file://'. + --oidc-user string The user when exchanging for a personal token ``` ## SEE ALSO diff --git a/content/docs/iac/cli/commands/pulumi_login.md b/content/docs/iac/cli/commands/pulumi_login.md index 77d94411d089..4e410131702f 100644 --- a/content/docs/iac/cli/commands/pulumi_login.md +++ b/content/docs/iac/cli/commands/pulumi_login.md @@ -60,6 +60,29 @@ PostgreSQL: $ pulumi login postgres://username:password@hostname:5432/database +### OIDC token exchange + +For secure authentication in CI/CD pipelines and automated workflows, you can use OIDC token exchange to log in without managing long-lived credentials. This feature exchanges a short-lived OIDC token from your identity provider for a Pulumi Cloud access token. + +To log in using OIDC token exchange, provide an OIDC token and your organization name: + + $ pulumi login --oidc-token --oidc-org + +The `--oidc-token` flag accepts either a raw token string or a file path prefixed with `file://`: + + $ pulumi login --oidc-token file:///path/to/token.txt --oidc-org my-org + +By default, the exchanged token is scoped to your organization. You can optionally scope it to a specific team or user: + + $ pulumi login --oidc-token --oidc-org my-org --oidc-team my-team + +The exchanged access token expires after 2 hours by default. You can customize the expiration using the `--oidc-expiration` flag: + + $ pulumi login --oidc-token --oidc-org my-org --oidc-expiration 4h + +This approach is particularly useful in environments like GitHub Actions, GitLab CI, or any CI/CD system that provides OIDC tokens, as it eliminates the need to store long-lived Pulumi access tokens as secrets. + +## Command ``` pulumi login [] [flags]