├── .github ├── script │ ├── STEP │ ├── 2-setup.sh │ ├── 3-setup.sh │ ├── 4-setup.sh │ └── 1-setup.sh ├── steps │ ├── -step.txt │ ├── 0-welcome.md │ ├── X-finish.md │ ├── 1-oidc-introduction.md │ ├── 3-branches.md │ ├── 4-environments.md │ └── 2-prs.md ├── dependabot.yml └── workflows │ ├── 0-start.yml │ ├── 1-verify-step.yml │ ├── 3-verify-step.yml │ ├── 4-verify-step.yml │ ├── 2-verify-step.yml │ ├── 1-oidc-hello-world.yml │ ├── 2-pull-request.yml │ ├── 3-main-branch.yml │ └── 4-environment.yml ├── .pre-commit-config.yaml ├── .gitignore ├── README.md └── LICENSE /.github/script/STEP: -------------------------------------------------------------------------------- 1 | 0 2 | -------------------------------------------------------------------------------- /.github/steps/-step.txt: -------------------------------------------------------------------------------- 1 | 0 -------------------------------------------------------------------------------- /.github/steps/0-welcome.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | groups: 8 | all: 9 | patterns: 10 | - "*" 11 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.4.0 5 | hooks: 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - id: check-executables-have-shebangs 9 | - id: check-merge-conflict 10 | - id: mixed-line-ending 11 | 12 | - repo: https://github.com/rhysd/actionlint 13 | rev: v1.6.23 14 | hooks: 15 | - id: actionlint-docker 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | *.sql 27 | *.sqlite 28 | 29 | # OS generated files # 30 | ###################### 31 | .DS_Store 32 | .DS_Store? 33 | ._* 34 | .Spotlight-V100 35 | .Trashes 36 | ehthumbs.db 37 | Thumbs.db 38 | 39 | # IDEs 40 | .idea 41 | .vscode 42 | -------------------------------------------------------------------------------- /.github/script/2-setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -eu 4 | 5 | # Install Vault binary 6 | wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg 7 | echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list 8 | sudo apt update && sudo apt install vault 9 | 10 | # Initialize Vault for this scenario 11 | vault login vaultiscool 12 | vault auth enable -path=gha jwt 13 | vault write auth/gha/config \ 14 | bound_issuer="https://token.actions.githubusercontent.com" \ 15 | oidc_discovery_url="https://token.actions.githubusercontent.com" 16 | 17 | # Create a secret 18 | vault kv put secret/development access_token=abc123 19 | # Add a policy 20 | vault policy write pr-policy - << EOF 21 | path "secret/data/development" { 22 | capabilities = ["read"] 23 | } 24 | EOF 25 | -------------------------------------------------------------------------------- /.github/script/3-setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -eu 4 | 5 | # Install Vault binary 6 | wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg 7 | echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list 8 | sudo apt update && sudo apt install vault 9 | 10 | # Initialize Vault for this scenario 11 | vault login vaultiscool 12 | vault auth enable -path=gha jwt 13 | vault write auth/gha/config \ 14 | bound_issuer="https://token.actions.githubusercontent.com" \ 15 | oidc_discovery_url="https://token.actions.githubusercontent.com" 16 | 17 | # Create a secret 18 | vault kv put secret/production access_token=xyz456 19 | # Add a policy 20 | vault policy write main-policy - << EOF 21 | path "secret/data/production" { 22 | capabilities = ["read"] 23 | } 24 | EOF 25 | -------------------------------------------------------------------------------- /.github/script/4-setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -eu 4 | 5 | # Install Vault binary 6 | wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg 7 | echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list 8 | sudo apt update && sudo apt install vault 9 | 10 | # Initialize Vault for this scenario 11 | vault login vaultiscool 12 | vault auth enable -path=gha jwt 13 | vault write auth/gha/config \ 14 | bound_issuer="https://token.actions.githubusercontent.com" \ 15 | oidc_discovery_url="https://token.actions.githubusercontent.com" 16 | 17 | # Create a secret 18 | vault kv put secret/staging access_token=abc123 19 | vault kv put secret/production access_token=xyz456 20 | # Add OIDC policies 21 | vault policy write staging-policy - << EOF 22 | path "secret/data/staging" { 23 | capabilities = ["read"] 24 | } 25 | EOF 26 | vault policy write prod-policy - << EOF 27 | path "secret/data/production" { 28 | capabilities = ["read"] 29 | } 30 | EOF 31 | -------------------------------------------------------------------------------- /.github/script/1-setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -eu 4 | 5 | # Install Vault binary 6 | wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg 7 | echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list 8 | sudo apt update && sudo apt install vault 9 | 10 | # Initialize Vault for this scenario 11 | vault login vaultiscool 12 | vault auth enable -path=gha jwt 13 | vault write auth/gha/config \ 14 | bound_issuer="https://token.actions.githubusercontent.com" \ 15 | oidc_discovery_url="https://token.actions.githubusercontent.com" 16 | 17 | # Create a secret 18 | vault kv put secret/foobar hello=world 19 | # Add OIDC role and policy 20 | vault policy write hello-policy - << EOF 21 | path "secret/data/foobar" { 22 | capabilities = ["read"] 23 | } 24 | EOF 25 | # This grants ANYONE on github.com the ability to authenticate to your Vault server! 26 | # !!! DO NOT USE THIS IN REAL LIFE !!! 27 | # Every other workflow configuration in this tutorial is real-world viable, but this 28 | # is configured solely to allow attendees of this course to authenticate from their 29 | # clone of this repo - enable a quick win in the first exercise of the course. 30 | vault write auth/gha/role/hello-world - << EOF 31 | { 32 | "role_type": "jwt", 33 | "user_claim": "actor", 34 | "bound_claims": { 35 | "iss": "https://token.actions.githubusercontent.com" 36 | }, 37 | "policies": ["hello-policy"], 38 | "ttl": "60s" 39 | } 40 | EOF 41 | -------------------------------------------------------------------------------- /.github/steps/X-finish.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ## Finish 7 | 8 | _Congratulations friend, you've completed this course! :1st_place_medal:_ 9 | 10 | Here's a recap of all the tasks you've accomplished in your repository: 11 | - Configure Vault to accept GitHub OIDC authentication requests 12 | - Customize the `bound_claims` of a Vault role to provide fine-grained access control across workflows 13 | - Create a workflow that can only retrieve secrets when triggered by a pull request 14 | - Create a workflow that can only retrieve secrets when triggered by a push to the `main` branch 15 | - Create jobs in a workflow that can only retrieve secrets when assigned to a specific GitHub Environment, and can't access each other's secrets 16 | 17 | Remember that configuring Vault roles should typically happen separately from consuming secrets, so you'll likely want to create a separate workflow that creates Vault roles. 18 | However, for the sake of this course, we've configured Vault roles in the same workflows that consumed secrets. 19 | 20 | ### What's next? 21 | 22 | - We'd love to hear what you thought of this course [in our discussion board](https://github.com/artis3n/course-vault-github-oidc/discussions). 23 | - You can combine multiple claims in a single Vault role to provide even more fine-grained access control! 24 | For example, learn how to combine `sub` and `job_workflow_ref` to [provide secrets for reusable workflows](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/using-openid-connect-with-reusable-workflows). 25 | - Read [this comprehensive article](https://www.digitalocean.com/blog/fine-grained-rbac-for-github-action-workflows-hashicorp-vault) to learn how DigitalOcean employs this GitHub OIDC pattern for streamlined secrets management. 26 | - Use this [Terraform module](https://github.com/digitalocean/terraform-vault-github-oidc) from DigitalOcean to help manage your GitHub OIDC Vault role configurations. 27 | -------------------------------------------------------------------------------- /.github/workflows/0-start.yml: -------------------------------------------------------------------------------- 1 | name: Step 0, Start 2 | 3 | # This step triggers after the learner creates a new repository from the template 4 | # This step sets STEP to 1 5 | # This step closes
and opens
6 | 7 | # This will run every time we create push a commit to `main` 8 | # Reference https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows 9 | on: 10 | workflow_dispatch: 11 | push: 12 | branches: 13 | - main 14 | 15 | # Reference https://docs.github.com/en/actions/security-guides/automatic-token-authentication 16 | permissions: 17 | # Need `contents: read` to checkout the repository 18 | # Need `contents: write` to update the step metadata 19 | contents: write 20 | 21 | jobs: 22 | get_current_step: 23 | name: Check current step number 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | - id: get_step 29 | run: | 30 | echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT 31 | outputs: 32 | current_step: "${{ steps.get_step.outputs.current_step }}" 33 | 34 | on_start: 35 | name: On start 36 | needs: get_current_step 37 | 38 | # We will only run this action when: 39 | # 1. This repository isn't the template repository 40 | # 2. The STEP is currently 0 41 | # Reference https://docs.github.com/en/actions/learn-github-actions/contexts 42 | # Reference https://docs.github.com/en/actions/learn-github-actions/expressions 43 | if: >- 44 | ${{ !github.event.repository.is_template 45 | && needs.get_current_step.outputs.current_step == 0 }} 46 | 47 | # We'll run Ubuntu for performance instead of Mac or Windows 48 | runs-on: ubuntu-latest 49 | 50 | steps: 51 | # We'll need to check out the repository so that we can edit the README 52 | - name: Checkout 53 | uses: actions/checkout@v4 54 | with: 55 | fetch-depth: 0 # Let's get all the branches 56 | 57 | # In README.md, switch step 0 for step 1. 58 | - name: Update to step 1 59 | uses: skills/action-update-step@v2 60 | with: 61 | token: ${{ secrets.GITHUB_TOKEN }} 62 | from_step: 0 63 | to_step: 1 64 | -------------------------------------------------------------------------------- /.github/workflows/1-verify-step.yml: -------------------------------------------------------------------------------- 1 | name: Step 1, Verify 2 | 3 | # This step triggers after a participant manually runs the "Step 1, OIDC Hello World" workflow 4 | # This step sets STEP to 2 5 | # This step closes
and opens
6 | 7 | # This will run every time we invoke the "Step 1, OIDC Hello World" workflow 8 | # Reference https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows 9 | on: 10 | workflow_run: 11 | workflows: ["Step 1, OIDC Hello World"] 12 | types: 13 | - completed 14 | 15 | # Reference https://docs.github.com/en/actions/security-guides/automatic-token-authentication 16 | permissions: 17 | # Need `contents: read` to checkout the repository 18 | # Need `contents: write` to update the step metadata 19 | contents: write 20 | 21 | jobs: 22 | get_current_step: 23 | name: Check current step number 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | - id: get_step 29 | run: | 30 | echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT 31 | outputs: 32 | current_step: ${{ steps.get_step.outputs.current_step }} 33 | 34 | move_to_next_step: 35 | name: Move to Step 2 36 | needs: get_current_step 37 | 38 | # We will only run this action when: 39 | # 1. This repository isn't the template repository 40 | # 2. The STEP is currently 1 41 | # 3. The "Step 1, OIDC Hello World" workflow completes successfully 42 | # Reference: https://docs.github.com/en/actions/learn-github-actions/contexts 43 | # Reference: https://docs.github.com/en/actions/learn-github-actions/expressions 44 | if: >- 45 | ${{ !github.event.repository.is_template 46 | && needs.get_current_step.outputs.current_step == 1 47 | && github.event.workflow_run.conclusion == 'success' 48 | }} 49 | 50 | # We'll run Ubuntu for performance instead of Mac or Windows 51 | runs-on: ubuntu-latest 52 | 53 | steps: 54 | # We'll need to check out the repository so that we can edit the README 55 | - name: Checkout 56 | uses: actions/checkout@v4 57 | with: 58 | fetch-depth: 0 # Let's get all the branches 59 | 60 | # Update README to close
and open
61 | # and set STEP to '2' 62 | - name: Update to step 2 63 | uses: skills/action-update-step@v2 64 | with: 65 | token: ${{ secrets.GITHUB_TOKEN }} 66 | from_step: 1 67 | to_step: 2 68 | -------------------------------------------------------------------------------- /.github/workflows/3-verify-step.yml: -------------------------------------------------------------------------------- 1 | name: Step 3, Verify 2 | 3 | # This step triggers after a participant pushes code to the `main` branch. 4 | # This step sets STEP to 4 5 | # This step closes
and opens
6 | 7 | # This will run every time we invoke the "Step 3, Fine-grained permissions - branches" workflow 8 | # Reference https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows 9 | on: 10 | workflow_run: 11 | workflows: [ "Step 3, Fine-grained permissions - branches" ] 12 | types: 13 | - completed 14 | 15 | # Reference https://docs.github.com/en/actions/security-guides/automatic-token-authentication 16 | permissions: 17 | # Need `contents: read` to checkout the repository 18 | # Need `contents: write` to update the step metadata 19 | contents: write 20 | 21 | jobs: 22 | get_current_step: 23 | name: Check current step number 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | - id: get_step 29 | run: | 30 | echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT 31 | outputs: 32 | current_step: ${{ steps.get_step.outputs.current_step }} 33 | 34 | move_to_next_step: 35 | name: Move to Step 4 36 | needs: get_current_step 37 | 38 | # We will only run this action when: 39 | # 1. This repository isn't the template repository 40 | # 2. The STEP is currently 3 41 | # 3. The "Step 3, Fine-grained permissions - branches" workflow completes successfully 42 | # Reference https://docs.github.com/en/actions/learn-github-actions/contexts 43 | # Reference https://docs.github.com/en/actions/learn-github-actions/expressions 44 | if: >- 45 | ${{ !github.event.repository.is_template 46 | && needs.get_current_step.outputs.current_step == 3 47 | && github.event.workflow_run.conclusion == 'success' 48 | }} 49 | 50 | # We'll run Ubuntu for performance instead of Mac or Windows 51 | runs-on: ubuntu-latest 52 | 53 | steps: 54 | # We'll need to check out the repository so that we can edit the README 55 | - name: Checkout 56 | uses: actions/checkout@v4 57 | with: 58 | fetch-depth: 0 # Let's get all the branches 59 | 60 | # Update README to close
and open
61 | # and set STEP to '4' 62 | - name: Update to step 4 63 | uses: skills/action-update-step@v2 64 | with: 65 | token: ${{ secrets.GITHUB_TOKEN }} 66 | from_step: 3 67 | to_step: 4 68 | -------------------------------------------------------------------------------- /.github/workflows/4-verify-step.yml: -------------------------------------------------------------------------------- 1 | name: Step 4, Verify 2 | 3 | # This step triggers after a workflow with Environments is triggered on the repository. 4 | # This step sets STEP to 5 5 | # This step closes
and opens
6 | 7 | # This will run every time we invoke the "Step 4, Fine-grained permissions - environments" workflow 8 | # Reference https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows 9 | on: 10 | workflow_run: 11 | workflows: [ "Step 4, Fine-grained permissions - environments" ] 12 | types: 13 | - completed 14 | 15 | # Reference https://docs.github.com/en/actions/security-guides/automatic-token-authentication 16 | permissions: 17 | # Need `contents: read` to checkout the repository 18 | # Need `contents: write` to update the step metadata 19 | contents: write 20 | 21 | jobs: 22 | get_current_step: 23 | name: Check current step number 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | - id: get_step 29 | run: | 30 | echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT 31 | outputs: 32 | current_step: ${{ steps.get_step.outputs.current_step }} 33 | 34 | move_to_next_step: 35 | name: Move to Step 5 36 | needs: get_current_step 37 | 38 | # We will only run this action when: 39 | # 1. This repository isn't the template repository 40 | # 2. The STEP is currently 4 41 | # 3. The "Step 4, Fine-grained permissions - environments" workflow completes successfully 42 | # Reference https://docs.github.com/en/actions/learn-github-actions/contexts 43 | # Reference https://docs.github.com/en/actions/learn-github-actions/expressions 44 | if: >- 45 | ${{ !github.event.repository.is_template 46 | && needs.get_current_step.outputs.current_step == 4 47 | && github.event.workflow_run.conclusion == 'success' 48 | }} 49 | 50 | # We'll run Ubuntu for performance instead of Mac or Windows 51 | runs-on: ubuntu-latest 52 | 53 | steps: 54 | # We'll need to check out the repository so that we can edit the README 55 | - name: Checkout 56 | uses: actions/checkout@v4 57 | with: 58 | fetch-depth: 0 # Let's get all the branches 59 | 60 | # Update README to close
and open
61 | # and set STEP to '5' 62 | - name: Update to step 5 63 | uses: skills/action-update-step@v2 64 | with: 65 | token: ${{ secrets.GITHUB_TOKEN }} 66 | from_step: 4 67 | to_step: X 68 | -------------------------------------------------------------------------------- /.github/workflows/2-verify-step.yml: -------------------------------------------------------------------------------- 1 | name: Step 2, Verify 2 | 3 | # This step triggers after a participant opens or updates a pull request with a properly configured OIDC role. 4 | # This step sets STEP to 3 5 | # This step closes
and opens
6 | 7 | # This will run every time we invoke the "Step 2, Fine-grained permissions - pull requests" workflow 8 | # Reference https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows 9 | on: 10 | workflow_run: 11 | workflows: [ "Step 2, Fine-grained permissions - pull requests" ] 12 | types: 13 | - completed 14 | 15 | # Reference https://docs.github.com/en/actions/security-guides/automatic-token-authentication 16 | permissions: 17 | # Need `contents: read` to checkout the repository 18 | # Need `contents: write` to update the step metadata 19 | contents: write 20 | 21 | jobs: 22 | get_current_step: 23 | name: Check current step number 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | - id: get_step 29 | run: | 30 | echo "current_step=$(cat ./.github/steps/-step.txt)" >> $GITHUB_OUTPUT 31 | outputs: 32 | current_step: ${{ steps.get_step.outputs.current_step }} 33 | 34 | move_to_next_step: 35 | name: Move to Step 3 36 | needs: get_current_step 37 | 38 | # We will only run this action when: 39 | # 1. This repository isn't the template repository 40 | # 2. The STEP is currently 2 41 | # 3. The "Step 2, Fine-grained permissions - pull requests" workflow completes successfully 42 | # Reference https://docs.github.com/en/actions/learn-github-actions/contexts 43 | # Reference https://docs.github.com/en/actions/learn-github-actions/expressions 44 | if: >- 45 | ${{ !github.event.repository.is_template 46 | && needs.get_current_step.outputs.current_step == 2 47 | && github.event.workflow_run.conclusion == 'success' 48 | }} 49 | 50 | # We'll run Ubuntu for performance instead of Mac or Windows 51 | runs-on: ubuntu-latest 52 | 53 | steps: 54 | # We'll need to check out the repository so that we can edit the README 55 | - name: Checkout 56 | uses: actions/checkout@v4 57 | with: 58 | fetch-depth: 0 # Let's get all the branches 59 | 60 | # Update README to close
and open
61 | # and set STEP to '3' 62 | - name: Update to step 3 63 | uses: skills/action-update-step@v2 64 | with: 65 | token: ${{ secrets.GITHUB_TOKEN }} 66 | from_step: 2 67 | to_step: 3 68 | -------------------------------------------------------------------------------- /.github/steps/1-oidc-introduction.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | ## Step 1: Introduction to OIDC 11 | 12 | _Welcome to "Getting secrets from HashiCorp Vault with GitHub OIDC in Action workflows!" :wave:_ 13 | 14 | Leveraging GitHub OIDC to Vault enables secure, short-lived, passwordless authentication for GitHub Actions workflows. 15 | This course will teach you how to configure a GitHub Actions workflow to retrieve secrets from Vault using OIDC. 16 | You will learn how to use GitHub's JWT claims to create Vault roles with fine-grained access to secrets. 17 | You will also learn how to use GitHub Environments to segregate access to secrets between jobs. 18 | 19 | **What is _OpenID Connect (OIDC)_?**: 20 | OpenID Connect is an authentication protocol built on top of OAuth 2.0 (which is an authorization protocol). 21 | OIDC is a way to authenticate a user or service to a third-party identity provider (IDP) using a JSON Web Token (JWT). 22 | Instead of managing login credentials, the token exposes parameters (known as `claims`) that can be used to authorize access to resources. 23 | 24 | **How does a workflow sign in to Vault with OIDC?**: 25 | GitHub authenticates directly to Vault by presenting a JWT with certain claims. 26 | Vault roles are pre-configured to bind to a combination of claims specified by the token. 27 | When a workflow presents a token to Vault, Vault verifies the token's signature and claims. 28 | If a role configuration matches the presented claims, Vault returns an auth token to the workflow. 29 | 30 | On the user's side, we can use Hashicorp's [vault-action](https://github.com/hashicorp/vault-action) GitHub Action to retrieve secrets from Vault using OIDC. 31 | Let's explore a "hello world" example. 32 | 33 | ### :keyboard: Activity: OIDC Hello World 34 | 35 | You may see some workflows fail for future steps, like "Step 3, Fine-grained permissions - branches". 36 | That is ok! 37 | We will get to them later. 38 | You can ignore those failures for now. 39 | 40 | 1. Open your repo in a new browser tab, and work on these steps in your second tab while you read the instructions in this tab. 41 | 1. Go to the **Actions tab**. 42 | 1. On the left-hand side, under "All workflows," select **Step 1, OIDC Hello World**. 43 | 1. On the right-hand side, open the **Run workflow** menu and click **Run workflow**. 44 | 45 | ![Manually run workflow](https://user-images.githubusercontent.com/6969296/212499178-7cfc18f9-6860-4d88-a21d-02806b358bb2.png) 46 | 47 | 1. After a few seconds, the workflow run will appear. Click into it. 48 | It can take between 20-40 seconds for this workflow to complete. 49 | Wait until the workflow completes - you should see a green checkmark. 50 | 51 | ![Workflow succeeds](https://user-images.githubusercontent.com/6969296/212499911-42871f96-7e11-4cbf-8d23-5fd1bc0cf480.png) 52 | 53 | 1. Wait about 20 seconds then refresh this README page for the next step. 54 | 55 | Don't worry if you don't understand everything that happened in this step. 56 | We will go over the details in the next step. 57 | -------------------------------------------------------------------------------- /.github/workflows/1-oidc-hello-world.yml: -------------------------------------------------------------------------------- 1 | name: Step 1, OIDC Hello World 2 | 3 | # This step retrieves a test secret from Vault to demonstrate basic usage of GitHub OIDC. 4 | 5 | # This will be manually triggered by the participant 6 | # Reference: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows 7 | on: 8 | workflow_dispatch: 9 | 10 | # Reference: https://docs.github.com/en/actions/security-guides/automatic-token-authentication 11 | permissions: 12 | # Need `contents: read` to checkout the repository 13 | # Need `id-token: write` to use the GitHub OIDC token 14 | # Reference: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-cloud-providers#adding-permissions-settings 15 | contents: read 16 | id-token: write 17 | 18 | jobs: 19 | example_oidc: 20 | name: Your first OIDC secret 21 | # When using services, the runner must be Linux 22 | runs-on: ubuntu-latest 23 | 24 | # Set up a Vault container to connect to 25 | # This allows us to run our exercises without having to set up Vault infrastructure separately 26 | # Reference: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idservices 27 | services: 28 | vault: 29 | image: hashicorp/vault:1.15 30 | # Make vault accessible to the runner at localhost:8200 31 | ports: 32 | - 8200:8200 33 | # Set up Vault as a dev server with a pre-defined root token. 34 | # Reference: https://developer.hashicorp.com/vault/docs/concepts/dev-server 35 | env: 36 | VAULT_DEV_ROOT_TOKEN_ID: vaultiscool 37 | # Grant the non-root user in the container the ability to lock memory to prevent swapping data to disk. 38 | # A step that is recommended when using the Vault Docker container. 39 | # Reference: https://hub.docker.com/_/vault/ 40 | options: >- 41 | --cap-add=IPC_LOCK 42 | 43 | steps: 44 | - name: Checkout 45 | uses: actions/checkout@v4 46 | 47 | # Initializes Vault with a JWT backend for GitHub OIDC 48 | # and sets up a role, policy, and secret to retrieve. 49 | - name: Setup Vault 50 | env: 51 | VAULT_ADDR: http://127.0.0.1:8200 52 | run: ./.github/script/1-setup.sh 53 | 54 | - name: Retrieve Secrets 55 | uses: hashicorp/vault-action@v3 56 | id: secrets 57 | with: 58 | # The previous step created a `hello-world` Vault role in `.setup/1-setup.sh`. 59 | # The role is configured to accept the GitHub OIDC token if it is issued by GitHub - therefore allowing any repo on GitHub.com. 60 | # More specifically, if the `iss` claim is `https://token.actions.githubusercontent.com`. 61 | role: hello-world 62 | # Retrieve a secret from the KV v2 secrets engine at the mount point `secret`. 63 | secrets: | 64 | secret/data/foobar hello | WORLD ; 65 | # Required configuration, do not modify 66 | url: http://127.0.0.1:8200 67 | path: gha 68 | method: jwt 69 | exportEnv: false 70 | 71 | 72 | - name: Print secrets 73 | run: | 74 | echo "Hello ${{ steps.secrets.outputs.WORLD }}!" 75 | echo "Hello ${{ steps.secrets.outputs.WORLD }}!" >> "${GITHUB_STEP_SUMMARY}" 76 | -------------------------------------------------------------------------------- /.github/workflows/2-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Step 2, Fine-grained permissions - pull requests 2 | 3 | # This step will retrieve a secret from Vault only if the workflow is triggered from a pull request 4 | 5 | # This workflow will only succeed when run from a pull request, 6 | # but participants should attempt to run it from other triggers, such as manually, to observe the outcome 7 | # Reference: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows 8 | on: 9 | workflow_dispatch: 10 | pull_request: 11 | branches: 12 | - main 13 | 14 | # Reference: https://docs.github.com/en/actions/security-guides/automatic-token-authentication 15 | permissions: 16 | # Need `contents: read` to checkout the repository 17 | # Need `id-token: write` to use the GitHub OIDC token 18 | # Reference: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-cloud-providers#adding-permissions-settings 19 | contents: read 20 | id-token: write 21 | 22 | jobs: 23 | oidc_pr: 24 | name: Secrets only from pull requests 25 | # When using services, the runner must be Linux 26 | runs-on: ubuntu-latest 27 | 28 | # Set up a Vault container to connect to 29 | # This allows us to run our exercises without having to set up Vault infrastructure separately 30 | # Reference: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idservices 31 | services: 32 | vault: 33 | image: hashicorp/vault:1.15 34 | # Make vault accessible to the runner at localhost:8200 35 | ports: 36 | - 8200:8200 37 | # Set up Vault as a dev server with a pre-defined root token. 38 | # Reference: https://developer.hashicorp.com/vault/docs/concepts/dev-server 39 | env: 40 | VAULT_DEV_ROOT_TOKEN_ID: vaultiscool 41 | # Grant the non-root user in the container the ability to lock memory to prevent swapping data to disk. 42 | # A step that is recommended when using the Vault Docker container. 43 | # Reference: https://hub.docker.com/_/vault/ 44 | options: >- 45 | --cap-add=IPC_LOCK 46 | 47 | steps: 48 | - name: Checkout 49 | uses: actions/checkout@v4 50 | 51 | # Initializes Vault with a JWT backend for GitHub OIDC 52 | # Creates a `pr-policy` policy granting access to `secret/data/development` 53 | - name: Setup Vault 54 | env: 55 | VAULT_ADDR: http://127.0.0.1:8200 56 | run: ./.github/script/2-setup.sh 57 | 58 | ############################################################### 59 | # Assign an appropriate bound_claims for this activity # 60 | # See the Step 2 instructions on the README for more details # 61 | ############################################################### 62 | - name: Create an OIDC Role 63 | env: 64 | VAULT_ADDR: http://127.0.0.1:8200 65 | run: | 66 | vault write auth/gha/role/GIVE_ME_A_NAME - << EOF 67 | { 68 | "role_type": "jwt", 69 | "user_claim": "actor", 70 | "bound_claims": { 71 | 72 | # Fill in missing "sub" claim 73 | 74 | }, 75 | "policies": ["pr-policy"], 76 | "ttl": "60s" 77 | } 78 | EOF 79 | 80 | - name: Retrieve Secrets 81 | uses: hashicorp/vault-action@v3 82 | id: secrets 83 | with: 84 | # TODO: Don't forget to enter the role name you created above! 85 | role: "" 86 | # Retrieve a secret from the KV v2 secrets engine at the mount point `secret`. 87 | secrets: | 88 | secret/data/development access_token | ACCESS_TOKEN ; 89 | # Required configuration, do not modify 90 | url: http://127.0.0.1:8200 91 | path: gha 92 | method: jwt 93 | exportEnv: false 94 | 95 | - name: Use the secret 96 | # Dummy example showing the secret is not an empty string 97 | run: | 98 | echo "::notice::🔐 Logging in to secure system! ${{ steps.secrets.outputs.ACCESS_TOKEN != '' }}" 99 | -------------------------------------------------------------------------------- /.github/workflows/3-main-branch.yml: -------------------------------------------------------------------------------- 1 | name: Step 3, Fine-grained permissions - branches 2 | 3 | # This step will retrieve a secret from Vault only if the workflow is triggered from the main branch 4 | 5 | # This workflow will only succeed when run from a push to the `main` branch, 6 | # but participants should attempt to run it from other triggers, such as manually, to observe the outcome 7 | # Reference: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows 8 | on: 9 | workflow_dispatch: 10 | push: 11 | branches: 12 | - main 13 | 14 | # Reference: https://docs.github.com/en/actions/security-guides/automatic-token-authentication 15 | permissions: 16 | # Need `contents: read` to checkout the repository 17 | # Need `id-token: write` to use the GitHub OIDC token 18 | # Reference: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-cloud-providers#adding-permissions-settings 19 | contents: read 20 | id-token: write 21 | 22 | jobs: 23 | oidc_branch: 24 | name: Secrets only from the default branch 25 | # When using services, the runner must be Linux 26 | runs-on: ubuntu-latest 27 | 28 | # Set up a Vault container to connect to 29 | # This allows us to run our exercises without having to set up Vault infrastructure separately 30 | # Reference: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idservices 31 | services: 32 | vault: 33 | image: hashicorp/vault:1.15 34 | # Make vault accessible to the runner at localhost:8200 35 | ports: 36 | - 8200:8200 37 | # Set up Vault as a dev server with a pre-defined root token. 38 | # Reference: https://developer.hashicorp.com/vault/docs/concepts/dev-server 39 | env: 40 | VAULT_DEV_ROOT_TOKEN_ID: vaultiscool 41 | # Grant the non-root user in the container the ability to lock memory to prevent swapping data to disk. 42 | # A step that is recommended when using the Vault Docker container. 43 | # Reference: https://hub.docker.com/_/vault/ 44 | options: >- 45 | --cap-add=IPC_LOCK 46 | 47 | steps: 48 | - name: Checkout 49 | uses: actions/checkout@v4 50 | 51 | # Initializes Vault with a JWT backend for GitHub OIDC 52 | # Creates a `main-policy` policy granting access to `secret/data/production` 53 | - name: Setup Vault 54 | env: 55 | VAULT_ADDR: http://127.0.0.1:8200 56 | run: ./.github/script/3-setup.sh 57 | 58 | ############################################################### 59 | # Assign an appropriate bound_claims for this activity # 60 | # See the Step 3 instructions on the README for more details # 61 | ############################################################### 62 | - name: Create an OIDC Role 63 | env: 64 | VAULT_ADDR: http://127.0.0.1:8200 65 | run: | 66 | vault write auth/gha/role/GIVE_ME_A_NAME - << EOF 67 | { 68 | "role_type": "jwt", 69 | "user_claim": "actor", 70 | "bound_claims": { 71 | 72 | # Fill in missing "sub" claim 73 | 74 | }, 75 | "policies": ["main-policy"], 76 | "ttl": "60s" 77 | } 78 | EOF 79 | 80 | - name: Retrieve Secrets 81 | uses: hashicorp/vault-action@v3 82 | id: secrets 83 | with: 84 | # TODO: Don't forget to enter the role name you created above! 85 | role: "" 86 | # Retrieve a secret from the KV v2 secrets engine at the mount point `secret`. 87 | secrets: | 88 | secret/data/production access_token | ACCESS_TOKEN ; 89 | # Required configuration, do not modify 90 | url: http://127.0.0.1:8200 91 | path: gha 92 | method: jwt 93 | exportEnv: false 94 | 95 | - name: Use the secret 96 | # Dummy example showing the secret is not an empty string 97 | run: | 98 | echo "::notice::🔐 Logging in to secure **production** system! ${{ steps.secrets.outputs.ACCESS_TOKEN != '' }}" 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 11 | 12 | # Getting secrets from HashiCorp Vault with GitHub OIDC in Action workflows 13 | 14 | Understand the principles behind configuring OIDC authentication from GitHub Action workflows to HashiCorp Vault for least-privilege access to secrets from CI/CD pipelines. 15 | 16 |
17 | 18 | - **Who is this for**: Developers, security engineers, and operators of secrets management programs. 19 | - **What you'll learn**: How to use GitHub OIDC for fine-grained role access to secrets in HashiCorp Vault. 20 | - **What you'll build**: You will create three GitHub Action workflows retrieving secrets from Vault for the following use cases: 21 | 1. Non-production secrets for integration testing within pull requests 22 | 1. Production secrets for deployments of code from the main branch 23 | 1. Segregating access to secrets between jobs in a workflow file with [GitHub Environments](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment) 24 | - **Prerequisites**: 25 | 1. You should have basic proficiency working with HashiCorp Vault. 26 | You should understand how Vault roles correspond to HCL policies and how policies grant access to secrets. 27 | Completing HashiCorp's [Vault Getting Started](https://developer.hashicorp.com/vault/tutorials/getting-started) tutorial is sufficient. 28 | 1. You should also understand the layout of a GitHub Actions workflow file. 29 | The GitHub tutorial [Continuous Integration](https://github.com/skills/continuous-integration) provides a good introduction. 30 | - **How long**: This course is 4 steps long and takes about 1 hour to complete. 31 | 32 | 40 | 41 | ## How to start this course 42 | 43 | [![start-course](https://user-images.githubusercontent.com/1221423/235727646-4a590299-ffe5-480d-8cd5-8194ea184546.svg)](https://github.com/new?template_owner=artis3n&template_name=course-vault-github-oidc&owner=%40me&name=course-vault-github-oidc&description=Learn+how+to+create+fine-grained,+least-privilege+HashiCorp+Vault+roles+for+GitHub+Action+workflows+using+GitHub+OIDC.&visibility=public) 44 | 45 | 1. Make sure you are signed in to GitHub. 46 | Right-click **Start course** and open the link in a new tab. 47 | 2. In the new tab, most of the prompts will automatically fill in for you. 48 | - For owner, choose your personal account or an organization to host the repository. 49 | - We recommend creating a public repository — private repositories will [use Actions minutes](https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions). 50 | 3. After your new repository is created, wait about 20 seconds, then refresh that page. 51 | Follow the step-by-step instructions in the new repository's README. 52 | 53 |
54 | 55 | 59 | 60 | --- 61 | 62 | Get help: [Post in our discussion board](https://github.com/artis3n/course-vault-github-oidc/discussions) • Something not working? [File an issue ticket](https://github.com/artis3n/course-vault-github-oidc/issues) 63 | 64 | © 2022 Ari Kalfus • [Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/code_of_conduct.md) • [CC-BY-4.0 License](https://creativecommons.org/licenses/by/4.0/legalcode) 65 | 66 |
67 | -------------------------------------------------------------------------------- /.github/workflows/4-environment.yml: -------------------------------------------------------------------------------- 1 | name: Step 4, Fine-grained permissions - environments 2 | 3 | # This step will retrieve a secret from Vault if the appropriate Environment is applied 4 | 5 | # This workflow will succeed from any trigger as long as Vault roles are bound to the given Environments in the workflow. 6 | # However, for the course to properly update to the next step, this workflow should be run from a manual `workflow_dispatch` from the `main` branch. 7 | # Reference: https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows 8 | on: 9 | workflow_dispatch: 10 | 11 | # Reference: https://docs.github.com/en/actions/security-guides/automatic-token-authentication 12 | permissions: 13 | # Need `contents: read` to checkout the repository 14 | # Need `id-token: write` to use the GitHub OIDC token 15 | # Reference: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-cloud-providers#adding-permissions-settings 16 | contents: read 17 | id-token: write 18 | 19 | jobs: 20 | staging: 21 | name: Retrieve staging secrets 22 | # We need to create a "Staging" Environment and bind it to Vault! 23 | environment: Staging 24 | 25 | # When using services, the runner must be Linux 26 | runs-on: ubuntu-latest 27 | # Set up a Vault container to connect to 28 | # This allows us to run our exercises without having to set up Vault infrastructure separately 29 | # Reference: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idservices 30 | services: 31 | vault: 32 | image: hashicorp/vault:1.15 33 | # Make vault accessible to the runner at localhost:8200 34 | ports: 35 | - 8200:8200 36 | # Set up Vault as a dev server with a pre-defined root token. 37 | # Reference: https://developer.hashicorp.com/vault/docs/concepts/dev-server 38 | env: 39 | VAULT_DEV_ROOT_TOKEN_ID: vaultiscool 40 | # Grant the non-root user in the container the ability to lock memory to prevent swapping data to disk. 41 | # A step that is recommended when using the Vault Docker container. 42 | # Reference: https://hub.docker.com/_/vault/ 43 | options: >- 44 | --cap-add=IPC_LOCK 45 | 46 | steps: 47 | - name: Checkout 48 | uses: actions/checkout@v4 49 | 50 | # Initializes Vault with a JWT backend for GitHub OIDC 51 | # Creates policies for our two environments - if we were using a real Vault, these would both be present 52 | - name: Setup Vault 53 | env: 54 | VAULT_ADDR: http://127.0.0.1:8200 55 | run: ./.github/script/4-setup.sh 56 | 57 | ############################################################### 58 | # Assign an appropriate bound_claims for this activity # 59 | # See the Step 4 instructions on the README for more details # 60 | ############################################################### 61 | - name: Create an OIDC Role 62 | env: 63 | VAULT_ADDR: http://127.0.0.1:8200 64 | run: | 65 | vault write auth/gha/role/GIVE_ME_A_NAME - << EOF 66 | { 67 | "role_type": "jwt", 68 | "user_claim": "actor", 69 | "bound_claims": { 70 | 71 | # Fill in missing "sub" claim 72 | 73 | }, 74 | "policies": ["staging-policy"], 75 | "ttl": "60s" 76 | } 77 | EOF 78 | 79 | - name: Retrieve Secrets 80 | uses: hashicorp/vault-action@v3 81 | id: secrets 82 | with: 83 | # TODO: Don't forget to enter the role name you created above! 84 | role: "" 85 | # Retrieve a secret from the KV v2 secrets engine at the mount point `secret`. 86 | secrets: | 87 | secret/data/staging access_token | ACCESS_TOKEN ; 88 | # Required configuration, do not modify 89 | url: http://127.0.0.1:8200 90 | path: gha 91 | method: jwt 92 | exportEnv: false 93 | 94 | - name: Use the secret 95 | # Dummy example showing the secret is not an empty string 96 | run: | 97 | echo "::notice::🔐 Logging in to **staging** system! ${{ steps.secrets.outputs.ACCESS_TOKEN != '' }}" 98 | 99 | prod: 100 | name: Retrieve production secrets 101 | # We need to create a "Production" Environment and bind it to Vault! 102 | environment: Production 103 | 104 | runs-on: ubuntu-latest 105 | services: 106 | vault: 107 | image: hashicorp/vault:1.15 108 | ports: 109 | - 8200:8200 110 | env: 111 | VAULT_DEV_ROOT_TOKEN_ID: vaultiscool 112 | options: >- 113 | --cap-add=IPC_LOCK 114 | 115 | steps: 116 | - name: Checkout 117 | uses: actions/checkout@v4 118 | 119 | - name: Setup Vault 120 | env: 121 | VAULT_ADDR: http://127.0.0.1:8200 122 | run: ./.github/script/4-setup.sh 123 | 124 | ############################################################### 125 | # Assign an appropriate bound_claims for this activity # 126 | # See the Step 4 instructions on the README for more details # 127 | ############################################################### 128 | - name: Create an OIDC Role 129 | env: 130 | VAULT_ADDR: http://127.0.0.1:8200 131 | run: | 132 | vault write auth/gha/role/GIVE_ME_A_NAME - << EOF 133 | { 134 | "role_type": "jwt", 135 | "user_claim": "actor", 136 | "bound_claims": { 137 | 138 | # Fill in missing "sub" claim 139 | 140 | }, 141 | "policies": ["prod-policy"], 142 | "ttl": "60s" 143 | } 144 | EOF 145 | 146 | - name: Retrieve Secrets 147 | uses: hashicorp/vault-action@v3 148 | id: secrets 149 | with: 150 | # TODO: Don't forget to enter the role name you created above! 151 | role: "" 152 | secrets: | 153 | secret/data/production access_token | ACCESS_TOKEN ; 154 | # Required configuration, do not modify 155 | url: http://127.0.0.1:8200 156 | path: gha 157 | method: jwt 158 | exportEnv: false 159 | 160 | - name: Use the secret 161 | # Dummy example showing the secret is not an empty string 162 | run: | 163 | echo "::notice::🔐 Logging in to **production** system! ${{ steps.secrets.outputs.ACCESS_TOKEN != '' }}" 164 | -------------------------------------------------------------------------------- /.github/steps/3-branches.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ## Step 3: Fine-grained permissions - branches 9 | 10 | _Nice work finishing Step 2: Fine-grained permissions - pull requests :sparkles:_ 11 | 12 | In the Step 2 activity, you created a Vault role that allowed a workflow to authenticate to Vault if it was run by a `pull_request` workflow trigger. 13 | After retrieving the secret, the workflow printed out an assertion that it received the value: 14 | 15 | ```yml 16 | - name: Use the secret 17 | # Dummy example showing the secret is not an empty string 18 | run: | 19 | echo "::notice::🔐 Logging in to secure system! ${{ steps.secrets.outputs.ACCESS_TOKEN != '' }}" 20 | ``` 21 | 22 | If you inspect the job summary for the `Step 2, Fine-grained permissions - pull requests` workflow, you should see the delivered message ends with `true`. 23 | 24 | ![Secure system message on job summary](https://user-images.githubusercontent.com/6969296/212549524-e430a8d9-96d4-4210-9fa6-3bc6ae903cec.png) 25 | 26 | Given any use of our secrets is redacted from GitHub's logs, this is a contrived example to demonstrate that `steps.secrets.outputs.ACCESS_TOKEN` is not empty - the value of the secret has been populated from Vault. 27 | 28 | If you haven't already, merge your pull request and attempt to manually run the `Step 2, Fine-grained permissions - pull requests` workflow - select it from under the "All workflows" pane similar to what you did in Step 1. 29 | The workflow should fail with a 400 error trying to authenticate to Vault - because you're running it via a manual `workflow_dispatch` instead of a `pull_request`! 30 | Optionally, you can modify the workflow and add a `push:` trigger and try that as well. 31 | Authentication to Vault will also fail because the GitHub JWT's claims no longer match the bound subject (`sub`) claim we defined for the Vault role. 32 | 33 | ### Bound claims 34 | 35 | We can bind Vault roles to any claim present in the JWT. 36 | The `sub` claim is the primary method we have to configure GitHub OIDC Vault roles. 37 | 38 | To learn more, check out ["Understanding the OIDC token"](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token). 39 | 40 | ```json 41 | { 42 | "typ": "JWT", 43 | "alg": "RS256", 44 | "x5t": "example-thumbprint", 45 | "kid": "example-key-id" 46 | } 47 | { 48 | "jti": "example-id", 49 | "sub": "repo:octo-org/octo-repo:environment:prod", 50 | "environment": "prod", 51 | "aud": "https://github.com/octo-org", 52 | "ref": "refs/heads/main", 53 | "sha": "example-sha", 54 | "repository": "octo-org/octo-repo", 55 | "repository_owner": "octo-org", 56 | "actor_id": "12", 57 | "repository_visibility": "private", 58 | "repository_id": "74", 59 | "repository_owner_id": "65", 60 | "run_id": "example-run-id", 61 | "run_number": "10", 62 | "run_attempt": "2", 63 | "actor": "octocat", 64 | "workflow": "example-workflow", 65 | "head_ref": "", 66 | "base_ref": "", 67 | "event_name": "workflow_dispatch", 68 | "ref_type": "branch", 69 | "job_workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main", 70 | "iss": "https://token.actions.githubusercontent.com", 71 | "nbf": 1632492967, 72 | "exp": 1632493867, 73 | "iat": 1632493567 74 | } 75 | ``` 76 | 77 | In our Step 1 hello world activity, we bound the Vault role to the `iss` claim. 78 | This will always be `https://token.actions.githubusercontent.com` for workflows on github.com, so that is not a very good claim to restrict fine-grained access. 79 | 80 | In our Step 2 activity, we took a real-world approach and bound the Vault role to the `sub` claim. 81 | The subject can be constructed from [various filters](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#example-subject-claims). 82 | The options boil down to: 83 | - `pull_request` (but no other) workflow triggers 84 | ``` 85 | repo::pull_request 86 | ``` 87 | - A single branch or tag on the repository 88 | ``` 89 | repo::ref:refs/heads/ 90 | repo::ref:refs/tags/ 91 | ``` 92 | - Multiple branches or tags using a wildcard (`*`) in the subject claim 93 | ``` 94 | repo::ref:refs/heads/feature/* 95 | repo::ref:refs/tags/v1.* 96 | ``` 97 | - A single [GitHub Environment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment) 98 | ``` 99 | repo::environment: 100 | ``` 101 | 102 | You could presumably use a wildcard for multiple GitHub Environments as well, but I have not come across a use case for that. 103 | 104 | You don't have to use the `sub` claim! 105 | For example, when designing a workflow meant to run when Dependabot opens a PR or merges code, you could bind the Vault role to the `actor` claim. 106 | ```json 107 | "bound_claims": { 108 | "actor": "dependabot[bot]" 109 | }, 110 | ``` 111 | 112 | Or, if you have a workflow that can run at any time, but the Vault role should only be used for one specific repository, you can bind the Vault role to the `repository` claim. 113 | ```json 114 | "bound_claims": { 115 | "repository": "myorg/myrepo" 116 | }, 117 | ``` 118 | 119 | However, `sub`, and combining `sub` with other claims, is the most powerful option we have to construct fine-grained access control. 120 | 121 | ### :keyboard: Activity: Fine-grained permissions - branches 122 | 123 | Let's apply what we've learned to our next workflow. 124 | You're going to follow the instructions from our previous activity, but this time you're going to bind the Vault role to the `main` branch. 125 | Vault authentication in this workflow will succeed only if the workflow is triggered by a push to the `main` branch. 126 | 127 | 1. Open this repository in a code editor or GitHub Codespace. 128 | If you still have this repository open from the previous activity, make sure to pull the latest changes from the `main` branch. 129 | ```bash 130 | git checkout main 131 | git pull 132 | ``` 133 | 1. From the code editor, make sure you are working on the `main` branch. 134 | > [!IMPORTANT] 135 | > For this activity, you must push code to the `main` branch. 136 | 1. In your code editor, open the file `.github/workflows/3-main-branch.yml`. 137 | 1. Locate the step `name: Create an OIDC Role`. 138 | 1. Replace this step with the following code. 139 | > [!IMPORTANT] 140 | > Replace the `YOUR_REPO` section with the `org/repo` string that applies to the repository you created from this course. 141 | 142 | For example, the course template hosted at would use: `"sub": "repo:artis3n/course-vault-github-oidc:ref:refs/heads/main"`. 143 | The workflow won't run unless the `org/repo` string is correct for your repository. 144 | ```yml 145 | - name: Create an OIDC Role 146 | env: 147 | VAULT_ADDR: http://127.0.0.1:8200 148 | run: | 149 | vault write auth/gha/role/GIVE_ME_A_NAME - << EOF 150 | { 151 | "role_type": "jwt", 152 | "user_claim": "actor", 153 | "bound_claims": { 154 | "sub": "repo:YOUR_REPO:ref:refs/heads/main" 155 | }, 156 | "policies": ["main-policy"], 157 | "ttl": "60s" 158 | } 159 | EOF 160 | ``` 161 | 1. Don't forget to pick a name for your Vault role as well! 162 | In the same code block, replace `GIVE_ME_A_NAME` with an alphanumeric (plus `_` and `-`) name of your choosing. 163 | 1. Locate the next step in the job, `name: Retrieve Secrets`. 164 | ```yml 165 | - name: Retrieve Secrets 166 | uses: hashicorp/vault-action@v2 167 | id: secrets 168 | with: 169 | # TODO: Don't forget to enter the role name you created above! 170 | role: "" 171 | # Retrieve a secret from the KV v2 secrets engine at the mount point `secret`. 172 | secrets: | 173 | secret/data/production access_token | ACCESS_TOKEN ; 174 | # Required configuration, do not modify 175 | url: http://127.0.0.1:8200 176 | path: gha 177 | method: jwt 178 | exportEnv: false 179 | ``` 180 | 1. Everything is set up for you, however the `role: ""` is missing. 181 | Enter the `GIVE_ME_A_NAME` role name you chose in the previous step. 182 | ```yml 183 | role: "GIVE_ME_A_NAME" # Enter the same role name you previously chose! 184 | ``` 185 | 1. Commit these changes to the `main` branch and push them to GitHub. 186 | ```bash 187 | git checkout main 188 | git add . 189 | git commit -m "Fine-grained permissions - branches" 190 | git push 191 | ``` 192 | 1. Open your repo in a new browser tab, and work on these steps in your second tab while you read the instructions in this tab. 193 | 1. Go to the **Actions** tab. 194 | 1. On the left-hand side, under "All workflows," select **Step 3, Fine-grained permissions - branches**. 195 | After a few seconds, you should observe a new workflow start up. 196 | 1. Wait until the workflow completes - you should see a green checkmark. 197 | - If the workflow fails, check that your `org/repo` value is correct for your current repository! 198 | Ensure the `role` name matches between both steps in the workflow. 199 | - If you continue to receive an error, pay close attention to the `sub` claim! 200 | It should end with `:ref:refs/heads/main`. 201 | 1. Once this workflow is successful, wait about 20 seconds further, then refresh this README page for the next step. 202 | -------------------------------------------------------------------------------- /.github/steps/4-environments.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ## Step 4: Fine-grained permissions - environments 9 | 10 | _Nicely done with Step 3: Fine-grained permissions - branches! :partying_face:_ 11 | 12 | Our last workflow will demonstrate how to provide fine-grained access control to Vault roles inside the same workflow file using [GitHub Environments](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment). 13 | We'll follow the same instructions as in the previous activities, but this time we'll have two jobs to configure in the same workflow file. 14 | We'll also need to create the GitHub Environments on our repository. 15 | 16 | ### :keyboard: Activity: Fine-grained permissions - environments 17 | 18 | 1. Open this repository in a code editor or GitHub Codespace. 19 | If you still have this repository open from the previous activity, make sure to pull the latest changes from the `main` branch. 20 | ```bash 21 | git checkout main 22 | git pull 23 | ``` 24 | 1. From the code editor, make sure you are working on the `main` branch. 25 | > [!IMPORTANT] 26 | > For this activity to properly update you to the next step, you must push code to the `main` branch. 27 | 28 | Environments are not restricted to the `main` branch by default, but this workflow must run from the `main` branch for this course to properly track your progress. 29 | 1. In your code editor, open the file `.github/workflows/4-environment.yml`. 30 | 1. There are two jobs in this workflow file! 31 | `staging` and `prod`. 32 | We'll configure a Vault role for each job. 33 | Notice the `staging` job includes the `environment: Staging` attribute and the `prod` job includes the `environment: Production` attribute. 34 | To learn more about using Environments in workflow files, see [GitHub's workflow syntax for environments](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idenvironment). 35 | ```yml 36 | staging: 37 | name: Retrieve staging secrets 38 | # We need to create a "Staging" Environment and bind it to Vault! 39 | environment: Staging 40 | 41 | # ... 42 | 43 | prod: 44 | name: Retrieve production secrets 45 | # We need to create a "Production" Environment and bind it to Vault! 46 | environment: Production 47 | ``` 48 | 1. Under the **staging** job, locate the step `name: Create an OIDC Role`. 49 | 1. Under the **staging** job, replace this step with the following code. 50 | > [!IMPORTANT] 51 | > Replace the `YOUR_REPO` section with the `org/repo` string that applies to the repository you created from this course. 52 | 53 | For example, the course template hosted at would use: `"sub": "repo:artis3n/course-vault-github-oidc:environment:Staging"`. 54 | The workflow won't run unless the `org/repo` string is correct for your repository. 55 | ```yml 56 | - name: Create an OIDC Role 57 | env: 58 | VAULT_ADDR: http://127.0.0.1:8200 59 | run: | 60 | vault write auth/gha/role/GIVE_ME_A_NAME - << EOF 61 | { 62 | "role_type": "jwt", 63 | "user_claim": "actor", 64 | "bound_claims": { 65 | "sub": "repo:YOUR_REPO:environment:Staging" 66 | }, 67 | "policies": ["staging-policy"], 68 | "ttl": "60s" 69 | } 70 | EOF 71 | ``` 72 | > [!NOTE] 73 | > **Environment names are case-sensitive.** `staging` is not the same as `Staging`. 74 | 1. Don't forget to pick a name for your Vault role as well! 75 | You should use different names for the staging and production Vault roles. 76 | In the same code block, replace `GIVE_ME_A_NAME` with an alphanumeric (plus `_` and `-`) name of your choosing. 77 | 1. Under the **staging** job, locate the next step in the job, `name: Retrieve Secrets`. 78 | ```yml 79 | - name: Retrieve Secrets 80 | uses: hashicorp/vault-action@v2 81 | id: secrets 82 | with: 83 | # TODO: Don't forget to enter the role name you created above! 84 | role: "" 85 | # Retrieve a secret from the KV v2 secrets engine at the mount point `secret`. 86 | secrets: | 87 | secret/data/staging access_token | ACCESS_TOKEN ; 88 | # Required configuration, do not modify 89 | url: http://127.0.0.1:8200 90 | path: gha 91 | method: jwt 92 | exportEnv: false 93 | ``` 94 | 1. Everything is set up for you, however the `role: ""` is missing. 95 | Enter the `GIVE_ME_A_NAME` role name you chose in the previous step. 96 | ```yml 97 | role: "GIVE_ME_A_NAME" # Enter the same role name you previously chose! 98 | ``` 99 | 1. Now repeat both updates for the **prod** job. 100 | 1. Under the **prod** job, locate the step `name: Create an OIDC Role`. 101 | 1. Under the **prod** job, replace this step with the following code. 102 | > [!IMPORTANT] 103 | > Replace the `YOUR_REPO` section with the `org/repo` string that applies to the repository you created from this course. 104 | 105 | For example, the course template hosted at would use: `"sub": "repo:artis3n/course-vault-github-oidc:environment:Production"`. 106 | The workflow won't run unless the `org/repo` string is correct for your repository. 107 | ```yml 108 | - name: Create an OIDC Role 109 | env: 110 | VAULT_ADDR: http://127.0.0.1:8200 111 | run: | 112 | vault write auth/gha/role/GIVE_ME_A_NAME - << EOF 113 | { 114 | "role_type": "jwt", 115 | "user_claim": "actor", 116 | "bound_claims": { 117 | "sub": "repo:YOUR_REPO:environment:Production" 118 | }, 119 | "policies": ["prod-policy"], 120 | "ttl": "60s" 121 | } 122 | EOF 123 | ``` 124 | > [!NOTE] 125 | > **Environment names are case-sensitive.** `production` is not the same as `Production`. 126 | 1. Don't forget to pick a name for your Vault role as well! 127 | You should use different names for the staging and production Vault roles. 128 | In the same code block, replace `GIVE_ME_A_NAME` with an alphanumeric (plus `_` and `-`) name of your choosing. 129 | 1. Under the **prod** job, locate the next step in the job, `name: Retrieve Secrets`. 130 | ```yml 131 | - name: Retrieve Secrets 132 | uses: hashicorp/vault-action@v2 133 | id: secrets 134 | with: 135 | # TODO: Don't forget to enter the role name you created above! 136 | role: "" 137 | # Retrieve a secret from the KV v2 secrets engine at the mount point `secret`. 138 | secrets: | 139 | secret/data/production access_token | ACCESS_TOKEN ; 140 | # Required configuration, do not modify 141 | url: http://127.0.0.1:8200 142 | path: gha 143 | method: jwt 144 | exportEnv: false 145 | ``` 146 | 1. Everything is set up for you, however the `role: ""` is missing. 147 | Enter the `GIVE_ME_A_NAME` role name you chose in the previous step. 148 | ```yml 149 | role: "GIVE_ME_A_NAME" # Enter the same role name you previously chose! 150 | ``` 151 | 152 | Great work! :computer: 153 | 154 | _Don't commit and push these changes just yet!_ 155 | 156 | We've set up our workflow file to retrieve secrets from GitHub Environments, but we haven't created the Environments yet. 157 | Let's do that now. 158 | 159 | ### :keyboard: Activity: Create the GitHub Environments 160 | 161 | 1. Open your repo in a new browser tab, and work on these steps in your second tab while you read the instructions in this tab. 162 | 1. Go to the **Settings tab**. 163 | 1. On the left-hand side, select **Environments** then **New environment**. 164 | 165 | ![Environment settings](https://user-images.githubusercontent.com/6969296/212564551-12594c1b-63d9-4826-a291-faff012d98af.png) 166 | 167 | 1. Enter `Staging` as the name of the environment and click **Configure environment**. 168 | 169 | ![Name environment staging](https://user-images.githubusercontent.com/6969296/212564575-498e47af-97df-4959-b071-8aefafba99b7.png) 170 | 171 | 1. For OIDC to Vault we do not need to configure any additional settings on the environment, however we can always include additional settings should we want further access controls (such as restricting a production environment to the `main` branch). 172 | 173 | ![No environment configuration](https://user-images.githubusercontent.com/6969296/212564636-f8eccc66-25d7-4f2f-b36c-b2101acd9645.png) 174 | 175 | 1. Now repeat the same steps to create a `Production` environment. 176 | Just as with `Staging`, do not configure any additional settings on the environment for this activity. 177 | 178 | You should now have two environments configured on your repository, `Staging` and `Production`. 179 | 180 | ![List of environments](https://user-images.githubusercontent.com/6969296/212564665-d44c530c-f4dc-4917-bbec-23c089146f4b.png) 181 | 182 | ### :keyboard: Activity: Run the workflow! 183 | 184 | We're now ready to run the workflow! 185 | 186 | 1. Commit the changes from these two activities and push them to GitHub. 187 | ```bash 188 | git checkout main 189 | git add . 190 | git commit -m "Fine-grained permissions - environments" 191 | git push 192 | ``` 193 | 1. Open your repo in a new browser tab, and work on these steps in your second tab while you read the instructions in this tab. 194 | 1. Go to the **Actions** tab. 195 | 1. On the left-hand side, under "All workflows," select **Step 4, Fine-grained permissions - environments**. 196 | 1. On the right-hand side, open the **Run workflow** menu and click **Run workflow**. 197 | 1. Wait until the workflow completes - you should see both the `staging` and `production` jobs complete successfully with a green checkmark. 198 | If the workflow fails, check the previous activities to ensure you've created two environments and configured both Vault roles in the workflow file. 199 | 200 | ![Both environments deploy successfully](https://user-images.githubusercontent.com/6969296/212571497-39feb61a-c1b6-4d3f-8411-9ad3ea029794.png) 201 | 202 | 1. Once this workflow is successful, wait about 20 seconds further, then refresh this README page for the next step. 203 | -------------------------------------------------------------------------------- /.github/steps/2-prs.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ## Step 2: Fine-grained permissions - pull requests 9 | 10 | _You successfully authenticated and retrieved a Vault secret using GitHub OIDC! :tada:_ 11 | 12 | What did we just do, and how? 13 | 14 | ### The GitHub OIDC Workflow 15 | 16 | Let's understand the [workflow file that you just ran](/.github/workflows/1-oidc-hello-world.yml). 17 | 18 | ```yml 19 | permissions: 20 | # Need `contents: read` to checkout the repository 21 | # Need `id-token: write` to use the GitHub OIDC token 22 | # Reference: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-cloud-providers#adding-permissions-settings 23 | contents: read 24 | id-token: write 25 | ``` 26 | 27 | To enable a workflow to use OIDC, you must grant the `id-token: write` permission. 28 | When you start defining permissions, all the default permissions are removed, so you have to add them back in. 29 | Typically, you will always need `contents: read` to checkout the repository. 30 | 31 | You can read more about the required workflow permissions on [GitHub's docs](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings). 32 | Also see ["Permissions for the `GITHUB_TOKEN`"](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token). 33 | 34 | ```yml 35 | # Initializes Vault with a JWT backend for GitHub OIDC 36 | # and sets up a role, policy, and secret to retrieve. 37 | - name: Setup Vault 38 | env: 39 | VAULT_ADDR: http://127.0.0.1:8200 40 | run: ./.github/script/1-setup.sh 41 | ``` 42 | 43 | For the purposes of this course, we set up a local Vault instance for you to authenticate against using OIDC. 44 | We'll take a look at this setup script in a moment. 45 | 46 | ```yml 47 | - name: Retrieve Secrets 48 | uses: hashicorp/vault-action@v2 49 | id: secrets 50 | with: 51 | url: http://127.0.0.1:8200 52 | path: gha 53 | method: jwt 54 | exportEnv: false 55 | # The previous step created a `hello-world` Vault role in `.github/script/1-setup.sh`. 56 | # The role is configured to accept the GitHub OIDC token if it is issued by GitHub - therefore allowing any repo on GitHub.com. 57 | # More specifically, if the `iss` claim is `https://token.actions.githubusercontent.com`. 58 | role: hello-world 59 | # Retrieve a secret from the KV v2 secrets engine at the mount point `secret`. 60 | secrets: | 61 | secret/data/foobar hello | WORLD ; 62 | ``` 63 | 64 | Here we use HashiCorp's [vault-action](https://github.com/hashicorp/vault-action) to retrieve a secret from Vault using OIDC. 65 | We specify the Vault role we want (`hello-world`), whatever secrets we want to retrieve, and, optionally but recommended for clarity, what output variable we'd like to assign to each secret. 66 | In this case, we save the output of the secret to the `WORLD` variable. 67 | 68 | If our OIDC configuration for the `hello-world` role matches the token that GitHub presents, our workflow will get an auth token and the requested secrets (assuming the Vault role's policy permits those paths). 69 | 70 | ```yaml 71 | - name: Print secrets 72 | run: | 73 | echo "Hello ${{ steps.secrets.outputs.WORLD }}!" 74 | echo "Hello ${{ steps.secrets.outputs.WORLD }}!" >> "${GITHUB_STEP_SUMMARY}" 75 | ``` 76 | 77 | Finally, since we've set `id: secrets` attribute on the `hashicorp/vault-action` step, we can access our secret using the syntax `steps.secrets.outputs.WORLD`. 78 | 79 | If you inspect the workflow run output, you'll see that GitHub automatically redacts the secret from the logs. 80 | 81 | ![Inspect workflow run output](https://user-images.githubusercontent.com/6969296/212510889-7d7f9c6a-b706-4c74-aa46-e6cecdef30e0.png) 82 | 83 | Our workflow also echoes the secret to the workflow step summary, where the value remains redacted. 84 | 85 | ![Inspect workflow step summary](https://user-images.githubusercontent.com/6969296/212511387-38764a5d-c04e-43d0-9746-192fac51205c.png) 86 | 87 | `hashicorp/vault-action` [configures this for us](https://github.com/hashicorp/vault-action#masking---hiding-secrets-from-logs). 88 | How convenient! 89 | Note that this only obscures the value from output logs - someone with the ability to edit your workflow and inject code can still read the secret. 90 | 91 | ### The Vault Setup 92 | 93 | Let's look at the Vault commands we ran in the [setup script](/.github/script/1-setup.sh). 94 | 95 | ```bash 96 | vault auth enable -path=gha jwt 97 | vault write auth/gha/config \ 98 | bound_issuer="https://token.actions.githubusercontent.com" \ 99 | oidc_discovery_url="https://token.actions.githubusercontent.com" 100 | ``` 101 | 102 | These are the base requirements to enable OIDC authentication in Vault. 103 | We created a new authentication backend at the path `gha` (you can choose whatever path name you prefer) and configured it to receive OIDC tokens from GitHub. 104 | The only requirements are the `bound_issuer` and `oidc_discovery_url` fields. 105 | 106 | To learn more, check out "[Configuring OpenID Connect in HashiCorp Vault](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-hashicorp-vault#adding-the-identity-provider-to-hashicorp-vault)". 107 | 108 | ```bash 109 | # Create a secret 110 | vault kv put secret/foobar hello=world 111 | # Add OIDC role and policy 112 | vault policy write hello-policy - << EOF 113 | path "secret/data/foobar" { 114 | capabilities = ["read"] 115 | } 116 | EOF 117 | ``` 118 | 119 | Then, we created a secret and a basic policy to access it. 120 | 121 | ```bash 122 | vault write auth/gha/role/hello-world - << EOF 123 | { 124 | "role_type": "jwt", 125 | "user_claim": "actor", 126 | "bound_claims": { 127 | "iss": "https://token.actions.githubusercontent.com" 128 | }, 129 | "policies": ["hello-policy"], 130 | "ttl": "60s" 131 | } 132 | EOF 133 | ``` 134 | 135 | Finally, we created a role that binds to the `iss` claim in GitHub's OIDC token. 136 | 137 | 138 | 139 | > [!WARNING] 140 | > You don't want to use this in real life! :wink: :scream: 141 | 142 | This claim means that anyone anywhere on github.com can authenticate to this Vault instance and be granted the `hello-policy` policy. 143 | We'll explore real-world examples of fine-grained access in the next steps of this course. 144 | 145 | Let's look at the other values: 146 | - `role_type` should always be `jwt` for GitHub OIDC. 147 | - `user_claim` is the value that Vault uses to identify the user. 148 | Its data will become the `auth.display_name` value in Vault's audit logs. 149 | In this case, we set the `user_claim` to be `actor`, which means Vault's audit log will record the GitHub username of the entity who ran the workflow. 150 | This can be a GitHub user or something like Dependabot, depending on what triggers the workflow. 151 | - `bound_claims` is a map of the claims that all must be present in GitHub's OIDC token in order to successfully authenticate to this role. 152 | There must be at least one bound claim. 153 | We'll see how to add more claims soon. 154 | - `policies` is a list of policies that will be granted to the Vault token when a workflow authenticates to this role. 155 | - `ttl` is the time-to-live for the token that Vault returns to the workflow. 156 | In this case, we've set the generated Vault token to expire after 60 seconds. 157 | While the workflow runs in 20-40 seconds, the time between retrieving the Vault token and accessing secrets inside the workflow is about 1-2 seconds. 158 | You could set the TTL to be really short! 159 | A short TTL means that an attacker who gains access to your CI/CD environment will have very little time to do anything malicious. :sunglasses: 160 | 161 | ### :keyboard: Activity: Fine-grained permissions - pull requests 162 | 163 | Wow, that was a lot of information! :exploding_head: 164 | 165 | Give yourself a pat on the back for making it here. 166 | Next, you will create your own OIDC Vault role! 167 | This role will allow a workflow to authenticate to Vault, but only if the workflow runs inside of a pull request. 168 | 169 | 1. Open this repository in a code editor or GitHub Codespace. 170 | 1. From the code editor, checkout a new branch. 171 | **For this activity, you must open a pull request from a branch other than `main`.** 172 | ```bash 173 | git checkout -b step2 174 | ``` 175 | 1. In your code editor, open the file `.github/workflows/2-pull-request.yml`. 176 | 1. Locate the step `name: Create an OIDC Role`. 177 | 1. Replace this step with the following code. 178 | **Replace the `YOUR_REPO` section with the `org/repo` string that applies to the repository you created from this course**. 179 | For example, the course template hosted at would use: `"sub": "repo:artis3n/course-vault-github-oidc:pull_request"`. 180 | The workflow won't run unless the `org/repo` string is correct for your repository. 181 | ```yml 182 | - name: Create an OIDC Role 183 | env: 184 | VAULT_ADDR: http://127.0.0.1:8200 185 | run: | 186 | vault write auth/gha/role/GIVE_ME_A_NAME - << EOF 187 | { 188 | "role_type": "jwt", 189 | "user_claim": "actor", 190 | "bound_claims": { 191 | "sub": "repo:YOUR_REPO:pull_request" 192 | }, 193 | "policies": ["pr-policy"], 194 | "ttl": "60s" 195 | } 196 | EOF 197 | ``` 198 | 1. Don't forget to pick a name for your Vault role as well! 199 | In the same code block, replace `GIVE_ME_A_NAME` with an alphanumeric (plus `_` and `-`) name of your choosing. 200 | 1. Locate the next step in the job, `name: Retrieve Secrets`. 201 | ```yml 202 | - name: Retrieve Secrets 203 | uses: hashicorp/vault-action@v2 204 | id: secrets 205 | with: 206 | # TODO: Don't forget to enter the role name you created above! 207 | role: "" 208 | # Retrieve a secret from the KV v2 secrets engine at the mount point `secret`. 209 | secrets: | 210 | secret/data/development access_token | ACCESS_TOKEN ; 211 | # Required configuration, do not modify 212 | url: http://127.0.0.1:8200 213 | path: gha 214 | method: jwt 215 | exportEnv: false 216 | ``` 217 | 1. Everything is set up for you, however the `role: ""` is missing. 218 | Enter the `GIVE_ME_A_NAME` role name you chose in the previous step. 219 | ```yml 220 | role: "GIVE_ME_A_NAME" # Enter the same role name you previously chose! 221 | ``` 222 | 1. Commit these changes to your branch and push your branch to GitHub. 223 | Open a pull request from your branch to the `main` branch. 224 | ```bash 225 | git add . 226 | git commit -m "Add OIDC role for pull requests" 227 | gh pr create --title "Fine-grained permissions - pull requests" --body "This pull request adds a new workflow that uses Vault to retrieve a secret only if the workflow runs inside a pull request." 228 | ``` 229 | 230 | > [!NOTE] 231 | > The `gh` command comes from the [GitHub CLI](https://cli.github.com/). 232 | > You can create the pull request from the UI as well. 233 | 1. Go to the **Pull Requests** tab and open your new pull request. 234 | After a few seconds, you should observe the `Step 2, Fine-grained permissions - pull requests` workflow begin to run on your PR. 235 | 236 | ![Pull request workflow running](https://user-images.githubusercontent.com/6969296/212520410-1f4a73ba-67db-4471-bf2c-fcc2d819f473.png) 237 | 238 | 1. Wait until your `Step 2, Fine-grained permissions - pull requests` workflow completes - you should see a green checkmark. 239 | If the workflow fails, check that your `org/repo` value is correct for your current repository! 240 | Ensure the `role` name matches between both steps in the workflow. 241 | 1. Once the PR workflow is successful, wait about 20 seconds further, then refresh this README page for the next step. 242 | 243 | Once the workflow is green, feel free to merge the PR or leave it as-is before moving on to the next step. 244 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Attribution 4.0 International 2 | 3 | ======================================================================= 4 | 5 | Creative Commons Corporation ("Creative Commons") is not a law firm and 6 | does not provide legal services or legal advice. Distribution of 7 | Creative Commons public licenses does not create a lawyer-client or 8 | other relationship. Creative Commons makes its licenses and related 9 | information available on an "as-is" basis. Creative Commons gives no 10 | warranties regarding its licenses, any material licensed under their 11 | terms and conditions, or any related information. Creative Commons 12 | disclaims all liability for damages resulting from their use to the 13 | fullest extent possible. 14 | 15 | Using Creative Commons Public Licenses 16 | 17 | Creative Commons public licenses provide a standard set of terms and 18 | conditions that creators and other rights holders may use to share 19 | original works of authorship and other material subject to copyright 20 | and certain other rights specified in the public license below. The 21 | following considerations are for informational purposes only, are not 22 | exhaustive, and do not form part of our licenses. 23 | 24 | Considerations for licensors: Our public licenses are 25 | intended for use by those authorized to give the public 26 | permission to use material in ways otherwise restricted by 27 | copyright and certain other rights. Our licenses are 28 | irrevocable. Licensors should read and understand the terms 29 | and conditions of the license they choose before applying it. 30 | Licensors should also secure all rights necessary before 31 | applying our licenses so that the public can reuse the 32 | material as expected. Licensors should clearly mark any 33 | material not subject to the license. This includes other CC- 34 | licensed material, or material used under an exception or 35 | limitation to copyright. More considerations for licensors: 36 | wiki.creativecommons.org/Considerations_for_licensors 37 | 38 | Considerations for the public: By using one of our public 39 | licenses, a licensor grants the public permission to use the 40 | licensed material under specified terms and conditions. If 41 | the licensor's permission is not necessary for any reason--for 42 | example, because of any applicable exception or limitation to 43 | copyright--then that use is not regulated by the license. Our 44 | licenses grant only permissions under copyright and certain 45 | other rights that a licensor has authority to grant. Use of 46 | the licensed material may still be restricted for other 47 | reasons, including because others have copyright or other 48 | rights in the material. A licensor may make special requests, 49 | such as asking that all changes be marked or described. 50 | Although not required by our licenses, you are encouraged to 51 | respect those requests where reasonable. More considerations 52 | for the public: 53 | wiki.creativecommons.org/Considerations_for_licensees 54 | 55 | ======================================================================= 56 | 57 | Creative Commons Attribution 4.0 International Public License 58 | 59 | By exercising the Licensed Rights (defined below), You accept and agree 60 | to be bound by the terms and conditions of this Creative Commons 61 | Attribution 4.0 International Public License ("Public License"). To the 62 | extent this Public License may be interpreted as a contract, You are 63 | granted the Licensed Rights in consideration of Your acceptance of 64 | these terms and conditions, and the Licensor grants You such rights in 65 | consideration of benefits the Licensor receives from making the 66 | Licensed Material available under these terms and conditions. 67 | 68 | 69 | Section 1 -- Definitions. 70 | 71 | a. Adapted Material means material subject to Copyright and Similar 72 | Rights that is derived from or based upon the Licensed Material 73 | and in which the Licensed Material is translated, altered, 74 | arranged, transformed, or otherwise modified in a manner requiring 75 | permission under the Copyright and Similar Rights held by the 76 | Licensor. For purposes of this Public License, where the Licensed 77 | Material is a musical work, performance, or sound recording, 78 | Adapted Material is always produced where the Licensed Material is 79 | synched in timed relation with a moving image. 80 | 81 | b. Adapter's License means the license You apply to Your Copyright 82 | and Similar Rights in Your contributions to Adapted Material in 83 | accordance with the terms and conditions of this Public License. 84 | 85 | c. Copyright and Similar Rights means copyright and/or similar rights 86 | closely related to copyright including, without limitation, 87 | performance, broadcast, sound recording, and Sui Generis Database 88 | Rights, without regard to how the rights are labeled or 89 | categorized. For purposes of this Public License, the rights 90 | specified in Section 2(b)(1)-(2) are not Copyright and Similar 91 | Rights. 92 | 93 | d. Effective Technological Measures means those measures that, in the 94 | absence of proper authority, may not be circumvented under laws 95 | fulfilling obligations under Article 11 of the WIPO Copyright 96 | Treaty adopted on December 20, 1996, and/or similar international 97 | agreements. 98 | 99 | e. Exceptions and Limitations means fair use, fair dealing, and/or 100 | any other exception or limitation to Copyright and Similar Rights 101 | that applies to Your use of the Licensed Material. 102 | 103 | f. Licensed Material means the artistic or literary work, database, 104 | or other material to which the Licensor applied this Public 105 | License. 106 | 107 | g. Licensed Rights means the rights granted to You subject to the 108 | terms and conditions of this Public License, which are limited to 109 | all Copyright and Similar Rights that apply to Your use of the 110 | Licensed Material and that the Licensor has authority to license. 111 | 112 | h. Licensor means the individual(s) or entity(ies) granting rights 113 | under this Public License. 114 | 115 | i. Share means to provide material to the public by any means or 116 | process that requires permission under the Licensed Rights, such 117 | as reproduction, public display, public performance, distribution, 118 | dissemination, communication, or importation, and to make material 119 | available to the public including in ways that members of the 120 | public may access the material from a place and at a time 121 | individually chosen by them. 122 | 123 | j. Sui Generis Database Rights means rights other than copyright 124 | resulting from Directive 96/9/EC of the European Parliament and of 125 | the Council of 11 March 1996 on the legal protection of databases, 126 | as amended and/or succeeded, as well as other essentially 127 | equivalent rights anywhere in the world. 128 | 129 | k. You means the individual or entity exercising the Licensed Rights 130 | under this Public License. Your has a corresponding meaning. 131 | 132 | 133 | Section 2 -- Scope. 134 | 135 | a. License grant. 136 | 137 | 1. Subject to the terms and conditions of this Public License, 138 | the Licensor hereby grants You a worldwide, royalty-free, 139 | non-sublicensable, non-exclusive, irrevocable license to 140 | exercise the Licensed Rights in the Licensed Material to: 141 | 142 | a. reproduce and Share the Licensed Material, in whole or 143 | in part; and 144 | 145 | b. produce, reproduce, and Share Adapted Material. 146 | 147 | 2. Exceptions and Limitations. For the avoidance of doubt, where 148 | Exceptions and Limitations apply to Your use, this Public 149 | License does not apply, and You do not need to comply with 150 | its terms and conditions. 151 | 152 | 3. Term. The term of this Public License is specified in Section 153 | 6(a). 154 | 155 | 4. Media and formats; technical modifications allowed. The 156 | Licensor authorizes You to exercise the Licensed Rights in 157 | all media and formats whether now known or hereafter created, 158 | and to make technical modifications necessary to do so. The 159 | Licensor waives and/or agrees not to assert any right or 160 | authority to forbid You from making technical modifications 161 | necessary to exercise the Licensed Rights, including 162 | technical modifications necessary to circumvent Effective 163 | Technological Measures. For purposes of this Public License, 164 | simply making modifications authorized by this Section 2(a) 165 | (4) never produces Adapted Material. 166 | 167 | 5. Downstream recipients. 168 | 169 | a. Offer from the Licensor -- Licensed Material. Every 170 | recipient of the Licensed Material automatically 171 | receives an offer from the Licensor to exercise the 172 | Licensed Rights under the terms and conditions of this 173 | Public License. 174 | 175 | b. No downstream restrictions. You may not offer or impose 176 | any additional or different terms or conditions on, or 177 | apply any Effective Technological Measures to, the 178 | Licensed Material if doing so restricts exercise of the 179 | Licensed Rights by any recipient of the Licensed 180 | Material. 181 | 182 | 6. No endorsement. Nothing in this Public License constitutes or 183 | may be construed as permission to assert or imply that You 184 | are, or that Your use of the Licensed Material is, connected 185 | with, or sponsored, endorsed, or granted official status by, 186 | the Licensor or others designated to receive attribution as 187 | provided in Section 3(a)(1)(A)(i). 188 | 189 | b. Other rights. 190 | 191 | 1. Moral rights, such as the right of integrity, are not 192 | licensed under this Public License, nor are publicity, 193 | privacy, and/or other similar personality rights; however, to 194 | the extent possible, the Licensor waives and/or agrees not to 195 | assert any such rights held by the Licensor to the limited 196 | extent necessary to allow You to exercise the Licensed 197 | Rights, but not otherwise. 198 | 199 | 2. Patent and trademark rights are not licensed under this 200 | Public License. 201 | 202 | 3. To the extent possible, the Licensor waives any right to 203 | collect royalties from You for the exercise of the Licensed 204 | Rights, whether directly or through a collecting society 205 | under any voluntary or waivable statutory or compulsory 206 | licensing scheme. In all other cases the Licensor expressly 207 | reserves any right to collect such royalties. 208 | 209 | 210 | Section 3 -- License Conditions. 211 | 212 | Your exercise of the Licensed Rights is expressly made subject to the 213 | following conditions. 214 | 215 | a. Attribution. 216 | 217 | 1. If You Share the Licensed Material (including in modified 218 | form), You must: 219 | 220 | a. retain the following if it is supplied by the Licensor 221 | with the Licensed Material: 222 | 223 | i. identification of the creator(s) of the Licensed 224 | Material and any others designated to receive 225 | attribution, in any reasonable manner requested by 226 | the Licensor (including by pseudonym if 227 | designated); 228 | 229 | ii. a copyright notice; 230 | 231 | iii. a notice that refers to this Public License; 232 | 233 | iv. a notice that refers to the disclaimer of 234 | warranties; 235 | 236 | v. a URI or hyperlink to the Licensed Material to the 237 | extent reasonably practicable; 238 | 239 | b. indicate if You modified the Licensed Material and 240 | retain an indication of any previous modifications; and 241 | 242 | c. indicate the Licensed Material is licensed under this 243 | Public License, and include the text of, or the URI or 244 | hyperlink to, this Public License. 245 | 246 | 2. You may satisfy the conditions in Section 3(a)(1) in any 247 | reasonable manner based on the medium, means, and context in 248 | which You Share the Licensed Material. For example, it may be 249 | reasonable to satisfy the conditions by providing a URI or 250 | hyperlink to a resource that includes the required 251 | information. 252 | 253 | 3. If requested by the Licensor, You must remove any of the 254 | information required by Section 3(a)(1)(A) to the extent 255 | reasonably practicable. 256 | 257 | 4. If You Share Adapted Material You produce, the Adapter's 258 | License You apply must not prevent recipients of the Adapted 259 | Material from complying with this Public License. 260 | 261 | 262 | Section 4 -- Sui Generis Database Rights. 263 | 264 | Where the Licensed Rights include Sui Generis Database Rights that 265 | apply to Your use of the Licensed Material: 266 | 267 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right 268 | to extract, reuse, reproduce, and Share all or a substantial 269 | portion of the contents of the database; 270 | 271 | b. if You include all or a substantial portion of the database 272 | contents in a database in which You have Sui Generis Database 273 | Rights, then the database in which You have Sui Generis Database 274 | Rights (but not its individual contents) is Adapted Material; and 275 | 276 | c. You must comply with the conditions in Section 3(a) if You Share 277 | all or a substantial portion of the contents of the database. 278 | 279 | For the avoidance of doubt, this Section 4 supplements and does not 280 | replace Your obligations under this Public License where the Licensed 281 | Rights include other Copyright and Similar Rights. 282 | 283 | 284 | Section 5 -- Disclaimer of Warranties and Limitation of Liability. 285 | 286 | a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE 287 | EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS 288 | AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF 289 | ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, 290 | IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, 291 | WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR 292 | PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, 293 | ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT 294 | KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT 295 | ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. 296 | 297 | b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE 298 | TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, 299 | NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, 300 | INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, 301 | COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR 302 | USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN 303 | ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR 304 | DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR 305 | IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. 306 | 307 | c. The disclaimer of warranties and limitation of liability provided 308 | above shall be interpreted in a manner that, to the extent 309 | possible, most closely approximates an absolute disclaimer and 310 | waiver of all liability. 311 | 312 | 313 | Section 6 -- Term and Termination. 314 | 315 | a. This Public License applies for the term of the Copyright and 316 | Similar Rights licensed here. However, if You fail to comply with 317 | this Public License, then Your rights under this Public License 318 | terminate automatically. 319 | 320 | b. Where Your right to use the Licensed Material has terminated under 321 | Section 6(a), it reinstates: 322 | 323 | 1. automatically as of the date the violation is cured, provided 324 | it is cured within 30 days of Your discovery of the 325 | violation; or 326 | 327 | 2. upon express reinstatement by the Licensor. 328 | 329 | For the avoidance of doubt, this Section 6(b) does not affect any 330 | right the Licensor may have to seek remedies for Your violations 331 | of this Public License. 332 | 333 | c. For the avoidance of doubt, the Licensor may also offer the 334 | Licensed Material under separate terms or conditions or stop 335 | distributing the Licensed Material at any time; however, doing so 336 | will not terminate this Public License. 337 | 338 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public 339 | License. 340 | 341 | 342 | Section 7 -- Other Terms and Conditions. 343 | 344 | a. The Licensor shall not be bound by any additional or different 345 | terms or conditions communicated by You unless expressly agreed. 346 | 347 | b. Any arrangements, understandings, or agreements regarding the 348 | Licensed Material not stated herein are separate from and 349 | independent of the terms and conditions of this Public License. 350 | 351 | 352 | Section 8 -- Interpretation. 353 | 354 | a. For the avoidance of doubt, this Public License does not, and 355 | shall not be interpreted to, reduce, limit, restrict, or impose 356 | conditions on any use of the Licensed Material that could lawfully 357 | be made without permission under this Public License. 358 | 359 | b. To the extent possible, if any provision of this Public License is 360 | deemed unenforceable, it shall be automatically reformed to the 361 | minimum extent necessary to make it enforceable. If the provision 362 | cannot be reformed, it shall be severed from this Public License 363 | without affecting the enforceability of the remaining terms and 364 | conditions. 365 | 366 | c. No term or condition of this Public License will be waived and no 367 | failure to comply consented to unless expressly agreed to by the 368 | Licensor. 369 | 370 | d. Nothing in this Public License constitutes or may be interpreted 371 | as a limitation upon, or waiver of, any privileges and immunities 372 | that apply to the Licensor or You, including from the legal 373 | processes of any jurisdiction or authority. 374 | 375 | 376 | ======================================================================= 377 | 378 | Creative Commons is not a party to its public 379 | licenses. Notwithstanding, Creative Commons may elect to apply one of 380 | its public licenses to material it publishes and in those instances 381 | will be considered the “Licensor.” The text of the Creative Commons 382 | public licenses is dedicated to the public domain under the CC0 Public 383 | Domain Dedication. Except for the limited purpose of indicating that 384 | material is shared under a Creative Commons public license or as 385 | otherwise permitted by the Creative Commons policies published at 386 | creativecommons.org/policies, Creative Commons does not authorize the 387 | use of the trademark "Creative Commons" or any other trademark or logo 388 | of Creative Commons without its prior written consent including, 389 | without limitation, in connection with any unauthorized modifications 390 | to any of its public licenses or any other arrangements, 391 | understandings, or agreements concerning use of licensed material. For 392 | the avoidance of doubt, this paragraph does not form part of the 393 | public licenses. 394 | 395 | Creative Commons may be contacted at creativecommons.org. 396 | --------------------------------------------------------------------------------