Skip to content

Instantly share code, notes, and snippets.

@vchatela
Created October 28, 2025 13:47
Show Gist options
  • Select an option

  • Save vchatela/3c987b01a085c88941b91bc79a1b74a4 to your computer and use it in GitHub Desktop.

Select an option

Save vchatela/3c987b01a085c88941b91bc79a1b74a4 to your computer and use it in GitHub Desktop.
Going secretless with Terraform & Vault

Overview

This guide demonstrates how to migrate from storing Vault tokens in terraform.tfvars to using GitHub OIDC (OpenID Connect) for secure, secretless authentication in CI/CD pipelines.

Benefits:

  • πŸ” Secretless Authentication: No more storing long-lived tokens in files or secrets
  • 🎯 Least Privilege: Granular policies scoped to specific paths and operations
  • πŸ”„ Automatic Rotation: Short-lived tokens (1 hour TTL) generated per workflow run
  • 🎭 Role-Based Access: Different permissions for PRs vs production deployments
  • πŸ“Š Better Audit Trail: GitHub actor tied to Vault authentication

Before: Token-Based Authentication

Previous Setup Issues

terraform.tfvars:

vault_token = "hvs.CAExxxxxxxxxxxU"

provider.tf:

provider "vault" {
  address = "https://vault.k3s.internal.mydomain.fr/"
  token   = var.vault_token
}

Vault Policy:

# Root-like access
path "*" {
  capabilities = ["create", "update", "read", "delete", "list", "sudo"]
}

Problems:

  • Long-lived, high privileged and static tokens
  • Root-level access to all Vault paths
  • Token rotation requires manual updates and reminders
  • Difficult to track who performed actions

After: GitHub OIDC Authentication

1. Vault JWT Authentication Setup

# Enable JWT auth method
vault auth enable jwt

# Configure GitHub OIDC
vault write auth/jwt/config \
    bound_issuer="https://token.actions.githubusercontent.com" \
    oidc_discovery_url="https://token.actions.githubusercontent.com"

# Create role for main branch (production)
vault write auth/jwt/role/terraform-github-actions \
    role_type="jwt" \
    bound_audiences="https://vault.k3s.internal.mydomain.fr" \
    bound_claims='{"repository":"vchatela/homelab"}' \
    bound_subject="repo:vchatela/homelab:ref:refs/heads/main" \
    user_claim="actor" \
    policies="terraform-admin" \
    ttl="1h"

# Create role for pull requests (read-only plan)
vault write auth/jwt/role/terraform-github-actions-pr \
    role_type="jwt" \
    bound_audiences="https://vault.k3s.internal.mydomain.fr" \
    bound_claims='{"repository":"vchatela/homelab"}' \
    user_claim="actor" \
    policies="terraform-admin" \
    ttl="1h"

2. Least Privilege Vault Policy

# Allow full access to secrets stored under secret/
path "secret/data/*" {
  capabilities = ["create", "update", "read", "delete", "list"]
}

# Allow read access to secret metadata (KV v2)
path "secret/metadata/*" {
  capabilities = ["create", "update", "read", "delete", "list"]
}

# Allow management of Kubernetes auth backend
path "auth/kubernetes/config" {
  capabilities = ["create", "update", "read", "delete"]
}

path "auth/kubernetes/role/*" {
  capabilities = ["create", "update", "read", "delete", "list"]
}

# Allow creating/updating policies for apps
path "sys/policies/acl/*" {
  capabilities = ["create", "update", "read", "delete", "list"]
}

# Allow managing auth backends
path "sys/auth/kubernetes" {
  capabilities = ["create", "update", "read", "delete"]
}

path "sys/auth/kubernetes/*" {
  capabilities = ["create", "update", "read", "delete"]
}

# Allow reading auth methods
path "sys/auth" {
  capabilities = ["read", "list"]
}

# Allow reading mounts (including auth mounts)
path "sys/mounts" {
  capabilities = ["read", "list"]
}

path "sys/mounts/*" {
  capabilities = ["read", "list"]
}

# Allow token creation (required by Vault provider)
path "auth/token/create" {
  capabilities = ["create", "update"]
}

# Allow token lookup (for self)
path "auth/token/lookup-self" {
  capabilities = ["read"]
}

# Allow token renewal (for self)
path "auth/token/renew-self" {
  capabilities = ["update"]
}

Apply the policy:

vault policy write terraform-admin /tmp/terraform-admin-policy.hcl

3. Terraform Provider Configuration

variables.tf:

variable "vault_jwt_role" {
  description = "Vault JWT role for authentication"
  type        = string
  default     = "terraform-github-actions"
}

variable "vault_jwt_token" {
  description = "JWT token from GitHub OIDC"
  type        = string
  sensitive   = true
}

provider.tf:

provider "vault" {
  address = "https://vault.k3s.internal.mydomain.fr/"
  
  auth_login_jwt {
    role = var.vault_jwt_role
    jwt  = var.vault_jwt_token
  }
}

4. GitHub Actions Workflow

name: Terraform Deployment

on:
  workflow_dispatch:
    inputs:
      action:
        type: choice
        options: [plan, apply]
  pull_request:
    paths: ['terraform/**']
  push:
    branches: [main, 'feature/**']
    paths: ['terraform/**']

permissions:
  id-token: write      # Required for OIDC token
  contents: read
  pull-requests: write

jobs:
  terraform:
    runs-on: arc-runner-set
    steps:
      - uses: actions/checkout@v5
      
      - name: Get GitHub OIDC Token
        id: oidc
        run: |
          OIDC_TOKEN=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
            "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://vault.k3s.internal.mydomain.fr" | jq -r '.value')
          echo "::add-mask::$OIDC_TOKEN"
          echo "token=$OIDC_TOKEN" >> $GITHUB_OUTPUT

      - name: Terraform Plan
        if: github.event_name == 'pull_request'
        env:
          TF_VAR_vault_jwt_token: ${{ steps.oidc.outputs.token }}
          TF_VAR_vault_jwt_role: terraform-github-actions-pr
        run: terraform plan

      - name: Terraform Apply
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
        env:
          TF_VAR_vault_jwt_token: ${{ steps.oidc.outputs.token }}
          TF_VAR_vault_jwt_role: terraform-github-actions
        run: terraform apply -auto-approve

Security Improvements

Before vs After Comparison

Aspect Before (Token) After (OIDC)
Authentication Long-lived static token Short-lived OIDC token (1h)
Storage In terraform.tfvars or GitHub secrets Generated on-demand
Permissions Root access to all paths Scoped to necessary paths only
Rotation Manual Automatic per workflow run
Audit Trail Token ID GitHub actor + repository
Compromise Risk High (full access if leaked) Low (limited scope, short TTL)
Environment Separation Same token everywhere Different roles per environment

Key Security Benefits

  1. No Stored Secrets: OIDC tokens are generated dynamically - nothing to leak
  2. Principle of Least Privilege: Policy grants only what Terraform needs
  3. Time-Limited Access: 1-hour TTL means compromised tokens expire quickly
  4. Audit Traceability: Vault logs show which GitHub user/workflow accessed what
  5. Bound Claims: Tokens only valid for specific repository and branches
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment