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:
- Validates input parameters - Ensures secret_name and secret_var_name are provided
- Checks authentication - Validates Universal Auth credentials or fallback
- Retrieves secret - Fetches from Infisical with project/env/path context
- Validates retrieval - Ensures secret was actually retrieved
- Uses
no_log- Prevents secrets from appearing in logs - 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
Universal Auth (Recommended)
Setup:
- Create service account in Infisical
- Generate Universal Auth credentials
- 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:
- Secret doesn't exist in specified path
- Wrong project_id/env/path
- 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