Files
gh-basher83-lunar-claude-pl…/skills/ansible-best-practices/patterns/secrets-management.md
2025-11-29 18:00:24 +08:00

13 KiB

Secrets Management with Infisical

Overview

This repository uses Infisical for centralized secrets management in Ansible playbooks. This pattern eliminates hard-coded credentials and provides audit trails for secret access.

Architecture

┌──────────────┐
│   Ansible    │
│   Playbook   │
└──────┬───────┘
       │
       │ include_tasks: infisical-secret-lookup.yml
       │
       ▼
┌──────────────────┐
│ Infisical Lookup │
│      Task        │
└──────┬───────────┘
       │
       ├─> Try Universal Auth (preferred)
       │   - INFISICAL_UNIVERSAL_AUTH_CLIENT_ID
       │   - INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET
       │
       ├─> Fallback to Environment Variable (optional)
       │   - Uses specified fallback_env_var
       │
       ▼
┌──────────────┐
│  Infisical   │ (Vault)
│     API      │
└──────────────┘

Reusable Task Pattern

The Infisical Lookup Task

Location: ansible/tasks/infisical-secret-lookup.yml

Purpose: Reusable task for secure secret retrieval with validation and fallback.

Key Features:

  1. Validates input parameters - Ensures secret_name and secret_var_name are provided
  2. Checks authentication - Validates Universal Auth credentials or fallback
  3. Retrieves secret - Fetches from Infisical with project/env/path context
  4. Validates retrieval - Ensures secret was actually retrieved
  5. Uses no_log - Prevents secrets from appearing in logs
  6. Supports fallback - Can fall back to environment variables

Usage Pattern

Basic usage:

- name: Retrieve Proxmox password
  ansible.builtin.include_tasks: tasks/infisical-secret-lookup.yml
  vars:
    secret_name: 'PROXMOX_PASSWORD'
    secret_var_name: 'proxmox_password'
    infisical_project_id: '7b832220-24c0-45bc-a5f1-ce9794a31259'
    infisical_env: 'prod'
    infisical_path: '/doggos-cluster'

# Now use the secret
- name: Create Proxmox user
  community.proxmox.proxmox_user:
    api_password: "{{ proxmox_password }}"
    # ... other config ...
  no_log: true

With fallback to environment variable:

- name: Retrieve database password
  ansible.builtin.include_tasks: tasks/infisical-secret-lookup.yml
  vars:
    secret_name: 'DB_PASSWORD'
    secret_var_name: 'db_password'
    fallback_env_var: 'DB_PASSWORD'  # Falls back to $DB_PASSWORD if Infisical fails
    infisical_project_id: '7b832220-24c0-45bc-a5f1-ce9794a31259'
    infisical_env: 'prod'
    infisical_path: '/database'

Allow empty values (optional):

- name: Retrieve optional API key
  ansible.builtin.include_tasks: tasks/infisical-secret-lookup.yml
  vars:
    secret_name: 'OPTIONAL_API_KEY'
    secret_var_name: 'api_key'
    allow_empty: true  # Won't fail if secret is empty

Required Variables

Task Parameters

Variable Required Default Description
secret_name Yes - Name of secret in Infisical
secret_var_name Yes - Variable name to store retrieved secret
infisical_project_id No 7b832220-... Infisical project ID
infisical_env No prod Environment slug (prod, dev, staging)
infisical_path No /apollo-13/vault Path within Infisical project
fallback_env_var No - Environment variable to use as fallback
allow_empty No false Whether to allow empty secret values

Environment Variables

Universal Auth (Preferred):

export INFISICAL_UNIVERSAL_AUTH_CLIENT_ID="your-client-id"
export INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET="your-client-secret"

Fallback (Optional):

export PROXMOX_PASSWORD="fallback-password"

Authentication Methods

Setup:

  1. Create service account in Infisical
  2. Generate Universal Auth credentials
  3. Set environment variables

Usage:

export INFISICAL_UNIVERSAL_AUTH_CLIENT_ID="ua-abc123"
export INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET="secret-xyz789"

cd ansible
uv run ansible-playbook playbooks/my-playbook.yml

Fallback to Environment Variables

When to use:

  • Local development
  • CI/CD pipelines without Infisical access
  • Emergency fallback

Usage:

- name: Get API token
  ansible.builtin.include_tasks: tasks/infisical-secret-lookup.yml
  vars:
    secret_name: 'API_TOKEN'
    secret_var_name: 'api_token'
    fallback_env_var: 'API_TOKEN'  # Falls back to $API_TOKEN

