From b68ef964d9274a3fc3488fec7ace5c9dc0d543ba Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Fri, 28 Aug 2026 10:42:52 +0200 Subject: [PATCH 1/3] fix(apps): remove the file ingress pointing at a powered-off NAS file.renner.uno served HTTP 502. The Service is an ExternalName to nas.renner.uno:7001, a CNAME to nas-1, and nas-1 has been off for months. The Ingress and Service had been in the cluster for 110 days pointing at nothing. Nothing caught it because file.renner.uno is not in the blackbox probe list, so a publicly resolvable hostname returned 502 for months without a single alert. The insecure-backend ServersTransport goes with it. It existed to let Traefik re-encrypt to the NAS web UI's self-signed certificate and apps/file was its only consumer, which its own header comment documents. --- kubernetes/apps/file/ingress.yaml | 31 ------------------- kubernetes/apps/file/kustomization.yaml | 5 --- kubernetes/apps/file/service.yaml | 18 ----------- .../insecure-backend-serverstransport.yaml | 20 ------------ kubernetes/apps/kustomization.yaml | 2 -- 5 files changed, 76 deletions(-) delete mode 100644 kubernetes/apps/file/ingress.yaml delete mode 100644 kubernetes/apps/file/kustomization.yaml delete mode 100644 kubernetes/apps/file/service.yaml delete mode 100644 kubernetes/apps/insecure-backend-serverstransport.yaml diff --git a/kubernetes/apps/file/ingress.yaml b/kubernetes/apps/file/ingress.yaml deleted file mode 100644 index 0ed5484f..00000000 --- a/kubernetes/apps/file/ingress.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: file - namespace: apps - labels: - app.kubernetes.io/name: file - annotations: - cert-manager.io/cluster-issuer: letsencrypt-cluster - # Backend serves HTTPS with a self-signed cert; re-encrypt + skip - # verify via the shared insecure-backend ServersTransport (see - # apps/insecure-backend-serverstransport.yaml). - traefik.ingress.kubernetes.io/service.serversscheme: https - traefik.ingress.kubernetes.io/service.serverstransport: insecure-backend@kubernetescrd -spec: - ingressClassName: traefik - rules: - - host: file.renner.uno - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: file - port: - number: 7001 - tls: - - secretName: file-tls - hosts: - - file.renner.uno diff --git a/kubernetes/apps/file/kustomization.yaml b/kubernetes/apps/file/kustomization.yaml deleted file mode 100644 index ab2ea8df..00000000 --- a/kubernetes/apps/file/kustomization.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -resources: - - service.yaml - - ingress.yaml diff --git a/kubernetes/apps/file/service.yaml b/kubernetes/apps/file/service.yaml deleted file mode 100644 index 6bfe78c9..00000000 --- a/kubernetes/apps/file/service.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: file - namespace: apps - labels: - app.kubernetes.io/name: file -spec: - # Caddy reverse-proxied https://nas.renner.uno:7001. nas.renner.uno - # is a CNAME -> nas-1.renner.uno owned by UniFi DNS, k8s-gateway - # forwards srv lookups so this resolves cluster-side. - type: ExternalName - externalName: nas.renner.uno - ports: - - name: https - port: 7001 - targetPort: 7001 - protocol: TCP diff --git a/kubernetes/apps/insecure-backend-serverstransport.yaml b/kubernetes/apps/insecure-backend-serverstransport.yaml deleted file mode 100644 index b111a3cc..00000000 --- a/kubernetes/apps/insecure-backend-serverstransport.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Shared Traefik ServersTransport for ingresses that re-encrypt to a -# backend serving HTTPS with a self-signed / hostname-mismatched cert: -# -# apps/file — NAS web UI on a self-signed cert -# -# Replaces the nginx `backend-protocol: HTTPS` + `proxy-ssl-verify: off` -# annotation pair. Each Ingress that wants this behaviour sets -# traefik.ingress.kubernetes.io/service.serversscheme: https -# traefik.ingress.kubernetes.io/service.serverstransport: insecure-backend -# -# (bare name — kubernetesIngress provider resolves ServersTransport in -# the same namespace as the Ingress; the `-@kubernetescrd` -# qualified form is for IngressRoute CRDs only). -apiVersion: traefik.io/v1alpha1 -kind: ServersTransport -metadata: - name: insecure-backend - namespace: apps -spec: - insecureSkipVerify: true diff --git a/kubernetes/apps/kustomization.yaml b/kubernetes/apps/kustomization.yaml index 099bf5ab..5af97dfd 100644 --- a/kubernetes/apps/kustomization.yaml +++ b/kubernetes/apps/kustomization.yaml @@ -1,10 +1,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - - insecure-backend-serverstransport.yaml - gatus/ - external-dns/ - - file/ - gitea/ - ical-proxy/ # woodpecker/ moved to its own dedicated `woodpecker` Kustomization -- 2.54.0 From 9c9e45d17ddbaa48d7285737d8354a89ec3e4302 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Fri, 28 Aug 2026 10:43:59 +0200 Subject: [PATCH 2/3] chore(ansible): drop the nas-1 inventory, roles and plays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nas-1 has been off for months. Everything scoped to it went with it: inventory/host_vars/nas-1.renner.uno/ vars and vault [synology_nas] in pve_hosts.yml the group's only member roles/pbs-client/ PBS backup client plays/install-pbs-client.yml role-based, hosts: synology_nas plays/install-pbs-client-synology.yml raw-module variant, same host plays/install-garage-synology.yml Garage S3, replaced by Hetzner .woodpecker/install-garage-synology.yml the pipeline that ran it The two PBS plays were already inconsistent with each other before the host went away: the role pulled aterfax/pbs-client, while the standalone play pulled proxmox/proxmox-backup-client, which has never existed on Docker Hub (404). Nothing referenced the standalone play at all. Both are moot now. pve_hosts.yml keeps its "gone from this file, and why" list, so synology_nas is recorded there alongside pve_cluster and the in-cluster migrations rather than vanishing without trace. Checked: `ansible-inventory --graph` resolves, and grep finds no remaining reference to synology_nas, nas-1, pbs-client or the garage play outside that one note. plays/archived/ is left alone — it is history, not live config. --- .woodpecker/install-garage-synology.yml | 27 -- .../host_vars/nas-1.renner.uno/vars.yml | 37 --- .../host_vars/nas-1.renner.uno/vault.yml | 27 -- ansible/inventory/pve_hosts.yml | 6 +- ansible/plays/install-garage-synology.yml | 313 ------------------ ansible/plays/install-pbs-client-synology.yml | 104 ------ ansible/plays/install-pbs-client.yml | 11 - ansible/roles/pbs-client/defaults/main.yml | 32 -- ansible/roles/pbs-client/handlers/main.yml | 6 - ansible/roles/pbs-client/tasks/main.yml | 23 -- ansible/roles/pbs-client/templates/.env.j2 | 6 - .../roles/pbs-client/templates/backup.sh.j2 | 36 -- .../templates/docker-compose.yml.j2 | 30 -- 13 files changed, 2 insertions(+), 656 deletions(-) delete mode 100644 .woodpecker/install-garage-synology.yml delete mode 100644 ansible/inventory/host_vars/nas-1.renner.uno/vars.yml delete mode 100644 ansible/inventory/host_vars/nas-1.renner.uno/vault.yml delete mode 100644 ansible/plays/install-garage-synology.yml delete mode 100644 ansible/plays/install-pbs-client-synology.yml delete mode 100644 ansible/plays/install-pbs-client.yml delete mode 100644 ansible/roles/pbs-client/defaults/main.yml delete mode 100644 ansible/roles/pbs-client/handlers/main.yml delete mode 100644 ansible/roles/pbs-client/tasks/main.yml delete mode 100644 ansible/roles/pbs-client/templates/.env.j2 delete mode 100644 ansible/roles/pbs-client/templates/backup.sh.j2 delete mode 100644 ansible/roles/pbs-client/templates/docker-compose.yml.j2 diff --git a/.woodpecker/install-garage-synology.yml b/.woodpecker/install-garage-synology.yml deleted file mode 100644 index 5cd132de..00000000 --- a/.woodpecker/install-garage-synology.yml +++ /dev/null @@ -1,27 +0,0 @@ -when: - - event: deployment - evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "install-garage-synology"' - -steps: - - name: run-playbook - image: gitea.renner.uno/gitea_admin/ci-ansible:2026-04-29 - environment: - ANSIBLE_FORCE_COLOR: "true" - ANSIBLE_HOST_KEY_CHECKING: "false" - 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-garage-synology.yml -v diff --git a/ansible/inventory/host_vars/nas-1.renner.uno/vars.yml b/ansible/inventory/host_vars/nas-1.renner.uno/vars.yml deleted file mode 100644 index 2b341c35..00000000 --- a/ansible/inventory/host_vars/nas-1.renner.uno/vars.yml +++ /dev/null @@ -1,37 +0,0 @@ ---- -# PBS Client configuration for nas-1 -pbs_backup_paths: - - /volume1/Documents - - /volume1/homes - - /volume1/Homeassistant Backup - - /volume1/Games - - /volume1/Media - - /volume1/Music - - /volume1/video - - /volume2/Backup - - /volume1/Movies - - # Add more paths as needed: - # - /volume1/photos - # - /volume1/music - -# PBS Server connection -pbs_server_host: "pbs2.renner.uno" -pbs_server_port: 8007 -pbs_datastore: "pool" -pbs_backup_id: "nas-1" - -# PBS Authentication - credentials in vault.yml -pbs_username: "root@pam!nas-1" - -# Garage S3 (K8s backup target) -garage_image: "dxflrs/garage:v2.3.0" -garage_zone: "dsm" -garage_capacity: "2T" - -# SSH connection - use password auth (key not supported by DSM) -ansible_ssh_pass: "{{ nas_ssh_password }}" -ansible_become_pass: "{{ nas_ssh_password }}" - -# Python 3.9+ from Synology package -ansible_python_interpreter: /usr/local/bin/python3.9 diff --git a/ansible/inventory/host_vars/nas-1.renner.uno/vault.yml b/ansible/inventory/host_vars/nas-1.renner.uno/vault.yml deleted file mode 100644 index 2cef7032..00000000 --- a/ansible/inventory/host_vars/nas-1.renner.uno/vault.yml +++ /dev/null @@ -1,27 +0,0 @@ -$ANSIBLE_VAULT;1.1;AES256 -34663462636437633437613863366266306334666635643839313737343739643864613136613938 -3138393966306561656366396465383539303137333036300a376237376636356230343834386362 -36636535396330333732383233323263613132333166343739396137636538653435336662353138 -6565643866623235610a316266383933303830323936303735623338623165386339383733373435 -62366461363833616438373237383132366438326364363462333265366533323861376630643365 -39343036343838333639363432643738666630363063323636646661313539643465366532363438 -33303837623234613434633563666562346536346435353461373536323737376233386163326537 -61383963343737656438356136613537623538333163343466343739346135333439616432663763 -36663962633037376561326434303035333938646436663832303432346365353439666435613337 -34363636326530333233313231343566306432376532396666643761633136316333376337666135 -65363338623035663438323338663863366362653131336434323865326530326534623631336263 -36633063383031386161363131333636393364623436613331353733313235383062656131313164 -63393665633361313961363637663237616364643236383835393736643937393564656238376362 -31393465383361326264376133393233616264663636383337363962356532613438663637616536 -33653137656335323763313461323633616635346232303062323062333766383931313965346361 -66656461313239633366636463633465636463663937393932636337643861383937663662306466 -32353166643832303234643264633062616330393963333034623432613730366339306637613239 -34626161663666316339323762373639353335653663353635366664643063393432623039306237 -38363161343930363763393364663364353735343236313665313461626165636135303839363164 -37336638396564326166376233353730346563656266323535666265323062646232323938353732 -32636235333839623166633761376334316434613235333730353439343262393537363461333238 -32653435623362666662336532643832356163326332636436346132643237643030343761663332 -36663530356539303636663438646430363335353135323932613863666264363533326137353164 -31663032346236616263343637396439663334396166656364623637666535386530383535663435 -66303037303265616634373763616437333835333136336563323339666563663931396133633131 -3963653266633535303630653335666166313336653831623362 diff --git a/ansible/inventory/pve_hosts.yml b/ansible/inventory/pve_hosts.yml index 702d2026..3d85391b 100644 --- a/ansible/inventory/pve_hosts.yml +++ b/ansible/inventory/pve_hosts.yml @@ -8,6 +8,8 @@ # pve_cluster (jupiter/neptune/saturn) run Talos on bare metal; no SSH at all # authentik_db / authentik_app authentik runs in-cluster (CNPG) # grafana runs in-cluster +# synology_nas (nas-1) powered off; Garage and the PBS client +# it hosted are gone with it # Their hosts no longer resolve, except the three that resolve to Talos nodes. [netbird_server] @@ -17,10 +19,6 @@ netbird.renner.uno ansible_user=root proxy-1.renner.uno ansible_user=root proxy-2.renner.uno ansible_user=root - -[synology_nas] -nas-1.renner.uno ansible_user=ladmin ansible_python_interpreter=/usr/bin/python3 - # Every bare-metal edge thin client. Gets the host-generic roles only: # flash-wear handling, the log shipper, the MQTT bridge, the VLAN # sub-interface. Nothing here assumes any peripheral is attached. diff --git a/ansible/plays/install-garage-synology.yml b/ansible/plays/install-garage-synology.yml deleted file mode 100644 index 745a52ca..00000000 --- a/ansible/plays/install-garage-synology.yml +++ /dev/null @@ -1,313 +0,0 @@ ---- -# Synology-specific playbook using raw module (Python 3.8 workaround) -# Deploys Garage (https://garagehq.deuxfleurs.fr) as a single-node S3 target -# for K8s backups (Velero + CNPG Barman Cloud Plugin). -# -# Required secrets in host_vars/nas-1.renner.uno/vault.yml: -# garage_rpc_secret: "" -# garage_admin_token: "" -# garage_metrics_token: "" -# -# Post-install bootstrap (run once, record the returned secret keys): -# docker exec garaged /garage status -# docker exec garaged /garage bucket create k8s-velero -# docker exec garaged /garage bucket create k8s-cnpg -# docker exec garaged /garage bucket create k8s-secrets -# docker exec garaged /garage bucket create k8s-migration -# docker exec garaged /garage key create velero-key -# docker exec garaged /garage key create cnpg-key -# docker exec garaged /garage key create secrets-key -# docker exec garaged /garage key create migration-key -# docker exec garaged /garage bucket allow --read --write --owner k8s-velero --key velero-key -# docker exec garaged /garage bucket allow --read --write --owner k8s-cnpg --key cnpg-key -# docker exec garaged /garage bucket allow --read --write --owner k8s-secrets --key secrets-key -# docker exec garaged /garage bucket allow --read --write --owner k8s-migration --key migration-key -- name: Install Garage S3 on Synology NAS - hosts: synology_nas - gather_facts: false - - vars: - garage_data_dir: "/volume1/docker/garage" - - tasks: - - name: Create Garage directory tree - ansible.builtin.raw: | - mkdir -p {{ garage_data_dir }}/config \ - {{ garage_data_dir }}/meta \ - {{ garage_data_dir }}/data - changed_when: true - - # Secret writes use the same compare-then-write pattern as garage.toml so - # rotation is idempotent AND a real change can drive force-recreate below. - # Garage reads these files once at startup; without container restart, a - # rotated token leaves the running process holding the previous value. - - name: Write rpc_secret - ansible.builtin.raw: | - cat > {{ garage_data_dir }}/config/rpc_secret.new << 'SECRET_EOF' - {{ garage_rpc_secret }} - SECRET_EOF - chmod 600 {{ garage_data_dir }}/config/rpc_secret.new - if [ ! -f {{ garage_data_dir }}/config/rpc_secret ] \ - || ! cmp -s {{ garage_data_dir }}/config/rpc_secret.new {{ garage_data_dir }}/config/rpc_secret; then - mv {{ garage_data_dir }}/config/rpc_secret.new {{ garage_data_dir }}/config/rpc_secret - echo RPC_SECRET_CHANGED - else - rm {{ garage_data_dir }}/config/rpc_secret.new - fi - register: rpc_secret_write - changed_when: "'RPC_SECRET_CHANGED' in rpc_secret_write.stdout" - no_log: true - - - name: Write admin_token - ansible.builtin.raw: | - cat > {{ garage_data_dir }}/config/admin_token.new << 'SECRET_EOF' - {{ garage_admin_token }} - SECRET_EOF - chmod 600 {{ garage_data_dir }}/config/admin_token.new - if [ ! -f {{ garage_data_dir }}/config/admin_token ] \ - || ! cmp -s {{ garage_data_dir }}/config/admin_token.new {{ garage_data_dir }}/config/admin_token; then - mv {{ garage_data_dir }}/config/admin_token.new {{ garage_data_dir }}/config/admin_token - echo ADMIN_TOKEN_CHANGED - else - rm {{ garage_data_dir }}/config/admin_token.new - fi - register: admin_token_write - changed_when: "'ADMIN_TOKEN_CHANGED' in admin_token_write.stdout" - no_log: true - - - name: Write metrics_token - ansible.builtin.raw: | - cat > {{ garage_data_dir }}/config/metrics_token.new << 'SECRET_EOF' - {{ garage_metrics_token }} - SECRET_EOF - chmod 600 {{ garage_data_dir }}/config/metrics_token.new - if [ ! -f {{ garage_data_dir }}/config/metrics_token ] \ - || ! cmp -s {{ garage_data_dir }}/config/metrics_token.new {{ garage_data_dir }}/config/metrics_token; then - mv {{ garage_data_dir }}/config/metrics_token.new {{ garage_data_dir }}/config/metrics_token - echo METRICS_TOKEN_CHANGED - else - rm {{ garage_data_dir }}/config/metrics_token.new - fi - register: metrics_token_write - changed_when: "'METRICS_TOKEN_CHANGED' in metrics_token_write.stdout" - no_log: true - - - name: Write garage.toml - ansible.builtin.raw: | - cat > {{ garage_data_dir }}/config/garage.toml.new << 'TOML_EOF' - metadata_dir = "/var/lib/garage/meta" - data_dir = "/var/lib/garage/data" - - # SQLite chosen over LMDB: single-node deployment has no peers to - # rebuild metadata from on unclean-shutdown corruption. SQLite is - # slower but portable and has no LMDB corruption window. - db_engine = "sqlite" - - # 6-hour auto-snapshot of the metadata DB as a backstop against - # corruption; last two snapshots retained. - metadata_auto_snapshot_interval = "6h" - - # Single-zone, single-node: only coherent replication factor. - replication_factor = 1 - - rpc_bind_addr = "[::]:3901" - rpc_secret_file = "/etc/garage/rpc_secret" - - compression_level = 2 - - # 64 MB blocks (default 1 MB; previously 20 MB). On the 5 × 7 TB - # Btrfs RAID 5 volume the per-block fixed cost (SQLite lookup, - # dir traversal, hash verify, zstd decompress, nginx buffering) - # is the dominant overhead per object; larger blocks amortize - # it. block_size is a maximum, so 16 MiB CNPG WAL segments - # still upload as one block each. Only applies to NEW uploads; - # existing objects keep their original chunking until re-uploaded. - block_size = "64M" - - [s3_api] - api_bind_addr = "[::]:3900" - s3_region = "garage" - root_domain = ".s3.{{ inventory_hostname }}" - - [s3_web] - bind_addr = "[::]:3902" - root_domain = ".web.{{ inventory_hostname }}" - - [admin] - api_bind_addr = "[::]:3903" - admin_token_file = "/etc/garage/admin_token" - metrics_token_file = "/etc/garage/metrics_token" - TOML_EOF - if [ ! -f {{ garage_data_dir }}/config/garage.toml ] \ - || ! cmp -s {{ garage_data_dir }}/config/garage.toml.new {{ garage_data_dir }}/config/garage.toml; then - mv {{ garage_data_dir }}/config/garage.toml.new {{ garage_data_dir }}/config/garage.toml - echo GARAGE_TOML_CHANGED - else - rm {{ garage_data_dir }}/config/garage.toml.new - fi - register: garage_toml_write - changed_when: "'GARAGE_TOML_CHANGED' in garage_toml_write.stdout" - - - name: Write docker-compose.yml - ansible.builtin.raw: | - cat > {{ garage_data_dir }}/docker-compose.yml << 'COMPOSE_EOF' - services: - garage: - image: {{ garage_image }} - container_name: garaged - restart: unless-stopped - # HTTP services (3900/3902/3903) bind to loopback; DSM - # reverse proxy fronts and TLS-terminates them. RPC 3901 - # is exposed directly — the Noise handshake is raw TCP, - # so DSM RP cannot front it. rpc_secret gates auth. - ports: - - "3901:3901" - - "127.0.0.1:3900:3900" - - "127.0.0.1:3902:3902" - - "127.0.0.1:3903:3903" - volumes: - - {{ garage_data_dir }}/config/garage.toml:/etc/garage.toml:ro - - {{ garage_data_dir }}/config/rpc_secret:/etc/garage/rpc_secret:ro - - {{ garage_data_dir }}/config/admin_token:/etc/garage/admin_token:ro - - {{ garage_data_dir }}/config/metrics_token:/etc/garage/metrics_token:ro - - {{ garage_data_dir }}/meta:/var/lib/garage/meta - - {{ garage_data_dir }}/data:/var/lib/garage/data - COMPOSE_EOF - changed_when: true - - - name: Pull image and start Garage container - ansible.builtin.shell: | - export PATH=$PATH:/usr/local/bin - cd {{ garage_data_dir }} && docker compose pull && docker compose up -d - become: true - changed_when: true - - # Garage does not reload config on SIGHUP and compose does not notice - # when a bind-mounted host file changes. Force-recreate the container - # when garage.toml OR any of the three secret files changed so the new - # config / rotated tokens are picked up. - - name: Force-recreate garaged when garage.toml or secrets changed - ansible.builtin.shell: | - export PATH=$PATH:/usr/local/bin - cd {{ garage_data_dir }} && docker compose up -d --force-recreate garage - become: true - when: garage_toml_write.changed - or rpc_secret_write.changed - or admin_token_write.changed - or metrics_token_write.changed - changed_when: true - - - name: Wait for Garage to accept CLI calls - ansible.builtin.shell: | - export PATH=$PATH:/usr/local/bin - for i in $(seq 1 30); do - if docker exec garaged /garage status >/dev/null 2>&1; then - exit 0 - fi - sleep 2 - done - exit 1 - become: true - changed_when: false - - - name: Check whether cluster layout is already applied - ansible.builtin.shell: | - export PATH=$PATH:/usr/local/bin - docker exec garaged /garage status - become: true - register: garage_status - changed_when: false - - - name: Apply single-node layout (idempotent — only on first run) - when: "'NO ROLE ASSIGNED' in garage_status.stdout" - ansible.builtin.shell: | - export PATH=$PATH:/usr/local/bin - NODE_ID=$(docker exec garaged /garage node id | head -n 1 | cut -d@ -f1) - docker exec garaged /garage layout assign "$NODE_ID" \ - -z {{ garage_zone }} \ - -c {{ garage_capacity }} \ - -t {{ inventory_hostname_short }} - docker exec garaged /garage layout apply --version 1 - become: true - changed_when: true - - - name: Report Garage status - ansible.builtin.shell: | - export PATH=$PATH:/usr/local/bin - docker exec garaged /garage status - become: true - register: garage_status_final - changed_when: false - - - name: Show Garage status - ansible.builtin.debug: - var: garage_status_final.stdout_lines - - # Bootstrap buckets + keys. Idempotent: bucket_info / key_info return - # non-zero when missing, so we only create on first run. Access-key - # secrets are captured at create time and persisted to chmod-600 files - # under {{ garage_data_dir }}/config/keys/.creds on nas-1. These - # files are the only post-install record of the secrets — Garage does - # not expose them again. - - name: Bootstrap Garage buckets, keys, and grants - ansible.builtin.shell: | - set -e - export PATH=$PATH:/usr/local/bin - KEYS_DIR={{ garage_data_dir }}/config/keys - mkdir -p "$KEYS_DIR" - chmod 700 "$KEYS_DIR" - - bootstrap_pair() { - local BUCKET="$1" - local KEY="$2" - local CREDS_FILE="$KEYS_DIR/$KEY.creds" - - if ! docker exec garaged /garage bucket info "$BUCKET" >/dev/null 2>&1; then - docker exec garaged /garage bucket create "$BUCKET" - fi - - if ! docker exec garaged /garage key info "$KEY" >/dev/null 2>&1; then - CREATE_OUT=$(docker exec garaged /garage key create "$KEY") - KEY_ID=$(echo "$CREATE_OUT" | awk '/^Key ID:/ {print $3}') - SECRET=$(echo "$CREATE_OUT" | awk '/^Secret key:/ {print $3}') - umask 177 - printf 'KEY_ID=%s\nSECRET_KEY=%s\n' "$KEY_ID" "$SECRET" > "$CREDS_FILE" - fi - - docker exec garaged /garage bucket allow \ - --read --write --owner \ - "$BUCKET" --key "$KEY" - } - - bootstrap_pair k8s-velero velero-key - bootstrap_pair k8s-cnpg cnpg-key - bootstrap_pair k8s-secrets secrets-key - bootstrap_pair k8s-migration migration-key - bootstrap_pair tfstate tfstate-key - become: true - changed_when: true - no_log: true - - - name: Report bucket + key state (no secrets) - ansible.builtin.shell: | - export PATH=$PATH:/usr/local/bin - docker exec garaged /garage bucket list - echo "---" - docker exec garaged /garage key list - echo "---" - ls -l {{ garage_data_dir }}/config/keys/ 2>&1 - become: true - register: garage_bootstrap_state - changed_when: false - - - name: Show bucket + key state - ansible.builtin.debug: - var: garage_bootstrap_state.stdout_lines - - - name: Post-install note - ansible.builtin.debug: - msg: - - "Buckets: k8s-velero, k8s-cnpg, k8s-secrets, k8s-migration, tfstate" - - "Keys: velero-key, cnpg-key, secrets-key, migration-key, tfstate-key" - - "Secrets are written to {{ garage_data_dir }}/config/keys/.creds on nas-1 (chmod 600, root-owned)." - - "SSH to nas-1 and `sudo cat` the file to retrieve KEY_ID + SECRET_KEY for Velero / CNPG / migration / tfstate config." diff --git a/ansible/plays/install-pbs-client-synology.yml b/ansible/plays/install-pbs-client-synology.yml deleted file mode 100644 index 2b3e6d8f..00000000 --- a/ansible/plays/install-pbs-client-synology.yml +++ /dev/null @@ -1,104 +0,0 @@ ---- -# Synology-specific playbook using raw module (Python 3.8 workaround) -- name: Install PBS Client on Synology NAS - hosts: synology_nas - gather_facts: false - - vars: - pbs_client_data_dir: "/volume1/docker/pbs-client" - - tasks: - - name: Create PBS client directory - ansible.builtin.raw: | - mkdir -p {{ pbs_client_data_dir }}/state - changed_when: true - - - name: Create docker-compose.yml - ansible.builtin.raw: | - cat > {{ pbs_client_data_dir }}/docker-compose.yml << 'COMPOSE_EOF' - services: - pbs-client: - image: proxmox/proxmox-backup-client:latest - container_name: pbs-client - restart: unless-stopped - hostname: {{ pbs_backup_id }} - env_file: - - .env - volumes: - - {{ pbs_client_data_dir }}/state:/root/.proxmox-backup - {% for path in pbs_backup_paths %} - - "{{ path }}:{{ path }}:ro" - {% endfor %} - mem_limit: 2g - command: ["sleep", "infinity"] - COMPOSE_EOF - changed_when: true - - - name: Create .env file - ansible.builtin.raw: | - cat > {{ pbs_client_data_dir }}/.env << 'ENV_EOF' - PBS_REPOSITORY={{ pbs_username }}@{{ pbs_server_host }}:{{ pbs_server_port }}:{{ pbs_datastore }} - PBS_PASSWORD={{ pbs_password }} - ENV_EOF - chmod 600 {{ pbs_client_data_dir }}/.env - changed_when: true - - - name: Create backup script - ansible.builtin.raw: | - cat > {{ pbs_client_data_dir }}/backup.sh << 'SCRIPT_EOF' - #!/bin/bash - set -e - source {{ pbs_client_data_dir }}/.env - - BACKUP_SOURCES="" - {% for path in pbs_backup_paths %} - BACKUP_SOURCES="$BACKUP_SOURCES {{ path | basename }}.pxar:{{ path }}" - {% endfor %} - - echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting backup for {{ pbs_backup_id }}" - docker exec pbs-client proxmox-backup-client backup \ - $BACKUP_SOURCES \ - --backup-id {{ pbs_backup_id }} \ - --repository "${PBS_REPOSITORY}" - - echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup completed, pruning..." - docker exec pbs-client proxmox-backup-client prune \ - --repository "${PBS_REPOSITORY}" \ - --backup-id {{ pbs_backup_id }} \ - --keep-last {{ pbs_keep_last | default(3) }} \ - --keep-daily {{ pbs_keep_daily | default(7) }} \ - --keep-weekly {{ pbs_keep_weekly | default(4) }} \ - --keep-monthly {{ pbs_keep_monthly | default(6) }} - - echo "[$(date '+%Y-%m-%d %H:%M:%S')] Done" - SCRIPT_EOF - chmod 755 {{ pbs_client_data_dir }}/backup.sh - changed_when: true - - - name: Start PBS client container - ansible.builtin.shell: | - export PATH=$PATH:/usr/local/bin - cd {{ pbs_client_data_dir }} && docker compose pull && docker compose up -d - become: true - changed_when: true - - - name: Setup cron job for daily backup at 2 AM (root crontab) - ansible.builtin.shell: | - # Remove existing entry if present - crontab -l 2>/dev/null | grep -v "pbs-client/backup.sh" | crontab - 2>/dev/null || true - # Add new entry - (crontab -l 2>/dev/null; echo "0 2 * * * {{ pbs_client_data_dir }}/backup.sh >> /var/log/pbs-backup.log 2>&1") | crontab - - become: true - changed_when: true - - - name: Verify container is running - ansible.builtin.shell: | - export PATH=$PATH:/usr/local/bin - docker ps --filter name=pbs-client --format "{{ '{{' }}.Status{{ '}}' }}" - become: true - register: container_status - changed_when: false - - - name: Show container status - ansible.builtin.debug: - msg: "PBS client container status: {{ container_status.stdout }}" diff --git a/ansible/plays/install-pbs-client.yml b/ansible/plays/install-pbs-client.yml deleted file mode 100644 index e6e28570..00000000 --- a/ansible/plays/install-pbs-client.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -- name: Install PBS Client on Synology NAS - hosts: synology_nas - become: true - # Variables are set in: - # - Role defaults: ansible/roles/pbs-client/defaults/main.yml - # - Host overrides: ansible/inventory/host_vars//vars.yml - # - Secrets: ansible/inventory/host_vars//vault.yml - - roles: - - pbs-client diff --git a/ansible/roles/pbs-client/defaults/main.yml b/ansible/roles/pbs-client/defaults/main.yml deleted file mode 100644 index c711c03e..00000000 --- a/ansible/roles/pbs-client/defaults/main.yml +++ /dev/null @@ -1,32 +0,0 @@ ---- -# PBS Client configuration -pbs_client_version: "latest" -pbs_client_data_dir: "/opt/pbs-client" - -# PBS Server connection (override in host_vars) -pbs_server_host: "pbs.obg.renner.uno" -pbs_server_port: 8007 -pbs_datastore: "backups" - -# Authentication (override in vault!) -pbs_username: "backup@pbs" -pbs_password: "" -# pbs_fingerprint: "" # Optional - omit for Let's Encrypt certs - -# Backup configuration -pbs_backup_id: "nas-1" -pbs_backup_paths: - - /volume1/docker - - /volume1/homes - -# Schedule (cron format) -pbs_backup_schedule: "0 2 * * *" # 2 AM daily - -# Retention policy (prune settings) -pbs_keep_last: 3 -pbs_keep_daily: 7 -pbs_keep_weekly: 4 -pbs_keep_monthly: 6 - -# Container resource limits -pbs_memory_limit: "2g" diff --git a/ansible/roles/pbs-client/handlers/main.yml b/ansible/roles/pbs-client/handlers/main.yml deleted file mode 100644 index 9aef8df3..00000000 --- a/ansible/roles/pbs-client/handlers/main.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- name: Restart PBS client - community.docker.docker_compose_v2: - project_src: "{{ pbs_client_data_dir }}" - state: restarted - docker_cli: "{{ docker_cli | default('/usr/local/bin/docker') }}" diff --git a/ansible/roles/pbs-client/tasks/main.yml b/ansible/roles/pbs-client/tasks/main.yml deleted file mode 100644 index 57442fa4..00000000 --- a/ansible/roles/pbs-client/tasks/main.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- -- name: Create PBS client directories - ansible.builtin.file: - path: "{{ item }}" - state: directory - mode: '0755' - loop: - - "{{ pbs_client_data_dir }}" - - "{{ pbs_client_data_dir }}/state" - -- name: Copy PBS client docker-compose file - ansible.builtin.template: - src: docker-compose.yml.j2 - dest: "{{ pbs_client_data_dir }}/docker-compose.yml" - mode: '0644' - notify: Restart PBS client - -- name: Start PBS client via Docker Compose - community.docker.docker_compose_v2: - project_src: "{{ pbs_client_data_dir }}" - state: present - pull: always - docker_cli: "{{ docker_cli | default('/usr/local/bin/docker') }}" diff --git a/ansible/roles/pbs-client/templates/.env.j2 b/ansible/roles/pbs-client/templates/.env.j2 deleted file mode 100644 index 0f78b929..00000000 --- a/ansible/roles/pbs-client/templates/.env.j2 +++ /dev/null @@ -1,6 +0,0 @@ -# PBS Client Environment Configuration -PBS_REPOSITORY={{ pbs_username }}@{{ pbs_server_host }}:{{ pbs_server_port }}:{{ pbs_datastore }} -PBS_PASSWORD={{ pbs_password }} -{% if pbs_fingerprint | default('') != '' %} -PBS_FINGERPRINT={{ pbs_fingerprint }} -{% endif %} diff --git a/ansible/roles/pbs-client/templates/backup.sh.j2 b/ansible/roles/pbs-client/templates/backup.sh.j2 deleted file mode 100644 index 248c1703..00000000 --- a/ansible/roles/pbs-client/templates/backup.sh.j2 +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash -# PBS Backup Script for {{ pbs_backup_id }} -# Managed by Ansible - do not edit manually - -set -e - -# Load environment -source {{ pbs_client_data_dir }}/.env - -# Build backup command with all paths -BACKUP_SOURCES="" -{% for path in pbs_backup_paths %} -BACKUP_SOURCES="$BACKUP_SOURCES {{ path | basename }}.pxar:{{ path }}" -{% endfor %} - -echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting backup for {{ pbs_backup_id }}" - -# Run backup via Docker -docker exec pbs-client proxmox-backup-client backup \ - $BACKUP_SOURCES \ - --backup-id {{ pbs_backup_id }} \ - --repository "${PBS_REPOSITORY}" - -echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup completed" - -# Prune old backups -echo "[$(date '+%Y-%m-%d %H:%M:%S')] Pruning old backups" -docker exec pbs-client proxmox-backup-client prune \ - --repository "${PBS_REPOSITORY}" \ - --backup-id {{ pbs_backup_id }} \ - --keep-last {{ pbs_keep_last }} \ - --keep-daily {{ pbs_keep_daily }} \ - --keep-weekly {{ pbs_keep_weekly }} \ - --keep-monthly {{ pbs_keep_monthly }} - -echo "[$(date '+%Y-%m-%d %H:%M:%S')] Prune completed" diff --git a/ansible/roles/pbs-client/templates/docker-compose.yml.j2 b/ansible/roles/pbs-client/templates/docker-compose.yml.j2 deleted file mode 100644 index 2a2f57a0..00000000 --- a/ansible/roles/pbs-client/templates/docker-compose.yml.j2 +++ /dev/null @@ -1,30 +0,0 @@ -services: - pbs-client: - image: aterfax/pbs-client:{{ pbs_client_version }} - container_name: pbs-client - restart: unless-stopped - hostname: {{ pbs_backup_id }} - environment: - - PBS_ENDPOINT={{ pbs_server_host }}:{{ pbs_server_port }} - - PBS_DATASTORE={{ pbs_datastore }} - - PBS_API_KEY_NAME={{ pbs_username }} - - PBS_API_KEY_SECRET={{ pbs_password }} - - PBS_PASSWORD={{ pbs_password }} - - PBS_ENCRYPTION_PASSWORD={{ pbs_encryption_password | default(pbs_password) }} - - PBS_REPOSITORY={{ pbs_username }}@{{ pbs_server_host }}:{{ pbs_server_port }}:{{ pbs_datastore }} - - PBS_BACKUP_ID={{ pbs_backup_id }} - - CRON_SCHEDULE={{ pbs_backup_schedule }} - - PBS_BACKUP_CMD_APPEND_EXTRA_OPTS=--crypt-mode=none - - PRUNE_KEEP_LAST={{ pbs_keep_last | default(3) }} - - PRUNE_KEEP_DAILY={{ pbs_keep_daily | default(7) }} - - PRUNE_KEEP_WEEKLY={{ pbs_keep_weekly | default(4) }} - - PRUNE_KEEP_MONTHLY={{ pbs_keep_monthly | default(6) }} - - CRON_BACKUP_ONLY=1 - volumes: - - {{ pbs_client_data_dir }}/state:/root/.config/proxmox-backup -{% for path in pbs_backup_paths %} - - "{{ path }}:/backups/{{ path | basename | replace(' ', '_') }}:ro" -{% endfor %} - tmpfs: - - /tmp - mem_limit: {{ pbs_memory_limit }} -- 2.54.0 From b8125e46694b53843c3fe079ac29280d13bb6870 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Fri, 28 Aug 2026 10:45:11 +0200 Subject: [PATCH 3/3] fix(ci): make an incomplete image scan visible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scan loop swallowed every Trivy failure with `|| true`, so a step that scanned 21 of 22 images reported exactly the same as one that scanned all 22. The gap was found by the fix agent noticing a missing file in its own input, not by the pipeline. The loop still continues past a bad image — one unpullable reference should not cost the scan of everything after it — but now counts what failed, names it, and exits non-zero: --- scanned 21 of 22 images --- NOT SCANNED: proxmox/proxmox-backup-client:latest `failure: ignore` stays, which per Woodpecker's semantics reports the step as failed while letting the workflow continue. That is deliberate: coverage is reported, not enforced. The step goes red and the fix agent still runs on what was scanned. Making the whole pipeline fail would mean one unscannable image blocks all remediation. The image that prompted this is gone with the nas-1 removal in the previous commit, so the next run should report 20 of 20. The accounting is what matters — the next dead reference will announce itself instead of being absorbed. Refs #694 --- .woodpecker/security-scan.yml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.woodpecker/security-scan.yml b/.woodpecker/security-scan.yml index dc6a91fa..c0fcea0f 100644 --- a/.woodpecker/security-scan.yml +++ b/.woodpecker/security-scan.yml @@ -40,12 +40,28 @@ steps: cat /tmp/images.txt echo "---" i=0 + failed=0 + failed_names="" while IFS= read -r img; do i=$$((i+1)) out="trivy-image-$$i.json" echo "=== [$$i] $$img -> $$out ===" - trivy image --skip-db-update --scanners vuln --timeout 15m --severity CRITICAL,HIGH --ignore-unfixed --format json -o "$$out" "$$img" || true + # Keep going on a bad image so one unpullable reference cannot cost + # the scan of every image after it, but record that it happened. + if ! trivy image --skip-db-update --scanners vuln --timeout 15m --severity CRITICAL,HIGH --ignore-unfixed --format json -o "$$out" "$$img"; then + failed=$$((failed+1)) + failed_names="$$failed_names $$img" + fi done < /tmp/images.txt + echo "--- scanned $$((i - failed)) of $$i images ---" + if [ "$$failed" -gt 0 ]; then + echo "NOT SCANNED:$$failed_names" + exit 1 + fi + # Non-zero above turns this step red without failing the workflow, so the + # fix agent still runs on what was scanned. Coverage is reported, not + # enforced: an incomplete scan is visible in the step status instead of + # being indistinguishable from a clean one. failure: ignore - name: security-fix-agent -- 2.54.0