Skip to content

Router Vault-Unseal Role Design

Bead: selfhosted-cluster-zph Date: 2026-01-18

Create an Ansible role to manage the vault-unseal service on the Firewalla router, following established patterns from router-hosts and alloy roles.

The vault-unseal service runs on the router but was deployed manually:

  • Image: ghcr.io/lrstanley/vault-unseal:0.7.0
  • Config: /home/pi/.firewalla/run/docker/vault-unseal/vault-unseal.yaml
  • Issues:
    • tls_skip_verify: true (CA certs mounted but not used)
    • Unseal tokens hardcoded in config file
    • No Ansible management

Constraint: Unseal tokens cannot be stored in Vault (chicken-and-egg: needed to unseal Vault).

Approach: SOPS-encrypted file with age encryption. Tokens are stored encrypted in git and decrypted at playbook runtime using the community.sops collection.

Terminal window
# Setup (one-time)
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
# Add public key to .sops.yaml
# Create encrypted secrets
sops ansible/secrets/vault-unseal.sops.yaml

The role loads tokens automatically from ansible/secrets/vault-unseal.sops.yaml:

vault_unseal_tokens:
- "your-first-unseal-token"
- "your-second-unseal-token"
- "your-third-unseal-token"

Alternative: For backwards compatibility, tokens can still be provided via extra-vars:

Terminal window
ansible-playbook router-playbook.yml --tags vault-unseal \
-e '{"vault_unseal_tokens": ["token1", "token2", "token3"]}'

Tokens are written to the config file on the router with restricted permissions (0600).

Current problem: CA certs are mounted to /usr/local/share/ca-certificates/ but not registered.

Fix: Use SSL_CERT_FILE environment variable in the container, pointing to a mounted CA bundle. This follows Go’s standard TLS behavior without requiring custom entrypoints.

environment:
SSL_CERT_FILE: /etc/ssl/certs/fzymgc-ca-bundle.pem

Following the established pattern from alloy role:

ansible/roles/router-vault-unseal/
├── defaults/main.yml # Configurable defaults
├── handlers/main.yml # Restart handler
├── meta/main.yml # Role metadata
├── tasks/
│ ├── main.yml # Task orchestration
│ ├── directories.yml # Create /extdata structure
│ ├── configure.yml # Deploy config and certs
│ ├── docker.yml # Deploy compose file
│ └── boot-script.yml # post_main.d script
└── templates/
├── vault-unseal.yaml.j2
└── docker-compose.yml.j2
defaults/main.yml
vault_unseal_base_dir: "/extdata/vault-unseal"
vault_unseal_image: "ghcr.io/lrstanley/vault-unseal"
vault_unseal_version: "0.7.0"
vault_unseal_nodes:
- "https://vault-0.fzymgc.house"
- "https://vault-1.fzymgc.house"
- "https://vault-2.fzymgc.house"
vault_unseal_check_interval: "15s"
vault_unseal_max_check_interval: "30m"
# CA bundle for TLS verification
vault_unseal_ca_cert: "{{ playbook_dir }}/files/fzymgc-ca-bundle.pem"
# DNS server for container
vault_unseal_dns_server: "192.168.20.1"
# Firewalla paths
vault_unseal_docker_compose_dir: "/home/pi/.firewalla/run/docker/vault-unseal"
vault_unseal_post_main_dir: "/home/pi/.firewalla/config/post_main.d"
# Loaded from SOPS file (preferred) or provided via extra-vars
vault_unseal_tokens: [] # List of unseal tokens
# SOPS file path (default)
vault_unseal_sops_file: "{{ playbook_dir }}/secrets/vault-unseal.sops.yaml"

Add to existing router-playbook.yml:

- name: Deploy vault-unseal
hosts: router
roles:
- role: router-vault-unseal
tags: [vault-unseal]
  1. Tokens encrypted in git: SOPS with age encryption allows secure storage in repository
  2. Config file permissions: 0600, owned by pi user on router
  3. TLS verification: Enabled with proper CA chain via SSL_CERT_FILE
  4. Token distribution: Per vault-unseal best practice, distribute tokens across multiple instances (current setup has all 3 tokens on one host - acceptable for home lab)
  5. Age key management: Private key must be protected; store in ~/.config/sops/age/keys.txt
0200-start-vault-unseal.sh
#!/bin/bash
cd /home/pi/.firewalla/run/docker/vault-unseal
/usr/bin/docker compose up -d

Numbered 0200 to start after Docker (0050) but early in boot sequence since Vault may need unsealing.

  1. Create role directory structure
  2. Write defaults/main.yml with configurable values
  3. Write templates for config and compose files
  4. Write task files following alloy pattern
  5. Add role to router-playbook.yml
  6. Test with --check --diff first
  7. Deploy and verify TLS works (no tls_skip_verify)

After deployment:

Terminal window
# Check container is running
ssh pi@192.168.20.1 "docker ps | grep vault-unseal"
# Check logs for successful TLS connection
ssh pi@192.168.20.1 "docker logs vault-unseal --tail 20"
# Verify no tls_skip_verify in config
ssh pi@192.168.20.1 "grep tls_skip /extdata/vault-unseal/vault-unseal.yaml"

If issues occur:

Terminal window
# Stop the managed service
ssh pi@192.168.20.1 "cd /home/pi/.firewalla/run/docker/vault-unseal && docker compose down"
# Restore manual config if needed (backup before running role)