Real-World Examples

Example 1: Proxmox Template Creation

From: ansible/playbooks/proxmox-build-template.yml

---
- name: Build Proxmox VM template
  hosts: proxmox_nodes
  gather_facts: false

  vars:
    infisical_project_id: '7b832220-24c0-45bc-a5f1-ce9794a31259'
    infisical_env: 'prod'
    infisical_path: '/doggos-cluster'

  tasks:
    - name: Retrieve Proxmox credentials
      ansible.builtin.include_tasks: tasks/infisical-secret-lookup.yml
      vars:
        secret_name: 'PROXMOX_PASSWORD'
        secret_var_name: 'proxmox_password'
        fallback_env_var: 'PROXMOX_PASSWORD'

    - name: Download cloud image
      ansible.builtin.get_url:
        url: "{{ cloud_image_url }}"
        dest: "/tmp/{{ image_name }}"
        checksum: "{{ cloud_image_checksum }}"
      # ... rest of playbook ...

Example 2: Terraform User Creation

From: ansible/playbooks/proxmox-create-terraform-user.yml

---
- name: Create Terraform service user in Proxmox
  hosts: proxmox_nodes
  become: true

  vars:
    infisical_project_id: '7b832220-24c0-45bc-a5f1-ce9794a31259'
    infisical_env: 'prod'
    infisical_path: '/doggos-cluster'

  tasks:
    - name: Retrieve Proxmox API credentials
      ansible.builtin.include_tasks: tasks/infisical-secret-lookup.yml
      vars:
        secret_name: 'PROXMOX_ROOT_PASSWORD'
        secret_var_name: 'proxmox_root_password'

    - name: Create system user
      ansible.builtin.user:
        name: terraform
        comment: "Terraform automation user"
        shell: /bin/bash
        state: present
      no_log: true

    - name: Create Proxmox API token
      ansible.builtin.command: >
        pveum user token add terraform@pam terraform-token
      register: token_result
      changed_when: "'already exists' not in token_result.stderr"
      failed_when:
        - token_result.rc != 0
        - "'already exists' not in token_result.stderr"
      no_log: true

Example 3: Multiple Secrets

---
- name: Deploy application with multiple secrets
  hosts: app_servers
  become: true

  vars:
    infisical_project_id: '7b832220-24c0-45bc-a5f1-ce9794a31259'
    infisical_env: 'prod'
    infisical_path: '/app-config'

  tasks:
    - name: Retrieve database password
      ansible.builtin.include_tasks: tasks/infisical-secret-lookup.yml
      vars:
        secret_name: 'DB_PASSWORD'
        secret_var_name: 'db_password'

    - name: Retrieve API key
      ansible.builtin.include_tasks: tasks/infisical-secret-lookup.yml
      vars:
        secret_name: 'API_KEY'
        secret_var_name: 'api_key'

    - name: Retrieve Redis password
      ansible.builtin.include_tasks: tasks/infisical-secret-lookup.yml
      vars:
        secret_name: 'REDIS_PASSWORD'
        secret_var_name: 'redis_password'

    - name: Deploy application config
      ansible.builtin.template:
        src: app-config.j2
        dest: /etc/app/config.yml
        owner: app
        group: app
        mode: '0600'
      vars:
        database_url: "postgres://user:{{ db_password }}@db.example.com/app"
        api_key: "{{ api_key }}"
        redis_url: "redis://:{{ redis_password }}@redis.example.com:6379"
      no_log: true

Security Best Practices

1. Always Use no_log

On secret retrieval:

- name: Get secret
  ansible.builtin.include_tasks: tasks/infisical-secret-lookup.yml
  vars:
    secret_name: 'PASSWORD'
    secret_var_name: 'password'
  # no_log: true (already in included task)

On tasks using secrets:

- name: Use secret in command
  ansible.builtin.command: create-user --password {{ password }}
  no_log: true  # CRITICAL: Prevents password in logs

2. Never Hard-Code Secrets

Bad:

- name: Create user
  community.proxmox.proxmox_user:
    api_password: "my-password-123"  # DON'T DO THIS!

Good:

- name: Retrieve password
  ansible.builtin.include_tasks: tasks/infisical-secret-lookup.yml
  vars:
    secret_name: 'PROXMOX_PASSWORD'
    secret_var_name: 'proxmox_password'

