Back to Blog

Managing Secrets with Ansible Vault

Viktor Vasylkovskyi

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:

  1. Store secrets in Ansible Vault (encrypted)
  2. Use Ansible to inject secrets into /etc/raspberrypi.env on your Pi
  3. 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-here

Important: Add .env to your .gitignore right now!

echo ".env" >> .gitignore
echo ".vault_pass.txt" >> .gitignore

Step 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.txt

This 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.yml

This 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: true

The 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-runner

Step 8: Run the Playbook

Apply the configuration with vault password:

ansible-playbook -i inventory/all.yml playbooks/playbook.yml --vault-password-file .vault_pass.txt

Your 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:latest

Or 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:$TAG

Updating Secrets

When you need to update secrets:

  1. Update your local .env file
  2. Delete the old encrypted file: rm secrets.yml
  3. Re-run the seed playbook:
    export $(grep -v '^#' .env | xargs)
    ansible-playbook ./playbooks/seed_vault.yml
  4. 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.txt

To edit secrets directly:

ansible-vault edit secrets.yml --vault-password-file .vault_pass.txt

Security 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 Pi

Troubleshooting

"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 .env file uses KEY=value format (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 visible

Docker containers read from this plain text file:

docker run --env-file /etc/raspberrypi.env myapp
# The env file is just plain text sitting on disk

No 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>/environ exposes 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.env leaks 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