diff --git a/.woodpecker/deploy-claude-workstation.yml b/.woodpecker/deploy-claude-workstation.yml deleted file mode 100644 index 4a3ea309..00000000 --- a/.woodpecker/deploy-claude-workstation.yml +++ /dev/null @@ -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 diff --git a/.woodpecker/deploy-edge-mqtt-bridge.yml b/.woodpecker/deploy-edge-mqtt-bridge.yml deleted file mode 100644 index 5cbaad27..00000000 --- a/.woodpecker/deploy-edge-mqtt-bridge.yml +++ /dev/null @@ -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 diff --git a/.woodpecker/deploy-infrastructure.yml b/.woodpecker/deploy-infrastructure.yml index d89ab1ae..6bb03797 100644 --- a/.woodpecker/deploy-infrastructure.yml +++ b/.woodpecker/deploy-infrastructure.yml @@ -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" diff --git a/.woodpecker/deploy-netbird-client.yml b/.woodpecker/deploy-netbird-client.yml deleted file mode 100644 index bb62e04e..00000000 --- a/.woodpecker/deploy-netbird-client.yml +++ /dev/null @@ -1,28 +0,0 @@ -when: - # Manual only: the internet hosts are reached over the NetBird mesh, from a workstation. - - 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 diff --git a/.woodpecker/deploy-netbird-proxy.yml b/.woodpecker/deploy-netbird-proxy.yml deleted file mode 100644 index d6db2908..00000000 --- a/.woodpecker/deploy-netbird-proxy.yml +++ /dev/null @@ -1,31 +0,0 @@ -when: - # Manual only: the internet hosts are reached over the NetBird mesh, from a workstation. - - 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 diff --git a/.woodpecker/deploy-netbird-server.yml b/.woodpecker/deploy-netbird-server.yml deleted file mode 100644 index f504fa71..00000000 --- a/.woodpecker/deploy-netbird-server.yml +++ /dev/null @@ -1,31 +0,0 @@ -when: - # Manual only: the internet hosts are reached over the NetBird mesh, from a workstation. - - 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 diff --git a/.woodpecker/deploy-ssh-ca.yml b/.woodpecker/deploy-ssh-ca.yml deleted file mode 100644 index ad5ccc11..00000000 --- a/.woodpecker/deploy-ssh-ca.yml +++ /dev/null @@ -1,27 +0,0 @@ -when: - # Manual only: the internet hosts are reached over the NetBird mesh, from a workstation. - - 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 diff --git a/.woodpecker/deploy-uptime-kuma.yml b/.woodpecker/deploy-uptime-kuma.yml index 16be1d9a..6c66c656 100644 --- a/.woodpecker/deploy-uptime-kuma.yml +++ b/.woodpecker/deploy-uptime-kuma.yml @@ -1,7 +1,4 @@ when: - # Manual only: the internet hosts are reached over the NetBird mesh, from a workstation. - - event: deployment - evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "deploy-uptime-kuma"' - event: cron cron: uptime-kuma-token-renew @@ -15,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: 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 diff --git a/.woodpecker/update-os.yml b/.woodpecker/update-os.yml deleted file mode 100644 index b44f3b79..00000000 --- a/.woodpecker/update-os.yml +++ /dev/null @@ -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 diff --git a/ansible/docs/edge-host.md b/ansible/docs/edge-host.md index 2964c9fd..432a5f54 100644 --- a/ansible/docs/edge-host.md +++ b/ansible/docs/edge-host.md @@ -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). diff --git a/ansible/inventory/group_vars/all/vars.yml b/ansible/inventory/group_vars/all/vars.yml index 1f7b3fe6..d208c159 100644 --- a/ansible/inventory/group_vars/all/vars.yml +++ b/ansible/inventory/group_vars/all/vars.yml @@ -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@ -# 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' diff --git a/ansible/inventory/group_vars/netbird_proxy/vars.yml b/ansible/inventory/group_vars/netbird_proxy/vars.yml index 96235fa0..fb1a9144 100644 --- a/ansible/inventory/group_vars/netbird_proxy/vars.yml +++ b/ansible/inventory/group_vars/netbird_proxy/vars.yml @@ -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 diff --git a/ansible/inventory/host_vars/netbird.renner.uno.yml b/ansible/inventory/host_vars/netbird.renner.uno.yml index cc6f6a82..f099a419 100644 --- a/ansible/inventory/host_vars/netbird.renner.uno.yml +++ b/ansible/inventory/host_vars/netbird.renner.uno.yml @@ -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: "" diff --git a/ansible/plays/edge-mqtt-bridge.yml b/ansible/plays/edge-mqtt-bridge.yml index 1fb2e801..ce64b033 100644 --- a/ansible/plays/edge-mqtt-bridge.yml +++ b/ansible/plays/edge-mqtt-bridge.yml @@ -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 diff --git a/images/edge-host-iso/airootfs/root/.ssh/authorized_keys b/images/edge-host-iso/airootfs/root/.ssh/authorized_keys index 0852b517..bb195c22 100644 --- a/images/edge-host-iso/airootfs/root/.ssh/authorized_keys +++ b/images/edge-host-iso/airootfs/root/.ssh/authorized_keys @@ -1,4 +1,3 @@ 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 diff --git a/images/edge-host-iso/airootfs/root/edge-authorized_keys b/images/edge-host-iso/airootfs/root/edge-authorized_keys index 0852b517..bb195c22 100644 --- a/images/edge-host-iso/airootfs/root/edge-authorized_keys +++ b/images/edge-host-iso/airootfs/root/edge-authorized_keys @@ -1,4 +1,3 @@ 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