Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fbfb0ed03 | ||
|
|
88c80ae21a | ||
|
|
12d11f8c50 | ||
|
|
355b046abf | ||
|
|
b4933d65e5 | ||
|
|
3187be1f62 | ||
|
|
0c1fb0a229 | ||
|
|
373b682902 | ||
|
|
aad723252a |
@@ -1,33 +0,0 @@
|
||||
when:
|
||||
# Deploy only on main (merge) + manual — never auto-deploy from a branch.
|
||||
- event: push
|
||||
branch: main
|
||||
path:
|
||||
- "ansible/roles/claude-workstation/**"
|
||||
- "ansible/plays/install-claude-workstation.yml"
|
||||
- ".woodpecker/deploy-claude-workstation.yml"
|
||||
- event: deployment
|
||||
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "deploy-claude-workstation"'
|
||||
|
||||
timeout: 30
|
||||
|
||||
steps:
|
||||
- name: deploy-claude-workstation
|
||||
image: gitea.renner.uno/gitea_admin/ci-ansible:2026-04-29
|
||||
environment:
|
||||
ANSIBLE_FORCE_COLOR: "true"
|
||||
ANSIBLE_HOST_KEY_CHECKING: "false"
|
||||
ANSIBLE_ROLES_PATH: "../roles:./roles"
|
||||
SSH_PRIVATE_KEY:
|
||||
from_secret: ssh_private_key
|
||||
VAULT_PASS:
|
||||
from_secret: vault_pass
|
||||
commands:
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_ed25519
|
||||
- chmod 600 ~/.ssh/id_ed25519
|
||||
- printf "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n" > ~/.ssh/config
|
||||
- cd ansible
|
||||
- echo "$VAULT_PASS" > .vault_pass && chmod 600 .vault_pass
|
||||
- export ANSIBLE_VAULT_PASSWORD_FILE=.vault_pass
|
||||
- ansible-playbook -i inventory/ plays/install-claude-workstation.yml -v
|
||||
@@ -1,36 +0,0 @@
|
||||
when:
|
||||
# Deploy only on main (merge) + manual — never auto-deploy from a branch.
|
||||
- event: push
|
||||
branch: main
|
||||
path:
|
||||
- "ansible/roles/mqtt-bridge/**"
|
||||
- "ansible/plays/edge-mqtt-bridge.yml"
|
||||
- "ansible/inventory/group_vars/edge_host/**"
|
||||
- ".woodpecker/deploy-edge-mqtt-bridge.yml"
|
||||
- event: deployment
|
||||
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "deploy-edge-mqtt-bridge"'
|
||||
|
||||
timeout: 30
|
||||
|
||||
steps:
|
||||
- name: deploy-edge-mqtt-bridge
|
||||
image: gitea.renner.uno/gitea_admin/ci-ansible:2026-04-29
|
||||
environment:
|
||||
ANSIBLE_FORCE_COLOR: "true"
|
||||
ANSIBLE_HOST_KEY_CHECKING: "false"
|
||||
ANSIBLE_ROLES_PATH: "../roles:./roles"
|
||||
SSH_PRIVATE_KEY:
|
||||
from_secret: ssh_private_key
|
||||
VAULT_PASS:
|
||||
from_secret: vault_pass
|
||||
commands:
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_ed25519
|
||||
- chmod 600 ~/.ssh/id_ed25519
|
||||
- printf "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n" > ~/.ssh/config
|
||||
- cd ansible
|
||||
- echo "$VAULT_PASS" > .vault_pass && chmod 600 .vault_pass
|
||||
- export ANSIBLE_VAULT_PASSWORD_FILE=.vault_pass
|
||||
# Local MQTT bridge on the edge hosts only (REN-1307); restarts only the
|
||||
# local mosquitto on edge-1, never the cluster broker.
|
||||
- ansible-playbook -i inventory/ plays/edge-mqtt-bridge.yml -v
|
||||
@@ -109,8 +109,6 @@ steps:
|
||||
from_secret: hetzner_s3_access_key
|
||||
TF_VAR_hetzner_s3_secret_key:
|
||||
from_secret: hetzner_s3_secret_key
|
||||
SSH_PRIVATE_KEY:
|
||||
from_secret: ssh_private_key
|
||||
PROXMOX_TOKEN_ID:
|
||||
from_secret: proxmox_token_id
|
||||
PROXMOX_TOKEN_SECRET:
|
||||
@@ -121,11 +119,6 @@ steps:
|
||||
ANSIBLE_ROLES_PATH: "../roles:./roles"
|
||||
ANSIBLE_FORCE_COLOR: "true"
|
||||
commands:
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_ed25519
|
||||
- chmod 600 ~/.ssh/id_ed25519
|
||||
- export TF_VAR_ssh_private_key="$(cat ~/.ssh/id_ed25519)"
|
||||
- printf "Host *\n\tStrictHostKeyChecking no\n" > ~/.ssh/config
|
||||
- echo "$VAULT_PASS" > /tmp/.vault_pass && chmod 600 /tmp/.vault_pass
|
||||
- export ANSIBLE_VAULT_PASSWORD_FILE=/tmp/.vault_pass
|
||||
- tofu init -upgrade -backend-config="access_key=$AWS_ACCESS_KEY_ID" -backend-config="secret_key=$AWS_SECRET_ACCESS_KEY"
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
when:
|
||||
# Deploy only on main (merge) + manual — never auto-deploy from a branch.
|
||||
- event: push
|
||||
branch: main
|
||||
path:
|
||||
- "ansible/roles/netbird-client/**"
|
||||
- "ansible/plays/install-netbird-client.yml"
|
||||
- "ansible/inventory/group_vars/claude_workstation/**"
|
||||
- ".woodpecker/deploy-netbird-client.yml"
|
||||
- event: deployment
|
||||
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "deploy-netbird-client"'
|
||||
|
||||
timeout: 30
|
||||
|
||||
steps:
|
||||
- name: deploy-netbird-client
|
||||
image: gitea.renner.uno/gitea_admin/ci-ansible:2026-04-29
|
||||
environment:
|
||||
ANSIBLE_FORCE_COLOR: "true"
|
||||
ANSIBLE_HOST_KEY_CHECKING: "false"
|
||||
ANSIBLE_ROLES_PATH: "../roles:./roles"
|
||||
SSH_PRIVATE_KEY:
|
||||
from_secret: ssh_private_key
|
||||
VAULT_PASS:
|
||||
from_secret: vault_pass
|
||||
commands:
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_ed25519
|
||||
- chmod 600 ~/.ssh/id_ed25519
|
||||
- printf "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n" > ~/.ssh/config
|
||||
- cd ansible
|
||||
- echo "$VAULT_PASS" > .vault_pass && chmod 600 .vault_pass
|
||||
- export ANSIBLE_VAULT_PASSWORD_FILE=.vault_pass
|
||||
# Enrol only the off-site Claude workstation into the Netbird mesh.
|
||||
- ansible-playbook -i inventory/ plays/install-netbird-client.yml --limit claude_workstation -v
|
||||
@@ -1,54 +0,0 @@
|
||||
when:
|
||||
# Deploy only on main (merge) + manual — never auto-deploy from a branch.
|
||||
- event: push
|
||||
branch: main
|
||||
path:
|
||||
- "ansible/roles/netbird-proxy/**"
|
||||
# install-netbird-proxy.yml also applies the netbird-proxy-watchdog role,
|
||||
# so changes to it must redeploy this host too.
|
||||
- "ansible/roles/netbird-proxy-watchdog/**"
|
||||
# install-netbird-proxy.yml also applies the edge bouncer.
|
||||
- "ansible/roles/crowdsec-edge-bouncer/**"
|
||||
- "ansible/plays/install-netbird-proxy.yml"
|
||||
# The variables that steer those roles, not just the roles themselves.
|
||||
# Without these two, a change that only touches inventory never deploys
|
||||
# and nothing says so: arming the bouncer lives in host_vars, and its
|
||||
# LAPI key lives in group_vars, so both would sit in git looking applied
|
||||
# while the hosts kept running the previous configuration.
|
||||
#
|
||||
# pve_hosts.yml is deliberately NOT listed. It is the single inventory
|
||||
# file for the whole fleet, so listing it would redeploy the proxies for
|
||||
# any unrelated host edit. The cost is that a change to [netbird_proxy]
|
||||
# group membership needs a manual deployment run.
|
||||
- "ansible/inventory/host_vars/proxy-*.yml"
|
||||
- "ansible/inventory/group_vars/netbird_proxy/**"
|
||||
- ".woodpecker/deploy-netbird-proxy.yml"
|
||||
- event: deployment
|
||||
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "deploy-netbird-proxy"'
|
||||
|
||||
timeout: 30
|
||||
|
||||
steps:
|
||||
- name: deploy-netbird-proxy
|
||||
image: gitea.renner.uno/gitea_admin/ci-ansible:2026-04-29
|
||||
environment:
|
||||
ANSIBLE_FORCE_COLOR: "true"
|
||||
ANSIBLE_HOST_KEY_CHECKING: "false"
|
||||
ANSIBLE_ROLES_PATH: "../roles:./roles"
|
||||
SSH_PRIVATE_KEY:
|
||||
from_secret: ssh_private_key
|
||||
VAULT_PASS:
|
||||
from_secret: vault_pass
|
||||
PROXMOX_TOKEN_ID:
|
||||
from_secret: proxmox_token_id
|
||||
PROXMOX_TOKEN_SECRET:
|
||||
from_secret: proxmox_token_secret
|
||||
commands:
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_ed25519
|
||||
- chmod 600 ~/.ssh/id_ed25519
|
||||
- printf "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n" > ~/.ssh/config
|
||||
- cd ansible
|
||||
- echo "$VAULT_PASS" > .vault_pass && chmod 600 .vault_pass
|
||||
- export ANSIBLE_VAULT_PASSWORD_FILE=.vault_pass
|
||||
- ansible-playbook -i inventory/ plays/install-netbird-proxy.yml -v
|
||||
@@ -1,37 +0,0 @@
|
||||
when:
|
||||
# Deploy only on main (merge) + manual — never auto-deploy from a branch.
|
||||
- event: push
|
||||
branch: main
|
||||
path:
|
||||
- "ansible/roles/netbird-server/**"
|
||||
- "ansible/plays/install-netbird-server.yml"
|
||||
- ".woodpecker/deploy-netbird-server.yml"
|
||||
- event: deployment
|
||||
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "deploy-netbird-server"'
|
||||
|
||||
timeout: 30
|
||||
|
||||
steps:
|
||||
- name: deploy-netbird-server
|
||||
image: gitea.renner.uno/gitea_admin/ci-ansible:2026-04-29
|
||||
environment:
|
||||
ANSIBLE_FORCE_COLOR: "true"
|
||||
ANSIBLE_HOST_KEY_CHECKING: "false"
|
||||
ANSIBLE_ROLES_PATH: "../roles:./roles"
|
||||
SSH_PRIVATE_KEY:
|
||||
from_secret: ssh_private_key
|
||||
VAULT_PASS:
|
||||
from_secret: vault_pass
|
||||
PROXMOX_TOKEN_ID:
|
||||
from_secret: proxmox_token_id
|
||||
PROXMOX_TOKEN_SECRET:
|
||||
from_secret: proxmox_token_secret
|
||||
commands:
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_ed25519
|
||||
- chmod 600 ~/.ssh/id_ed25519
|
||||
- printf "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n" > ~/.ssh/config
|
||||
- cd ansible
|
||||
- echo "$VAULT_PASS" > .vault_pass && chmod 600 .vault_pass
|
||||
- export ANSIBLE_VAULT_PASSWORD_FILE=.vault_pass
|
||||
- ansible-playbook -i inventory/ plays/install-netbird-server.yml -v
|
||||
@@ -1,37 +0,0 @@
|
||||
when:
|
||||
# Deploy only on main (merge) + manual — never auto-deploy from a branch.
|
||||
- event: push
|
||||
branch: main
|
||||
path:
|
||||
- "ansible/roles/ssh-ca/**"
|
||||
- "ansible/plays/configure-ssh-ca.yml"
|
||||
- "ansible/inventory/pve_hosts.yml"
|
||||
- "ansible/inventory/group_vars/all/**"
|
||||
- "ansible/inventory/group_vars/netbird_proxy/vars.yml"
|
||||
- "ansible/inventory/host_vars/netbird.renner.uno.yml"
|
||||
- ".woodpecker/deploy-ssh-ca.yml"
|
||||
- event: deployment
|
||||
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "deploy-ssh-ca"'
|
||||
|
||||
timeout: 30
|
||||
|
||||
steps:
|
||||
- name: deploy-ssh-ca
|
||||
image: gitea.renner.uno/gitea_admin/ci-ansible:2026-04-29
|
||||
environment:
|
||||
ANSIBLE_FORCE_COLOR: "true"
|
||||
ANSIBLE_HOST_KEY_CHECKING: "false"
|
||||
ANSIBLE_ROLES_PATH: "../roles:./roles"
|
||||
SSH_PRIVATE_KEY:
|
||||
from_secret: ssh_private_key
|
||||
VAULT_PASS:
|
||||
from_secret: vault_pass
|
||||
commands:
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_ed25519
|
||||
- chmod 600 ~/.ssh/id_ed25519
|
||||
- printf "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n" > ~/.ssh/config
|
||||
- cd ansible
|
||||
- echo "$VAULT_PASS" > .vault_pass && chmod 600 .vault_pass
|
||||
- export ANSIBLE_VAULT_PASSWORD_FILE=.vault_pass
|
||||
- ansible-playbook -i inventory/ plays/configure-ssh-ca.yml -v
|
||||
@@ -1,13 +1,4 @@
|
||||
when:
|
||||
# Deploy only on main (merge) + manual — never auto-deploy from a branch.
|
||||
- event: push
|
||||
branch: main
|
||||
path:
|
||||
- "ansible/roles/uptime-kuma/**"
|
||||
- "ansible/plays/install-uptime-kuma.yml"
|
||||
- ".woodpecker/deploy-uptime-kuma.yml"
|
||||
- event: deployment
|
||||
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "deploy-uptime-kuma"'
|
||||
- event: cron
|
||||
cron: uptime-kuma-token-renew
|
||||
|
||||
@@ -21,27 +12,3 @@ steps:
|
||||
from_secret: openbao_uptime_kuma_token
|
||||
commands:
|
||||
- 'curl -sSf -X POST -H "X-Vault-Token: $VAULT_TOKEN" https://openbao.renner.uno/v1/auth/token/renew-self -o /dev/null'
|
||||
|
||||
- name: deploy-uptime-kuma
|
||||
image: gitea.renner.uno/gitea_admin/ci-ansible:2026-10-18
|
||||
when:
|
||||
- event: [push, deployment]
|
||||
environment:
|
||||
ANSIBLE_FORCE_COLOR: "true"
|
||||
ANSIBLE_HOST_KEY_CHECKING: "false"
|
||||
ANSIBLE_ROLES_PATH: "../roles:./roles"
|
||||
SSH_PRIVATE_KEY:
|
||||
from_secret: ssh_private_key
|
||||
VAULT_PASS:
|
||||
from_secret: vault_pass
|
||||
VAULT_TOKEN:
|
||||
from_secret: openbao_uptime_kuma_token
|
||||
commands:
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_ed25519
|
||||
- chmod 600 ~/.ssh/id_ed25519
|
||||
- printf "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n" > ~/.ssh/config
|
||||
- cd ansible
|
||||
- echo "$VAULT_PASS" > .vault_pass && chmod 600 .vault_pass
|
||||
- export ANSIBLE_VAULT_PASSWORD_FILE=.vault_pass
|
||||
- ansible-playbook -i inventory/ plays/install-uptime-kuma.yml -v
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
when:
|
||||
# Manual only: reboots edge-1 (Z-Wave, heating CAN, sole NTP server) and the
|
||||
# NetBird control plane. Run inside the 02:00-05:00 Europe/Berlin window.
|
||||
- event: deployment
|
||||
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "update-os"'
|
||||
|
||||
timeout: 120
|
||||
|
||||
steps:
|
||||
- name: update-os
|
||||
image: gitea.renner.uno/gitea_admin/ci-ansible:2026-04-29
|
||||
environment:
|
||||
ANSIBLE_FORCE_COLOR: "true"
|
||||
ANSIBLE_HOST_KEY_CHECKING: "false"
|
||||
ANSIBLE_ROLES_PATH: "../roles:./roles"
|
||||
SSH_PRIVATE_KEY:
|
||||
from_secret: ssh_private_key
|
||||
VAULT_PASS:
|
||||
from_secret: vault_pass
|
||||
commands:
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_ed25519
|
||||
- chmod 600 ~/.ssh/id_ed25519
|
||||
- printf "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n" > ~/.ssh/config
|
||||
- cd ansible
|
||||
- echo "$VAULT_PASS" > .vault_pass && chmod 600 .vault_pass
|
||||
- export ANSIBLE_VAULT_PASSWORD_FILE=.vault_pass
|
||||
- ansible-playbook -i inventory/ plays/update-os.yml -v
|
||||
@@ -245,8 +245,8 @@ its own server key, nothing else. On a push it writes
|
||||
`/etc/mosquitto/bridge.d/edge-1.conf`, deletes `bridge.d/legacy.conf` and
|
||||
restarts the local mosquitto. The controller sets a new password whenever the
|
||||
bridge reports "not connected" (also after a reinstall), so no play run is
|
||||
needed for a rotation. `.woodpecker/deploy-edge-mqtt-bridge.yml` runs
|
||||
`plays/edge-mqtt-bridge.yml` on merge. Diagnosis: `journalctl -u
|
||||
needed for a rotation. `plays/edge-mqtt-bridge.yml` is run by hand from a
|
||||
workstation. Diagnosis: `journalctl -u
|
||||
edge-mqtt-cred`, and `mosquitto_sub -t '$SYS/broker/connection/edge-1/state'`
|
||||
(retained `1` = connected).
|
||||
|
||||
|
||||
@@ -5,37 +5,10 @@ pve_token_id: "ansible"
|
||||
ansible_user: "ladmin"
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
|
||||
# The only plain keys the admin logins accept on [ssh_keys_managed] hosts
|
||||
# The only plain key the admin logins accept on [ssh_keys_managed] hosts
|
||||
# (ssh-ca role, tasks/authorized_keys.yml). People log in with OpenBao SSH CA
|
||||
# certificates; these two are for when that path is unavailable.
|
||||
# certificates; this one is for when that path is unavailable.
|
||||
# breakglass-neo: PIV key (slot 9a, PIN) on the offline YubiKey NEO.
|
||||
# ssh -I /usr/lib/opensc-pkcs11.so root@<host>
|
||||
# woodpecker-ci: the CI deploy key, from ssh_ci_key_from where set.
|
||||
ssh_admin_authorized_keys:
|
||||
- key: "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKvSl9LUP8u3S2Sq5YTWpW0Jxbue8cJhD7/LMFQbEsa3xo145HVR7pBzRxW71MUy+jgrgmpSlDhIb606Hswhvoo= breakglass-neo-6532348"
|
||||
- key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICyNnX1I4W9jyDvCJ1DJauHSmbo1ZXSyBxVhSAwMWgen woodpecker-ci@homelab"
|
||||
options: "{{ ('from=\"' ~ ssh_ci_key_from ~ '\"') if ssh_ci_key_from else '' }}"
|
||||
|
||||
# Source the CI key is accepted from. LAN hosts see the pod address; hosts on
|
||||
# the internet see the home WAN address (NAT, dynamic) and set this empty in
|
||||
# their group/host vars.
|
||||
ssh_ci_key_from: "{{ ci_ssh_from_cidrs }}"
|
||||
|
||||
# Network-of-origin restriction for the woodpecker-ci@homelab CI key
|
||||
# (issue #184). Used as the authorized_keys `from=` option so the CI key only
|
||||
# authenticates from within the cluster's pod network — NOT from any
|
||||
# host-pinned IP.
|
||||
#
|
||||
# The in-cluster Woodpecker step pods SSH to fleet hosts using their POD IP as
|
||||
# source, not a node IP: Cilium runs `ipv4-native-routing-cidr=10.0.0.0/8` +
|
||||
# `ipv6-native-routing-cidr=fd00::/8` with masquerade enabled, so pod egress to
|
||||
# any 10.x / fd.. LAN destination is NOT SNAT'd, and each node's PodCIDR is
|
||||
# advertised to the UDM over BGP (kubernetes/infrastructure/post-crds/
|
||||
# cilium-bgp.yaml) so return traffic L3-routes back to the pod. Hosts therefore
|
||||
# see the real pod source IP.
|
||||
#
|
||||
# The cluster pod subnets (kubernetes/talos/patches/cluster.yaml podSubnets)
|
||||
# are the stable boundary — they survive node DHCP-lease churn and pod
|
||||
# rescheduling across k8s-1/2/3, unlike pinning any node's current IP.
|
||||
# v6 is included because some SSH targets (edge IoT hosts) are IPv6-only.
|
||||
ci_ssh_from_cidrs: '10.244.0.0/16,fd10:244::/48'
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
---
|
||||
# CI reaches these internet hosts NATed from the home WAN address, never from
|
||||
# the pod network, so the CI key carries no from= restriction here.
|
||||
ssh_ci_key_from: ""
|
||||
|
||||
# The host NetBird client (wt0) must not take over DNS: resolv.conf is pinned to
|
||||
# the systemd-resolved stub by the netbird-proxy role.
|
||||
netbird_disable_dns: true
|
||||
|
||||
@@ -2,7 +2,3 @@
|
||||
# NetBird server - VPS not managed by Proxmox
|
||||
# Enable Docker monitoring (cAdvisor) for container metrics
|
||||
docker_monitoring_enabled: true
|
||||
|
||||
# CI reaches this internet host NATed from the home WAN address, never from
|
||||
# the pod network, so the CI key carries no from= restriction here.
|
||||
ssh_ci_key_from: ""
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
# Their hosts no longer resolve, except the three that resolve to Talos nodes.
|
||||
|
||||
[netbird_server]
|
||||
netbird.renner.uno ansible_user=root
|
||||
netbird.renner.uno ansible_host=netbird.nb.renner.uno ansible_user=root
|
||||
|
||||
[netbird_proxy]
|
||||
proxy-1.renner.uno ansible_user=root
|
||||
proxy-2.renner.uno ansible_user=root
|
||||
proxy-1.renner.uno ansible_host=proxy-1.nb.renner.uno ansible_user=root
|
||||
proxy-2.renner.uno ansible_host=proxy-2.nb.renner.uno ansible_user=root
|
||||
|
||||
# Every bare-metal edge thin client. Gets the host-generic roles only:
|
||||
# flash-wear handling, the log shipper, the MQTT bridge, the VLAN
|
||||
@@ -79,11 +79,18 @@ proxy-2.renner.uno
|
||||
netbird.renner.uno
|
||||
edge-1.iot.renner.uno
|
||||
|
||||
# Hosts whose admin logins accept only ssh_admin_authorized_keys (break-glass
|
||||
# + CI); everything else needs a CA certificate. Members must be in [ssh_ca].
|
||||
# Hosts whose admin logins accept only ssh_admin_authorized_keys (break-glass);
|
||||
# everything else needs a CA certificate. Members must be in [ssh_ca].
|
||||
# Added one host at a time after the [ssh_ca] report was reviewed.
|
||||
[ssh_keys_managed]
|
||||
proxy-1.renner.uno
|
||||
proxy-2.renner.uno
|
||||
netbird.renner.uno
|
||||
edge-1.iot.renner.uno
|
||||
|
||||
# Internet hosts whose sshd listens on the NetBird interface only
|
||||
# (plays/configure-ssh-mesh-only.yml). Members need the host NetBird client.
|
||||
# Added one host at a time after the previous one was verified.
|
||||
[ssh_mesh_only]
|
||||
proxy-1.renner.uno
|
||||
proxy-2.renner.uno
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
roles:
|
||||
- ssh-ca
|
||||
|
||||
- name: Reduce authorized_keys to break-glass and CI
|
||||
- name: Reduce authorized_keys to the break-glass key
|
||||
hosts: ssh_keys_managed
|
||||
become: true
|
||||
gather_facts: false
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
# sshd on the hosts in [ssh_mesh_only] accepts connections only on the NetBird
|
||||
# interface; nothing listens on their public addresses. Run from a workstation
|
||||
# in the mesh. A switchover reverts by itself unless the play logs in again
|
||||
# through the new listener.
|
||||
#
|
||||
# serial: 1 so a host that does not come back stops the run before the next
|
||||
# host is touched.
|
||||
- name: Restrict sshd to the mesh interface
|
||||
hosts: ssh_mesh_only
|
||||
become: true
|
||||
gather_facts: true
|
||||
serial: 1
|
||||
roles:
|
||||
- ssh-mesh-only
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
# Local MQTT bridge and its credential receiver only (REN-1307). Run by
|
||||
# .woodpecker/deploy-edge-mqtt-bridge.yml on merge, so a change to this role
|
||||
# reaches edge-1 without a play run by hand. The full base play
|
||||
# (install-edge-host.yml) carries the same role.
|
||||
# Local MQTT bridge and its credential receiver only (REN-1307). Run from a
|
||||
# workstation. The full base play (install-edge-host.yml) carries the same role.
|
||||
- name: Converge the edge MQTT bridge
|
||||
hosts: edge_host
|
||||
become: true
|
||||
|
||||
@@ -136,23 +136,13 @@
|
||||
- /opt/netbird/proxy
|
||||
- /opt/netbird/proxy-state
|
||||
|
||||
# The management store has no cluster-side backup. Pulling one put a root
|
||||
# key on this host that the cluster held, and this server's own state is
|
||||
# its own business to protect rather than the cluster's. Dropping the two
|
||||
# tasks that placed them would only stop ansible managing the key and the
|
||||
# script — the pair below revokes them instead, and can go once the role
|
||||
# has run against every host that ever carried them.
|
||||
# The management store has no cluster-side backup; the cluster's backup
|
||||
# key is gone with the managed authorized_keys (ssh-ca role).
|
||||
- name: Remove the backup dump script
|
||||
ansible.builtin.file:
|
||||
path: /usr/local/bin/netbird-backup-dump
|
||||
state: absent
|
||||
|
||||
- name: Revoke the cluster backup key
|
||||
ansible.posix.authorized_key:
|
||||
user: root
|
||||
key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFeW/L9bHMTSa5/Qb0ClppCf4Z0hL39JwzI6tBmKd2FG netbird-backup@homelab-cluster"
|
||||
state: absent
|
||||
|
||||
- name: Wait for management container to be ready
|
||||
ansible.builtin.pause:
|
||||
seconds: 10
|
||||
|
||||
@@ -130,7 +130,9 @@
|
||||
# The unit is sshd.service on Arch and RedHat, ssh.service on Debian/Ubuntu.
|
||||
# Ubuntu also lists sshd.service as a not-found alias, so the running one is
|
||||
# picked. sshd -T only parses the files, so the running daemon is reloaded on
|
||||
# every run; a reload keeps open sessions.
|
||||
# every run; a reload keeps open sessions. None runs when sshd is socket
|
||||
# activated and stopped, or per connection (ssh-mesh-only role); the next
|
||||
# start reads the config.
|
||||
- name: Read the service list
|
||||
ansible.builtin.service_facts:
|
||||
|
||||
@@ -138,16 +140,17 @@
|
||||
vars:
|
||||
ssh_ca_sshd_units: "{{ ansible_facts.services | dict2items | selectattr('key', 'in', ['sshd.service', 'ssh.service']) | selectattr('value.state', 'equalto', 'running') | map(attribute='key') | list }}"
|
||||
block:
|
||||
- name: Check exactly one sshd unit is running
|
||||
- name: Check at most one sshd unit is running
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ssh_ca_sshd_units | length == 1
|
||||
fail_msg: "Expected one running sshd.service or ssh.service, found {{ ssh_ca_sshd_units }}"
|
||||
- ssh_ca_sshd_units | length <= 1
|
||||
fail_msg: "Expected at most one running sshd.service or ssh.service, found {{ ssh_ca_sshd_units }}"
|
||||
|
||||
- name: Reload sshd so the running daemon uses the CA config
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ ssh_ca_sshd_units | first }}"
|
||||
state: reloaded
|
||||
when: ssh_ca_sshd_units | length == 1
|
||||
|
||||
- name: Report the admin logins' authorized keys
|
||||
ansible.builtin.include_tasks: report.yml
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
# The only interface sshd accepts connections on.
|
||||
ssh_mesh_interface: wt0
|
||||
|
||||
# Minutes until a switchover reverts to the distro listener, unless a new
|
||||
# login through the mesh-only listener confirms it first.
|
||||
ssh_mesh_revert_minutes: 5
|
||||
|
||||
ssh_mesh_udev_rule: /etc/udev/rules.d/90-ssh-mesh-only.rules
|
||||
@@ -0,0 +1,136 @@
|
||||
---
|
||||
- name: Load the distro's sshd unit names
|
||||
ansible.builtin.include_vars: "{{ ansible_facts['os_family'] }}.yml"
|
||||
|
||||
- name: Look for the mesh interface
|
||||
ansible.builtin.stat:
|
||||
path: "/sys/class/net/{{ ssh_mesh_interface }}"
|
||||
register: ssh_mesh_iface
|
||||
|
||||
- name: Check the mesh interface exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ssh_mesh_iface.stat.exists
|
||||
fail_msg: "No {{ ssh_mesh_interface }}: sshd would be unreachable. Install the NetBird client first."
|
||||
|
||||
- name: Create the socket unit directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ ssh_mesh_socket_file | dirname }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Install the mesh-only socket
|
||||
ansible.builtin.template:
|
||||
src: "{{ ssh_mesh_socket_template }}"
|
||||
dest: "{{ ssh_mesh_socket_file }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: Bind the sshd service to the socket
|
||||
when: ssh_mesh_service_dropin | length > 0
|
||||
block:
|
||||
- name: Create the service drop-in directory
|
||||
ansible.builtin.file:
|
||||
path: "{{ ssh_mesh_service_dropin | dirname }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
|
||||
- name: Install the service drop-in
|
||||
ansible.builtin.template:
|
||||
src: ssh.service-dropin.conf.j2
|
||||
dest: "{{ ssh_mesh_service_dropin }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: Install the udev rule that starts the socket with the interface
|
||||
ansible.builtin.template:
|
||||
src: udev.rules.j2
|
||||
dest: "{{ ssh_mesh_udev_rule }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
register: ssh_mesh_udev
|
||||
|
||||
- name: Reload the udev rules
|
||||
ansible.builtin.command: udevadm control --reload
|
||||
when: ssh_mesh_udev is changed
|
||||
changed_when: true
|
||||
|
||||
- name: Install the switch and revert scripts
|
||||
ansible.builtin.template:
|
||||
src: "{{ item }}.j2"
|
||||
dest: "/usr/local/sbin/{{ item }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
loop:
|
||||
- ssh-mesh-switch
|
||||
- ssh-mesh-revert
|
||||
|
||||
- name: Reload systemd units
|
||||
ansible.builtin.systemd:
|
||||
daemon_reload: true
|
||||
|
||||
- name: Read the sshd listeners
|
||||
ansible.builtin.command: ss -Htln 'sport = :22'
|
||||
changed_when: false
|
||||
register: ssh_mesh_listeners
|
||||
|
||||
# Each run that finds a listener outside the interface switches over. The
|
||||
# revert timer runs in its own unit, so a switch that locks the play out is
|
||||
# undone after ssh_mesh_revert_minutes.
|
||||
- name: Switch sshd to the mesh-only socket
|
||||
when: >-
|
||||
ssh_mesh_listeners.stdout_lines | length == 0 or
|
||||
ssh_mesh_listeners.stdout_lines | reject('search', '%' ~ ssh_mesh_interface ~ ':22\s') | list | length > 0
|
||||
block:
|
||||
- name: Clear a revert timer left by an earlier run
|
||||
ansible.builtin.command: systemctl stop ssh-mesh-revert.timer ssh-mesh-revert.service
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Arm the revert timer
|
||||
ansible.builtin.command: >-
|
||||
systemd-run --unit=ssh-mesh-revert --collect
|
||||
--on-active={{ ssh_mesh_revert_minutes }}min
|
||||
/usr/local/sbin/ssh-mesh-revert
|
||||
changed_when: true
|
||||
|
||||
- name: Switch the listener
|
||||
ansible.builtin.command: systemd-run --unit=ssh-mesh-switch --collect --wait /usr/local/sbin/ssh-mesh-switch
|
||||
changed_when: true
|
||||
|
||||
- name: Reconnect through the current listener
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: Read the sshd listeners after reconnecting
|
||||
ansible.builtin.command: ss -Htln 'sport = :22'
|
||||
changed_when: false
|
||||
register: ssh_mesh_listeners_after
|
||||
|
||||
- name: Check sshd listens on the mesh interface only
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ssh_mesh_listeners_after.stdout_lines | length > 0
|
||||
- ssh_mesh_listeners_after.stdout_lines | reject('search', '%' ~ ssh_mesh_interface ~ ':22\s') | list | length == 0
|
||||
fail_msg: "Listeners outside {{ ssh_mesh_interface }}: {{ ssh_mesh_listeners_after.stdout_lines }}"
|
||||
|
||||
- name: Cancel the revert timer
|
||||
ansible.builtin.command: systemctl stop ssh-mesh-revert.timer
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Check the public name refuses SSH
|
||||
ansible.builtin.wait_for:
|
||||
host: "{{ inventory_hostname }}"
|
||||
port: 22
|
||||
state: stopped
|
||||
timeout: 15
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
# {{ ansible_managed }}
|
||||
# Restores the distro listener on all addresses. Armed as a timer by every
|
||||
# switchover; the play cancels it after a login through the new listener.
|
||||
systemctl stop {{ ssh_mesh_service }} {{ ssh_mesh_socket }}
|
||||
rm -f {{ ssh_mesh_udev_rule }} {{ ssh_mesh_socket_file }}{{ (' ' ~ ssh_mesh_service_dropin) if ssh_mesh_service_dropin else '' }}
|
||||
udevadm control --reload
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now {{ ssh_mesh_distro_listener }}
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
# {{ ansible_managed }}
|
||||
# Replaces the distro listener with the {{ ssh_mesh_interface }}-only socket.
|
||||
# Open sessions survive: they run in logind scopes and the sshd units use
|
||||
# KillMode=process. Any failure restores the distro listener at once.
|
||||
set -eu
|
||||
trap '/usr/local/sbin/ssh-mesh-revert' EXIT
|
||||
systemctl disable {{ ssh_mesh_disable_units }}
|
||||
systemctl stop {{ ssh_mesh_service }} {{ ssh_mesh_socket }}
|
||||
systemctl start {{ ssh_mesh_socket }}
|
||||
trap - EXIT
|
||||
@@ -0,0 +1,7 @@
|
||||
# {{ ansible_managed }}
|
||||
# sshd holds the socket's listener; it stops with the socket, so it never keeps
|
||||
# a listener on a removed {{ ssh_mesh_interface }}. Replaces the Requires= that
|
||||
# enabling {{ ssh_mesh_socket }} would add.
|
||||
[Unit]
|
||||
BindsTo={{ ssh_mesh_socket }}
|
||||
After={{ ssh_mesh_socket }}
|
||||
@@ -0,0 +1,8 @@
|
||||
# {{ ansible_managed }}
|
||||
# Listen on {{ ssh_mesh_interface }} only. The unit is disabled; the udev rule
|
||||
# starts it whenever {{ ssh_mesh_interface }} appears.
|
||||
[Socket]
|
||||
ListenStream=
|
||||
ListenStream=22
|
||||
BindToDevice={{ ssh_mesh_interface }}
|
||||
BindIPv6Only=both
|
||||
@@ -0,0 +1,12 @@
|
||||
# {{ ansible_managed }}
|
||||
# Started by the udev rule whenever {{ ssh_mesh_interface }} appears; BindToDevice
|
||||
# stops it when the interface goes away. No [Install]: waiting for the device
|
||||
# from sockets.target would hold up boot until NetBird is up.
|
||||
[Unit]
|
||||
Description=OpenSSH on {{ ssh_mesh_interface }} only
|
||||
|
||||
[Socket]
|
||||
ListenStream=22
|
||||
BindToDevice={{ ssh_mesh_interface }}
|
||||
BindIPv6Only=both
|
||||
Accept=yes
|
||||
@@ -0,0 +1,3 @@
|
||||
# {{ ansible_managed }}
|
||||
# NetBird deletes and recreates {{ ssh_mesh_interface }} on every engine restart.
|
||||
SUBSYSTEM=="net", KERNEL=="{{ ssh_mesh_interface }}", ENV{SYSTEMD_WANTS}+="{{ ssh_mesh_socket }}"
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
# No packaged socket; sshd.socket (Accept=yes) pairs with the packaged
|
||||
# per-connection sshd@.service, since sshd -D cannot take a listening fd.
|
||||
ssh_mesh_socket: sshd.socket
|
||||
ssh_mesh_service: sshd.service
|
||||
ssh_mesh_distro_listener: sshd.service
|
||||
ssh_mesh_disable_units: sshd.service
|
||||
ssh_mesh_socket_file: /etc/systemd/system/sshd.socket
|
||||
ssh_mesh_socket_template: sshd.socket.j2
|
||||
ssh_mesh_service_dropin: ""
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
# Ubuntu's ssh.socket (Accept=no) hands its listener to ssh.service. The
|
||||
# drop-in is named like sshd-socket-generator's output so it masks it.
|
||||
ssh_mesh_socket: ssh.socket
|
||||
ssh_mesh_service: ssh.service
|
||||
ssh_mesh_distro_listener: ssh.socket
|
||||
ssh_mesh_disable_units: ssh.service ssh.socket
|
||||
ssh_mesh_socket_file: /etc/systemd/system/ssh.socket.d/addresses.conf
|
||||
ssh_mesh_socket_template: ssh.socket-addresses.conf.j2
|
||||
ssh_mesh_service_dropin: /etc/systemd/system/ssh.service.d/ssh-mesh-only.conf
|
||||
@@ -12,7 +12,10 @@ The image is the official archiso **releng** profile plus:
|
||||
- `edge-install` in the live environment — partitions the NVMe
|
||||
(ESP + ext4 root, systemd-boot), pacstraps the base system incl.
|
||||
docker/python, configures DHCP via systemd-networkd, enables sshd and
|
||||
drops the common homelab SSH keys for root (key-only, no root password)
|
||||
sets up root login (key-only, no root password) with owner certificates
|
||||
from the OpenBao SSH CA, fetched at build time from the `ssh-ca` role's
|
||||
URL, and the break-glass key (YubiKey NEO, PIV) as fallback:
|
||||
`ssh -I /usr/lib/opensc-pkcs11.so root@<host>`
|
||||
- the extra disk tools the installer needs
|
||||
|
||||
Division of labour: the ISO produces an **ansible-ready base system**;
|
||||
@@ -30,7 +33,8 @@ sudo ./build.sh # ISO lands in ./out/
|
||||
|
||||
The profile is assembled at build time from `/usr/share/archiso/configs/releng`
|
||||
plus the overlay in `airootfs/` — the repo carries only the delta, so
|
||||
archiso updates flow in automatically.
|
||||
archiso updates flow in automatically. The build needs openbao.renner.uno
|
||||
for the CA key; rebuild the ISO after a CA rotation.
|
||||
|
||||
## Install
|
||||
|
||||
@@ -38,8 +42,7 @@ archiso updates flow in automatically.
|
||||
2. Boot the target from it (UEFI, Secure Boot off; one-time F12/setup
|
||||
visit if the box doesn't already boot USB first)
|
||||
3. Either on the console (auto-login as root) or headless —
|
||||
`ssh root@<dhcp-ip>` works out of the box, the live env carries the
|
||||
homelab keys — run `edge-install`. It auto-detects the internal disk
|
||||
`ssh root@<dhcp-ip>` with an owner certificate — run `edge-install`. It auto-detects the internal disk
|
||||
(NVMe, else eMMC — covers the Wyse 3040 — else first non-USB disk),
|
||||
shows the target and requires typing the hostname before wiping
|
||||
anything. Override with `edge-install /dev/sdY`.
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPzFNBP3bq2m/6JqDqod9w+FP/uxXxHMolqhq3Ojflr1 krenner@fortress
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGfe3HJqFR+jIGrboweseAO7XjIVE24sje/b1HybgU9C ansible-runner@ci-new
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICyNnX1I4W9jyDvCJ1DJauHSmbo1ZXSyBxVhSAwMWgen woodpecker-ci@homelab
|
||||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB1OFjh6Q5xeioI6Cwfuv8Xka27veomYy9tKBbpMo8mcrsB4yIN4FX8HovBLtogetBhwBUefBar8nhA9GbRh6HQ= krenner@krenner-XPS-13-9310
|
||||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKvSl9LUP8u3S2Sq5YTWpW0Jxbue8cJhD7/LMFQbEsa3xo145HVR7pBzRxW71MUy+jgrgmpSlDhIb606Hswhvoo= breakglass-neo-6532348
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPzFNBP3bq2m/6JqDqod9w+FP/uxXxHMolqhq3Ojflr1 krenner@fortress
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGfe3HJqFR+jIGrboweseAO7XjIVE24sje/b1HybgU9C ansible-runner@ci-new
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICyNnX1I4W9jyDvCJ1DJauHSmbo1ZXSyBxVhSAwMWgen woodpecker-ci@homelab
|
||||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB1OFjh6Q5xeioI6Cwfuv8Xka27veomYy9tKBbpMo8mcrsB4yIN4FX8HovBLtogetBhwBUefBar8nhA9GbRh6HQ= krenner@krenner-XPS-13-9310
|
||||
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKvSl9LUP8u3S2Sq5YTWpW0Jxbue8cJhD7/LMFQbEsa3xo145HVR7pBzRxW71MUy+jgrgmpSlDhIb606Hswhvoo= breakglass-neo-6532348
|
||||
|
||||
@@ -81,6 +81,17 @@ ln -sf /run/systemd/resolve/stub-resolv.conf /mnt/etc/resolv.conf
|
||||
install -d -m700 /mnt/root/.ssh
|
||||
install -m600 /root/edge-authorized_keys /mnt/root/.ssh/authorized_keys
|
||||
|
||||
# SSH user CA trust as baked into this ISO (owner certificates for root).
|
||||
install -m644 -t /mnt/etc/ssh /etc/ssh/trusted_user_ca_keys /etc/ssh/revoked_keys
|
||||
install -d -m755 /mnt/etc/ssh/auth_principals /mnt/etc/ssh/sshd_config.d
|
||||
install -m644 /etc/ssh/auth_principals/root /mnt/etc/ssh/auth_principals/root
|
||||
install -m644 /etc/ssh/sshd_config.d/50-ssh-ca.conf /mnt/etc/ssh/sshd_config.d/
|
||||
# sshd -t/-T need host keys; sshdgenkeys would create them on first boot.
|
||||
ssh-keygen -A -f /mnt
|
||||
arch-chroot /mnt sshd -t
|
||||
arch-chroot /mnt sshd -T | grep -qix 'trustedusercakeys /etc/ssh/trusted_user_ca_keys' \
|
||||
|| { echo "installed sshd does not use the SSH CA"; exit 1; }
|
||||
|
||||
arch-chroot /mnt systemctl enable \
|
||||
systemd-networkd systemd-resolved systemd-timesyncd sshd docker
|
||||
|
||||
|
||||
@@ -10,6 +10,23 @@ profile=/tmp/profile
|
||||
cp -r /usr/share/archiso/configs/releng/. "$profile"
|
||||
cp -r /src/airootfs/. "$profile/airootfs/"
|
||||
|
||||
# SSH user CA trust from the ssh-ca role (mounted at /ssh-ca): the live system
|
||||
# and the installed one accept owner certificates for root, like the managed
|
||||
# hosts. The CA public key is fetched from OpenBao at build time.
|
||||
ca_url=$(sed -n 's/^ssh_ca_public_key_url: "\(.*\)"$/\1/p' /ssh-ca/defaults/main.yml)
|
||||
principal=$(sed -n 's/^ssh_ca_owner_principal: \(.*\)$/\1/p' /ssh-ca/defaults/main.yml)
|
||||
[ -n "$ca_url" ] && [ -n "$principal" ] || { echo "ssh-ca defaults not readable"; exit 1; }
|
||||
ca_key=$(curl -sSf "$ca_url")
|
||||
[[ "$ca_key" == "ssh-ed25519 AAAA"* ]] || { echo "unexpected CA key from $ca_url"; exit 1; }
|
||||
sshd_conf=$(grep -vx '# {{ ansible_managed }}' /ssh-ca/templates/ssh-ca.conf.j2)
|
||||
! grep -q '{[{%]' <<<"$sshd_conf" || { echo "ssh-ca.conf.j2 has template logic"; exit 1; }
|
||||
ssh_dir="$profile/airootfs/etc/ssh"
|
||||
install -d "$ssh_dir/sshd_config.d" "$ssh_dir/auth_principals"
|
||||
printf '%s\n' "$ca_key" > "$ssh_dir/trusted_user_ca_keys"
|
||||
cp /ssh-ca/files/revoked_keys "$ssh_dir/revoked_keys"
|
||||
printf '%s\n' "$principal" > "$ssh_dir/auth_principals/root"
|
||||
printf '%s\n' "$sshd_conf" > "$ssh_dir/sshd_config.d/50-ssh-ca.conf"
|
||||
|
||||
# Tools the installer script needs in the live environment (idempotent —
|
||||
# skip entries releng already ships).
|
||||
for pkg in gptfdisk dosfstools e2fsprogs; do
|
||||
|
||||
@@ -13,6 +13,7 @@ mkdir -p "$out"
|
||||
|
||||
docker run --rm --privileged \
|
||||
-v "$here:/src:ro" \
|
||||
-v "$here/../../ansible/roles/ssh-ca:/ssh-ca:ro" \
|
||||
-v "$out:/out" \
|
||||
archlinux:latest /src/build-inner.sh
|
||||
|
||||
|
||||
@@ -43,12 +43,6 @@ provider "proxmox" {
|
||||
endpoint = var.proxmox_api_url
|
||||
api_token = "${var.proxmox_user}!${var.proxmox_token_id}=${var.proxmox_token_secret}"
|
||||
insecure = var.proxmox_tls_insecure
|
||||
|
||||
ssh {
|
||||
agent = false
|
||||
username = "root"
|
||||
private_key = var.ssh_private_key
|
||||
}
|
||||
}
|
||||
|
||||
provider "unifi" {
|
||||
|
||||
@@ -28,16 +28,4 @@ variable "proxmox_tls_insecure" {
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "ssh_public_key" {
|
||||
description = "Public SSH key to inject into containers"
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "ssh_private_key" {
|
||||
description = "SSH private key for Proxmox provider to import disk images"
|
||||
type = string
|
||||
sensitive = true
|
||||
default = ""
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user