513 lines
13 KiB
Markdown
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)
|