Establish clean homelab infrastructure baseline
lint / yaml (push) Has been cancelled
lint / ansible (push) Has been cancelled
lint / terraform (push) Has been cancelled

Reorganize the brownfield repository, remove retired and generated artifacts, harden ignore rules, and record the GitOps/IaC redesign.
This commit is contained in:
2026-09-09 16:47:20 +00:00
commit 88a02ababa
418 changed files with 50579 additions and 0 deletions
@@ -0,0 +1,49 @@
---
# openbao role defaults — override in group_vars/host_vars.
# Secrets (openbao_transit_token) MUST come from an Ansible Vault file, not here.
# --- Release ---
openbao_version: "2.6.1"
openbao_download_checksum: "" # "sha256:..." REQUIRED — set in group_vars
openbao_download_url: "https://github.com/openbao/openbao/releases/download/v{{ openbao_version }}/openbao_{{ openbao_version }}_linux_amd64.tar.gz"
# --- Service account ---
openbao_user: "openbao"
openbao_group: "openbao"
# --- Paths ---
openbao_bin: "/usr/local/bin/bao"
openbao_config_dir: "/etc/openbao"
openbao_tls_dir: "/etc/openbao/tls"
openbao_data_dir: "/opt/openbao/data" # integrated Raft storage
# --- Identity / addresses ---
openbao_fqdn: "bao.example.com"
openbao_lan_ip: "10.10.10.10"
openbao_api_port: 8200
openbao_cluster_port: 8201
openbao_ui: true
openbao_node_id: "{{ ansible_facts['hostname'] }}"
# Listener binds all interfaces; lock it down with the firewall vars below.
openbao_listen_address: "0.0.0.0:{{ openbao_api_port }}"
openbao_api_addr: "https://{{ openbao_lan_ip }}:{{ openbao_api_port }}"
openbao_cluster_addr: "https://{{ openbao_lan_ip }}:{{ openbao_cluster_port }}"
# SANs for the bootstrap self-signed listener cert (replace with PKI-issued later).
openbao_tls_sans:
- "DNS:{{ openbao_fqdn }}"
- "IP:{{ openbao_lan_ip }}"
- "IP:127.0.0.1"
# --- Auto-unseal (transit) — off by default (Shamir manual unseal on first init) ---
openbao_auto_unseal: false
openbao_transit_address: ""
openbao_transit_token: ""
openbao_transit_key_name: "autounseal"
openbao_transit_mount_path: "transit/"
# --- Optional host firewall ---
openbao_manage_firewall: false
openbao_allowed_cidrs:
- "100.64.0.0/10"
@@ -0,0 +1,11 @@
---
- name: reload systemd
ansible.builtin.systemd:
daemon_reload: true
# NOTE: restarting a running, Shamir-sealed node re-seals it (manual unseal needed).
# Harmless before first init; automatic re-unseal when transit auto-unseal is on.
- name: restart openbao
ansible.builtin.systemd:
name: openbao
state: restarted
@@ -0,0 +1,11 @@
---
galaxy_info:
role_name: openbao
description: Deploy OpenBao (secrets management) with integrated Raft storage + TLS listener, under systemd.
min_ansible_version: "2.15"
platforms:
- name: Debian
versions: [bookworm]
- name: Ubuntu
versions: [jammy, noble]
dependencies: []
@@ -0,0 +1,138 @@
---
# Deploy OpenBao as a systemd service with integrated Raft storage + a TLS listener.
# This role ONLY installs and runs the daemon. It does NOT initialize/unseal or enable
# any secrets engines/auth methods — that is the separate bootstrap play.
#
# NOTE: restarting a running, Shamir-sealed node re-seals it (needs manual unseal).
# On first deploy the node is uninitialized, so the restart handler is harmless; with
# transit auto-unseal a restart re-unseals automatically.
- name: Assert required variables are set
ansible.builtin.assert:
that:
- openbao_version | length > 0
- openbao_download_checksum | length > 0
- openbao_fqdn | length > 0
- openbao_lan_ip | length > 0
- (not openbao_auto_unseal) or (openbao_transit_address | length > 0 and openbao_transit_token | length > 0)
fail_msg: >-
Set openbao_version + openbao_download_checksum (sha256:...), openbao_fqdn and
openbao_lan_ip. If openbao_auto_unseal is true you must also provide
openbao_transit_address and openbao_transit_token (from vault).
- name: Install runtime dependencies
ansible.builtin.apt:
name:
- tar
- python3-cryptography # community.crypto (bootstrap TLS cert)
state: present
update_cache: true
# --- Service account + directories --------------------------------------------
- name: Create openbao group
ansible.builtin.group:
name: "{{ openbao_group }}"
system: true
- name: Create openbao system user
ansible.builtin.user:
name: "{{ openbao_user }}"
group: "{{ openbao_group }}"
system: true
shell: /usr/sbin/nologin
home: "{{ openbao_data_dir }}"
create_home: false
- name: Create config, tls and data directories
ansible.builtin.file:
path: "{{ item.path }}"
state: directory
owner: "{{ item.owner }}"
group: "{{ openbao_group }}"
mode: "{{ item.mode }}"
loop:
- { path: "{{ openbao_config_dir }}", owner: "root", mode: "0755" }
- { path: "{{ openbao_tls_dir }}", owner: "root", mode: "0750" }
- { path: "{{ openbao_data_dir }}", owner: "{{ openbao_user }}", mode: "0700" }
# --- Install binary (checksum-verified release tarball) ------------------------
- name: Check installed bao version
ansible.builtin.command: "{{ openbao_bin }} version"
register: bao_installed
changed_when: false
failed_when: false
- name: Install OpenBao when missing or version mismatch
when: openbao_version not in (bao_installed.stdout | default(''))
block:
- name: Download OpenBao release tarball (checksum-verified)
ansible.builtin.get_url:
url: "{{ openbao_download_url }}"
dest: "/tmp/openbao_{{ openbao_version }}.tar.gz"
checksum: "{{ openbao_download_checksum }}"
mode: "0644"
- name: Create staging directory for extraction
ansible.builtin.file:
path: "/tmp/openbao_{{ openbao_version }}"
state: directory
mode: "0755"
- name: Extract tarball
ansible.builtin.unarchive:
src: "/tmp/openbao_{{ openbao_version }}.tar.gz"
dest: "/tmp/openbao_{{ openbao_version }}"
remote_src: true
- name: Install bao binary
ansible.builtin.copy:
src: "/tmp/openbao_{{ openbao_version }}/bao"
dest: "{{ openbao_bin }}"
remote_src: true
owner: root
group: root
mode: "0755"
notify: restart openbao
# --- Bootstrap TLS listener cert (replace with PKI-issued later) ---------------
- name: Generate bootstrap TLS certificate
ansible.builtin.import_tasks: tls.yml
# --- Config + service ----------------------------------------------------------
- name: Write OpenBao config
ansible.builtin.template:
src: config.hcl.j2
dest: "{{ openbao_config_dir }}/config.hcl"
owner: root
group: "{{ openbao_group }}"
mode: "0640"
no_log: "{{ openbao_auto_unseal }}" # config carries the transit token when auto-unseal is on
notify: restart openbao
- name: Install systemd unit
ansible.builtin.template:
src: openbao.service.j2
dest: /etc/systemd/system/openbao.service
owner: root
group: root
mode: "0644"
notify:
- reload systemd
- restart openbao
- name: Enable and start OpenBao
ansible.builtin.systemd:
name: openbao
state: started
enabled: true
daemon_reload: true
# --- Optional host firewall ----------------------------------------------------
- name: Restrict API/cluster ports to tailnet + cluster CIDRs
community.general.ufw:
rule: allow
port: "{{ item.0 }}"
proto: tcp
from_ip: "{{ item.1 }}"
loop: "{{ [openbao_api_port, openbao_cluster_port] | product(openbao_allowed_cidrs) | list }}"
when: openbao_manage_firewall | bool
@@ -0,0 +1,40 @@
---
# Bootstrap TLS for the OpenBao listener: a self-signed server certificate with SANs
# (FQDN + LAN IP + loopback), generated ONCE to get the listener up. It is never
# regenerated once a cert exists — so the ACME (or PKI) cert that replaces it later is
# never clobbered on a re-run. Delete cert.pem to force a fresh self-signed bootstrap.
- name: Check for an existing listener certificate
ansible.builtin.stat:
path: "{{ openbao_tls_dir }}/cert.pem"
register: openbao_listener_cert
- name: OpenBao server private key
community.crypto.openssl_privatekey:
path: "{{ openbao_tls_dir }}/key.pem"
size: 4096
owner: "{{ openbao_user }}"
group: "{{ openbao_group }}"
mode: "0640"
when: not openbao_listener_cert.stat.exists
- name: OpenBao server CSR (FQDN + SANs)
community.crypto.openssl_csr:
path: "{{ openbao_tls_dir }}/server.csr"
privatekey_path: "{{ openbao_tls_dir }}/key.pem"
common_name: "{{ openbao_fqdn }}"
subject_alt_name: "{{ openbao_tls_sans }}"
when: not openbao_listener_cert.stat.exists
- name: OpenBao server certificate (self-signed, positive serial)
community.crypto.x509_certificate:
path: "{{ openbao_tls_dir }}/cert.pem"
csr_path: "{{ openbao_tls_dir }}/server.csr"
privatekey_path: "{{ openbao_tls_dir }}/key.pem"
provider: selfsigned
selfsigned_not_after: "+825d"
owner: "{{ openbao_user }}"
group: "{{ openbao_group }}"
mode: "0644"
when: not openbao_listener_cert.stat.exists
notify: restart openbao
@@ -0,0 +1,26 @@
---
# Smoke tests — run via the `verify` tag:
# ansible-playbook provision-openbao.yml --tags verify
# Confirms the daemon is up and the API answers. A sealed/uninitialized node is the
# EXPECTED state before you run `bao operator init`, so that is treated as success.
- name: OpenBao service is active
ansible.builtin.systemd:
name: openbao
register: bao_svc
changed_when: false
failed_when: bao_svc.status.ActiveState != "active"
- name: API listener answers (sealed/uninitialized is OK)
ansible.builtin.command: "{{ openbao_bin }} status -address=https://127.0.0.1:{{ openbao_api_port }}"
environment:
BAO_SKIP_VERIFY: "true"
register: bao_status
changed_when: false
failed_when: bao_status.rc not in [0, 2] # 0 = unsealed, 2 = sealed/uninitialized
- name: Report
ansible.builtin.debug:
msg: >-
OpenBao {{ openbao_version }} is running and answering on :{{ openbao_api_port }}
(status rc={{ bao_status.rc }}; 2 = sealed/uninitialized, expected before init).
@@ -0,0 +1,27 @@
# {{ ansible_managed }} — openbao role. Do not edit by hand.
ui = {{ openbao_ui | lower }}
storage "raft" {
path = "{{ openbao_data_dir }}"
node_id = "{{ openbao_node_id }}"
}
listener "tcp" {
address = "{{ openbao_listen_address }}"
tls_cert_file = "{{ openbao_tls_dir }}/cert.pem"
tls_key_file = "{{ openbao_tls_dir }}/key.pem"
tls_min_version = "tls12"
}
api_addr = "{{ openbao_api_addr }}"
cluster_addr = "{{ openbao_cluster_addr }}"
{% if openbao_auto_unseal %}
# Auto-unseal via a Transit engine on a separate OpenBao/Vault instance.
seal "transit" {
address = "{{ openbao_transit_address }}"
token = "{{ openbao_transit_token }}"
key_name = "{{ openbao_transit_key_name }}"
mount_path = "{{ openbao_transit_mount_path }}"
}
{% endif %}
@@ -0,0 +1,31 @@
# {{ ansible_managed }} — openbao role. Do not edit by hand.
[Unit]
Description=OpenBao secrets management
Documentation=https://openbao.org/docs/
Requires=network-online.target
After=network-online.target
ConditionFileNotEmpty={{ openbao_config_dir }}/config.hcl
[Service]
User={{ openbao_user }}
Group={{ openbao_group }}
ProtectSystem=full
ProtectHome=read-only
PrivateTmp=yes
PrivateDevices=yes
SecureBits=keep-caps
AmbientCapabilities=CAP_IPC_LOCK
CapabilityBoundingSet=CAP_SYSLOG CAP_IPC_LOCK
NoNewPrivileges=yes
ExecStart={{ openbao_bin }} server -config={{ openbao_config_dir }}/config.hcl
ExecReload=/bin/kill --signal HUP $MAINPID
KillMode=process
KillSignal=SIGINT
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
LimitNOFILE=65536
LimitMEMLOCK=infinity
[Install]
WantedBy=multi-user.target