Managing Secrets with Ansible Vault
Previous: Docker Build Deployment
Applications running on your Raspberry Pi will need sensitive data like API keys, database passwords, and authentication tokens. Storing these secrets securely is crucial - you don't want them in plain text or committed to git! In this guide, we'll use Ansible Vault to encrypt secrets and inject them into your Pi for Docker containers to use.
Github Repository
All the Ansible vault configuration and example secrets templates from this guide are available in https://github.com/IaC-Toolbox/iac-toolbox-raspberrypi. Feel free to clone it and follow along!
Why Secrets Management Matters
Let's be clear about why this is important:
Security: Plain text secrets in configuration files are a disaster waiting to happen. Anyone with access to your repository or file system can see them.
Version Control: You want to track your infrastructure configuration in git, but you absolutely don't want secrets committed to your repository.
Automation: For CI/CD to work, your Docker containers need access to secrets without manual intervention.
The Strategy
We're going to:
- Store secrets in Ansible Vault (encrypted)
- Use Ansible to inject secrets into
/etc/raspberrypi.envon your Pi - Reference that file when running Docker containers
Step 1: Create a Local .env File
First, create a .env file locally with your secrets. This stays on your machine and is never committed to git:
# .env (keep this local!)
OPENAI_API_KEY=sk-your-key-here
DATABASE_URL=postgres://user:pass@host:5432/db
API_SECRET=your-secret-hereImportant: Add .env to your .gitignore right now!
echo ".env" >> .gitignore
echo ".vault_pass.txt" >> .gitignoreStep 2: Create Vault Template
Create secrets.yml.j2 template that reads from environment variables:
# secrets.yml.j2
---
openai_api_key: "{{ lookup('env', 'OPENAI_API_KEY') }}"
database_url: "{{ lookup('env', 'DATABASE_URL') }}"
api_secret: "{{ lookup('env', 'API_SECRET') }}"Step 3: Create Vault Seed Playbook
Create playbooks/seed_vault.yml to generate and encrypt the secrets file:
# playbooks/seed_vault.yml
- hosts: localhost
gather_facts: false
tasks:
- name: Create secrets file from environment variables
template:
src: ../secrets.yml.j2
dest: ../secrets.yml
- name: Encrypt the secrets file with Ansible Vault
ansible.builtin.command:
cmd: ansible-vault encrypt ../secrets.yml --vault-password-file ../.vault_pass.txt
register: encrypt_result
failed_when:
- encrypt_result.rc != 0
- "'already encrypted' not in encrypt_result.stderr"Step 4: Generate Vault Password
Create a vault password file:
# Generate a random password
openssl rand -base64 32 > .vault_pass.txt
chmod 600 .vault_pass.txtThis file is used to encrypt/decrypt your secrets. Keep it safe and never commit it!
Step 5: Seed the Vault
Load your environment variables and create the encrypted vault:
# Load environment variables from .env
export $(grep -v '^#' .env | xargs)
# Run the seed playbook
ansible-playbook ./playbooks/seed_vault.ymlThis creates an encrypted secrets.yml file that you can safely commit to git.
Step 6: Create Secrets Role
Create roles/secrets/tasks/main.yml to inject secrets into your Pi:
# roles/secrets/tasks/main.yml
- name: Create environment file for Raspberry Pi applications
copy:
dest: /etc/raspberrypi.env
content: |
OPENAI_API_KEY={{ openai_api_key }}
DATABASE_URL={{ database_url }}
API_SECRET={{ api_secret }}
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: "0600"
no_log: trueThe no_log: true prevents secrets from appearing in Ansible output.
Step 7: Update Main Playbook
Update playbooks/playbook.yml to use the secrets:
# playbooks/playbook.yml
- name: Setup Raspberry Pi
hosts: all
become: true
vars_files:
- ../secrets.yml
roles:
- setup
- docker
- secrets
- github-runnerStep 8: Run the Playbook
Apply the configuration with vault password:
ansible-playbook -i inventory/all.yml playbooks/playbook.yml --vault-password-file .vault_pass.txtYour secrets are now on your Pi at /etc/raspberrypi.env!
Using Secrets in Docker Containers
Reference the secrets file when running containers:
docker run -d \
--env-file /etc/raspberrypi.env \
--name my-app \
-p 4000:4000 \
my-username/my-app:latestOr in your GitHub Actions workflow:
- name: Pull and run container
run: |
TAG=${{ needs.build_and_push.outputs.tag }}
docker run -d \
--env-file /etc/raspberrypi.env \
--name backend-api \
-p 4000:4000 \
${{ secrets.DOCKER_USERNAME }}/backend-api:$TAGUpdating Secrets
When you need to update secrets:
- Update your local
.envfile - Delete the old encrypted file:
rm secrets.yml - Re-run the seed playbook:
export $(grep -v '^#' .env | xargs) ansible-playbook ./playbooks/seed_vault.yml - Re-run the main playbook to update the Pi
Viewing Encrypted Secrets
To view what's in your encrypted vault:
ansible-vault view secrets.yml --vault-password-file .vault_pass.txtTo edit secrets directly:
ansible-vault edit secrets.yml --vault-password-file .vault_pass.txtSecurity Best Practices
Never commit these files:
.env- your local secrets.vault_pass.txt- your vault password- Any unencrypted secrets files
Always commit:
secrets.yml- your encrypted vault (this is safe!)secrets.yml.j2- the template (no secrets here)
File permissions:
chmod 600 .env
chmod 600 .vault_pass.txt
chmod 600 /etc/raspberrypi.env # on the PiTroubleshooting
"Decryption failed" error?
- Verify your vault password file path
- Ensure the vault was encrypted with the same password
Secrets not appearing in containers?
- SSH into your Pi and check:
cat /etc/raspberrypi.env - Verify file permissions allow your user to read it
- Check Docker logs:
docker logs my-app
Environment variable not loading?
- Ensure your
.envfile usesKEY=valueformat (no spaces around =) - Check the template references the correct variable names
Security Limitations: Why This Approach Isn't Truly Secure
While Ansible Vault is a significant improvement over plain text secrets in git, it's important to understand its limitations:
The Core Problem: Secrets End Up in Plain Text on Disk
Once the Ansible playbook runs, your secrets are written to /etc/raspberrypi.env in plain text. This means:
Anyone with file system access can read them:
# Any user or process with file permissions can do this:
cat /etc/raspberrypi.env
# Result: All your secrets are visibleDocker containers read from this plain text file:
docker run --env-file /etc/raspberrypi.env myapp
# The env file is just plain text sitting on diskNo access control or audit trail:
- You can't track who accessed which secrets
- You can't revoke access to secrets without redeploying
- You can't rotate individual secrets without editing files
Process memory exposure:
- Any process can inspect environment variables of running containers
/proc/<pid>/environexposes all secrets to root users
No encryption at rest:
- While Ansible Vault encrypts secrets in git, they're unencrypted on the Pi
- If someone gains SSH access, they get all your secrets
- A compromised backup of
/etc/raspberrypi.envleaks everything
When This Approach is "Good Enough"
Despite these limitations, this approach works for many scenarios:
- ✅ Personal projects - You're the only one with access
- ✅ Learning and prototyping - Security isn't the primary concern
- ✅ Small teams with high trust - Everyone already has full access
- ✅ Low-risk applications - The secrets aren't protecting critical data
When You Should Upgrade
Consider a production-grade secrets manager like HashiCorp Vault when:
- ❌ Multiple people need different access levels - Not everyone should see everything
- ❌ Compliance requirements - You need audit trails and access controls
- ❌ High-value secrets - Protecting production databases, payment APIs, etc.
- ❌ Automated secret rotation - You want to rotate credentials regularly
- ❌ Dynamic secrets - Generate temporary credentials on-demand
Next Steps: Moving to Production-Grade Secrets Management
If the security limitations above concern you (and for production systems, they should!), continue to the next guide where we'll set up HashiCorp Vault:
👉 Managing Secrets with HashiCorp Vault - Enterprise-grade secrets management that keeps secrets encrypted at rest, provides audit logging, and never writes plain text to disk.
The Vault guide builds on what you've learned here but addresses all the security limitations with:
- Secrets fetched on-demand via API (no plain text files)
- Encryption at rest and in transit
- Granular access controls and audit logging
- Web UI for easy management
- Still automated via Ansible for easy deployment
Or, if you're comfortable with the current approach, continue with setting up your GitHub Actions runner to use these secrets.
Previous: Docker Build Deployment | Next: Managing Secrets with Vault