Skip to content
Open
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
151 changes: 151 additions & 0 deletions content/blog/native-oidc-token-exchange/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
title: "Native OIDC Token Exchange for Pulumi CLI"
date: 2025-12-11
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.

<!--more-->

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 new `pulumi login` command accepts OIDC tokens directly:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The new `pulumi login` command accepts OIDC tokens directly:
The `pulumi login` command now accepts OIDC tokens directly:

pulumi login is not a new command


```bash
pulumi login --oidc-token <token> --oidc-org <org-name>
```

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 <token> --oidc-org my-org --oidc-team platform-team

# Scope to a user
pulumi login --oidc-token <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).
Binary file added content/blog/native-oidc-token-exchange/meta.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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 <token> --oidc-org <org-name>
```

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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
`
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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=$(</var/run/secrets/pulumi/token)
echo "OIDC Token:"
echo $OIDC_GKE_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=$OIDC_GKE_TOKEN" \
https://api.pulumi.com/api/oauth/token | jq -r '.access_token')
echo "Access Token:"
echo $PULUMI_ACCESS_TOKEN
pulumi login --oidc-token $OIDC_GKE_TOKEN --oidc-org ${loginParams.org_name}
pulumi whoami
`
}
Expand Down Expand Up @@ -119,7 +105,7 @@ const job = new kubernetes.batch.v1.Job("runner", {
sources: [
{
serviceAccountToken: {
audience: "urn:pulumi:org:ORG_NAME",
audience: "urn:pulumi:org:MY_ORG_NAME",
expirationSeconds: 3600,
path: "token",
},
Expand Down
38 changes: 33 additions & 5 deletions content/docs/esc/cli/commands/esc_login.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,29 @@ to log in to a self-hosted Pulumi Cloud running at the api.pulumi.acmecorp.com d
For `https://` URLs, the CLI will speak REST to a Pulumi Cloud that manages state and concurrency control.
You can specify a default org to use when logging into the Pulumi Cloud backend or a self-hosted Pulumi Cloud.

### 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:

$ esc login --oidc-token <token> --oidc-org <org-name>

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 <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 <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 [<url>] [flags]
Expand All @@ -38,11 +61,16 @@ esc login [<url>] [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
Expand Down
23 changes: 23 additions & 0 deletions content/docs/iac/cli/commands/pulumi_login.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <token> --oidc-org <org-name>

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 <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 <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 [<url>] [flags]
Expand Down
Loading