Establish clean homelab infrastructure baseline
Reorganize the brownfield repository, remove retired and generated artifacts, harden ignore rules, and record the GitOps/IaC redesign.
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user