Router Vault-Unseal Role Design
Bead: selfhosted-cluster-zph Date: 2026-01-18
Overview
Section titled “Overview”Create an Ansible role to manage the vault-unseal service on the Firewalla router, following established patterns from router-hosts and alloy roles.
Current State
Section titled “Current State”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
Design
Section titled “Design”Unseal Token Handling
Section titled “Unseal Token Handling”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.
# Setup (one-time)mkdir -p ~/.config/sops/ageage-keygen -o ~/.config/sops/age/keys.txt# Add public key to .sops.yaml
# Create encrypted secretssops ansible/secrets/vault-unseal.sops.yamlThe 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:
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).
TLS Configuration
Section titled “TLS Configuration”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.pemRole Structure
Section titled “Role Structure”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.j2Configuration Defaults
Section titled “Configuration Defaults”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 verificationvault_unseal_ca_cert: "{{ playbook_dir }}/files/fzymgc-ca-bundle.pem"
# DNS server for containervault_unseal_dns_server: "192.168.20.1"
# Firewalla pathsvault_unseal_docker_compose_dir: "/home/pi/.firewalla/run/docker/vault-unseal"vault_unseal_post_main_dir: "/home/pi/.firewalla/config/post_main.d"Required Variables
Section titled “Required Variables”# Loaded from SOPS file (preferred) or provided via extra-varsvault_unseal_tokens: [] # List of unseal tokens
# SOPS file path (default)vault_unseal_sops_file: "{{ playbook_dir }}/secrets/vault-unseal.sops.yaml"Playbook Integration
Section titled “Playbook Integration”Add to existing router-playbook.yml:
- name: Deploy vault-unseal hosts: router roles: - role: router-vault-unseal tags: [vault-unseal]Security Considerations
Section titled “Security Considerations”- Tokens encrypted in git: SOPS with age encryption allows secure storage in repository
- Config file permissions: 0600, owned by pi user on router
- TLS verification: Enabled with proper CA chain via SSL_CERT_FILE
- 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)
- Age key management: Private key must be protected; store in
~/.config/sops/age/keys.txt
Boot Script
Section titled “Boot Script”#!/bin/bashcd /home/pi/.firewalla/run/docker/vault-unseal/usr/bin/docker compose up -dNumbered 0200 to start after Docker (0050) but early in boot sequence since Vault may need unsealing.
Implementation Tasks
Section titled “Implementation Tasks”- Create role directory structure
- Write defaults/main.yml with configurable values
- Write templates for config and compose files
- Write task files following alloy pattern
- Add role to router-playbook.yml
- Test with
--check --difffirst - Deploy and verify TLS works (no
tls_skip_verify)
Verification
Section titled “Verification”After deployment:
# Check container is runningssh pi@192.168.20.1 "docker ps | grep vault-unseal"
# Check logs for successful TLS connectionssh pi@192.168.20.1 "docker logs vault-unseal --tail 20"
# Verify no tls_skip_verify in configssh pi@192.168.20.1 "grep tls_skip /extdata/vault-unseal/vault-unseal.yaml"Rollback
Section titled “Rollback”If issues occur:
# Stop the managed servicessh 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)