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

513 lines
13 KiB
Markdown

# 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
```text
┌──────────────┐
│ 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:**
```yaml
- 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:**
```yaml
- 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):**
```yaml
- 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):**
```bash
export INFISICAL_UNIVERSAL_AUTH_CLIENT_ID="your-client-id"
export INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET="your-client-secret"
```
**Fallback (Optional):**
```bash
export PROXMOX_PASSWORD="fallback-password"
```
## Authentication Methods
### Universal Auth (Recommended)
**Setup:**
1. Create service account in Infisical
2. Generate Universal Auth credentials
3. Set environment variables
**Usage:**
```bash
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:**
```yaml
- 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`
```yaml
---
- 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`
```yaml
---
- 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
```yaml
---
- 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:**
```yaml
- 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:**
```yaml
- 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:**
```yaml
- name: Create user
community.proxmox.proxmox_user:
api_password: "my-password-123" # DON'T DO THIS!
```
**✅ Good:**
```yaml
- 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:
```yaml
- 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:**
```yaml
# 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:**
```yaml
- 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:**
```bash
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:**
```yaml
- 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:**
```yaml
# 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
```yaml
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
```yaml
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
- [Infisical Documentation](https://infisical.com/docs)
- [Infisical Ansible Collection](https://github.com/Infisical/ansible-collection)
- [Ansible no_log Documentation](https://docs.ansible.com/ansible/latest/reference_appendices/logging.html)