Skip to content

Commit

Permalink
Merge pull request #1511 from pulumi/torian/aws-py-oidc-provider-pulu…
Browse files Browse the repository at this point in the history
…mi-cloud

Create Pulumi to AWS OIDC Configuration Example
  • Loading branch information
toriancrane committed Oct 19, 2023
2 parents fa96ac5 + ac0a236 commit 989c85e
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 0 deletions.
2 changes: 2 additions & 0 deletions aws-py-oidc-provider-pulumi-cloud/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.pyc
venv/
6 changes: 6 additions & 0 deletions aws-py-oidc-provider-pulumi-cloud/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name: oidc-test
runtime:
name: python
options:
virtualenv: venv
description: A Python Pulumi Program
96 changes: 96 additions & 0 deletions aws-py-oidc-provider-pulumi-cloud/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Provisioning an OIDC Provider in AWS for Pulumi Cloud

This example will create OIDC configuration between Pulumi Cloud and AWS, specifically demonstrating connectivity with [Pulumi ESC](https://www.pulumi.com/docs/pulumi-cloud/esc/). The program automates the process detailed in the AWS documentation for the following activities:

- [Obtaining the thumbprint for an OpenID Connect Identity Provider](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html)
- [Creating an OpenID Connect Identity Provider](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html)

## Prerequisites

* [Install Pulumi](https://www.pulumi.com/docs/get-started/install/)
* [Configure Pulumi to Use AWS](https://www.pulumi.com/docs/intro/cloud-providers/aws/setup/) (if your AWS CLI is configured, no further changes are required)
* Install Python 3.x

Make sure to deploy this example in an AWS account that does not already have a provider configured for Pulumi, otherwise the deployment will fail with the following error:

`creating IAM OIDC Provider: EntityAlreadyExists: Provider with url https://api.pulumi.com/oidc already exists.`

## Running the Example

Clone [the examples repo](https://github.com/pulumi/examples/tree/master/aws-py-oidc-provider) and navigate to the folder for this example.

```bash
git clone https://github.com/pulumi/examples.git
cd examples/aws-py-oidc-provider-pulumi-cloud
```

Next, to deploy the application and its infrastructure, follow these steps:

1. Create a new stack, which is an isolated deployment target for this example:

```bash
$ pulumi stack init dev
```

1. Set your desired AWS region:

```bash
pulumi config set aws:region us-east-1 # any valid AWS region will work
```

1. Install requirements.

```bash
python3 -m venv venv
venv/bin/pip install -r requirements.txt
```

1. Run `pulumi up -y`. Once the program completes, it will output a YAML template for you to use in the next step.

## Validating the OIDC Configuration

This next section will walk you through validating your OIDC configuration using [Pulumi ESC](https://www.pulumi.com/docs/pulumi-cloud/esc/).

Start by [creating a new Pulumi ESC environment](https://www.pulumi.com/docs/pulumi-cloud/esc/get-started/#create-an-environment). Then, copy the template definition from the output in the CLI and paste it into your environment. Save your environment file and run the `pulumi env open <your-pulumi-org>/<your-environment>` command in the CLI. You should see output similar to the following:

```bash
$ pulumi env open myOrg/myEnvironment
{
"aws": {
"login": {
"accessKeyId": "ASIA......",
"secretAccessKey": "PYP.....",
"sessionToken": "FwoGZ....."
}
}
}
```

You can configure more granular access control by adding the `sub` claim to the Provider role's trust policy conditions with the appropriate pattern. In the following example, the role may only be assumed by the specific Pulumi ESC environment that you designate.
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::616138583583:oidc-provider/api.pulumi.com/oidc"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"api.pulumi.com/oidc:aud": "<your-pulumi-org>",
"api.pulumi.com/oidc:sub": "pulumi:environments:org:<your-pulumi-org>:env:<your-environment-name>"
}
}
}
]
}
```
Once you are done, you can destroy all of the resources as well as the stack:
```bash
$ pulumi destroy
$ pulumi stack rm
```
96 changes: 96 additions & 0 deletions aws-py-oidc-provider-pulumi-cloud/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import pulumi
from pulumi_aws import iam
import requests
import subprocess
import OpenSSL
import json
import yaml

audience = pulumi.get_organization()
oidc_idp_url = 'https://api.pulumi.com/oidc'
base_url = 'api.pulumi.com/oidc'

# Obtain the OIDC IdP URL and form the configuration document URL
print("Forming configuration document URL...")
configuration_url = f'{oidc_idp_url}/.well-known/openid-configuration'

# Locate "jwks_uri" and extract the domain name
print("Extracting domain name from jwks_uri...")
response = requests.get(configuration_url)
jwks_uri = response.json().get('jwks_uri', '')
domain_name = jwks_uri.split('/')[2]

# Run OpenSSL command to get certificates
print("Retrieving OpenSSL certificates (this will take some time)...")
command = f'openssl s_client -servername {domain_name} -showcerts -connect {domain_name}:443'
result = subprocess.run(command, shell=True, capture_output=True, text=True)
certificates = result.stdout.split('-----END CERTIFICATE-----')

# Get the last certificate from the output
print("Retrieving last OpenSSL certificate...")
last_certificate = certificates[-2] + '-----END CERTIFICATE-----'

# Save the certificate to a file
print("Saving certificate to file...")
with open('certificate.crt', 'w') as file:
file.write(last_certificate)

# Get the thumbprint of the final certificate
print("Retrieving certificate thumbprint...")
x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, last_certificate)
thumbprint = (x509.digest('sha1').decode()).replace(":", "")

# Create an OIDC identity provider
print("Creating OIDC provider...")
oidc_provider = iam.OpenIdConnectProvider("oidcProvider",
client_id_lists=[audience],
thumbprint_lists=[thumbprint],
url=oidc_idp_url
)

# Create an IAM role with a trust policy that trusts the OIDC provider
print("Creating Provider IAM role...")
def create_assume_role_policy(args):
url, arn, audience = args
policy = {
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Federated": arn},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {"StringEquals": {f"{url}:aud": audience}}
}]
}
return json.dumps(policy)

oidc_role = iam.Role("oidcProviderRole",
assume_role_policy=pulumi.Output.all(oidc_provider.url, oidc_provider.arn, audience).apply(create_assume_role_policy)
)

print("OIDC configuration complete!")
print("Copy and paste the following template into your Pulumi ESC environment:")
print("--------")
def create_yaml_structure(role_arn):
return {
'values': {
'aws': {
'login': {
'fn::open::aws-login': {
'oidc': {
'duration': '1h',
'roleArn': role_arn,
'sessionName': 'pulumi-environments-session'
}
}
}
}
}
}

def print_yaml(role_arn):
yaml_structure = create_yaml_structure(role_arn)
yaml_string = yaml.dump(yaml_structure, sort_keys=False)
print(yaml_string)

# Use apply to wait for oidc_role.arn to be available, then print the YAML string
oidc_role.arn.apply(print_yaml)
12 changes: 12 additions & 0 deletions aws-py-oidc-provider-pulumi-cloud/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
pulumi>=3.0.0,<4.0.0
pulumi-aws>=6.0.2,<7.0.0
certifi==2023.7.22
cffi==1.15.1
charset-normalizer==3.3.0
cryptography==41.0.4
idna==3.4
pycparser==2.21
pyOpenSSL==23.2.0
requests==2.31.0
urllib3==1.26.7
PyYAML

0 comments on commit 989c85e

Please sign in to comment.