- name: Create user
  community.proxmox.proxmox_user:
    api_password: "{{ proxmox_password }}"
  no_log: true

3. Validate Secret Retrieval

The reusable task automatically validates secrets, but you can add additional checks:

- name: Get secret
  ansible.builtin.include_tasks: tasks/infisical-secret-lookup.yml
  vars:
    secret_name: 'DB_PASSWORD'
    secret_var_name: 'db_password'

- name: Validate password format
  ansible.builtin.assert:
    that:
      - db_password | length >= 16
      - db_password is regex('^[A-Za-z0-9!@#$%^&*()]+$')
    fail_msg: "Password doesn't meet complexity requirements"
  no_log: true

4. Use Project/Environment Isolation

Separate secrets by environment:

# Production
- name: Get prod secret
  ansible.builtin.include_tasks: tasks/infisical-secret-lookup.yml
  vars:
    secret_name: 'DB_PASSWORD'
    secret_var_name: 'db_password'
    infisical_env: 'prod'
    infisical_path: '/production/database'

# Development
- name: Get dev secret
  ansible.builtin.include_tasks: tasks/infisical-secret-lookup.yml
  vars:
    secret_name: 'DB_PASSWORD'
    secret_var_name: 'db_password'
    infisical_env: 'dev'
    infisical_path: '/development/database'

5. Limit Secret Scope

Only retrieve secrets when needed, not at playbook start:

Good:

- name: System tasks (no secrets needed)
  ansible.builtin.apt:
    name: nginx
    state: present

# Only retrieve secret when needed
- name: Get credentials
  ansible.builtin.include_tasks: tasks/infisical-secret-lookup.yml
  vars:
    secret_name: 'DB_PASSWORD'
    secret_var_name: 'db_password'

- name: Configure database connection
  ansible.builtin.template:
    src: db-config.j2
    dest: /etc/app/db.yml
  no_log: true

Troubleshooting

Error: Missing Infisical authentication credentials

Cause: Universal Auth environment variables not set

Solution:

export INFISICAL_UNIVERSAL_AUTH_CLIENT_ID="ua-abc123"
export INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET="secret-xyz789"

Error: Failed to retrieve secret from Infisical

Possible causes:

  1. Secret doesn't exist in specified path
  2. Wrong project_id/env/path
  3. Insufficient permissions

Debug:

- name: Debug secret retrieval
  ansible.builtin.include_tasks: tasks/infisical-secret-lookup.yml
  vars:
    secret_name: 'TEST_SECRET'
    secret_var_name: 'test_secret'
    infisical_project_id: '7b832220-24c0-45bc-a5f1-ce9794a31259'
    infisical_env: 'prod'
    infisical_path: '/test'
  # Check Infisical UI to verify secret exists at this path

Error: Secret validation failed (empty value)

Cause: Secret retrieved but value is empty

Solutions:

# Option 1: Allow empty values
- name: Get optional secret
  ansible.builtin.include_tasks: tasks/infisical-secret-lookup.yml
  vars:
    secret_name: 'OPTIONAL_KEY'
    secret_var_name: 'optional_key'
    allow_empty: true

# Option 2: Use fallback
- name: Get secret with fallback
  ansible.builtin.include_tasks: tasks/infisical-secret-lookup.yml
  vars:
    secret_name: 'API_KEY'
    secret_var_name: 'api_key'
    fallback_env_var: 'DEFAULT_API_KEY'

CI/CD Integration

GitHub Actions

name: Deploy with Infisical
on: push

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Infisical credentials
        env:
          INFISICAL_CLIENT_ID: ${{ secrets.INFISICAL_CLIENT_ID }}
          INFISICAL_CLIENT_SECRET: ${{ secrets.INFISICAL_CLIENT_SECRET }}
        run: |
          echo "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID=$INFISICAL_CLIENT_ID" >> $GITHUB_ENV
          echo "INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET=$INFISICAL_CLIENT_SECRET" >> $GITHUB_ENV

      - name: Run Ansible playbook
        run: |
          cd ansible
          uv run ansible-playbook playbooks/deploy.yml

GitLab CI

deploy:
  stage: deploy
  variables:
    INFISICAL_UNIVERSAL_AUTH_CLIENT_ID: $INFISICAL_CLIENT_ID
    INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET: $INFISICAL_CLIENT_SECRET
  script:
    - cd ansible
    - uv run ansible-playbook playbooks/deploy.yml

Further Reading