Compare commits

...
Author SHA1 Message Date
claude-botandClaude Opus 5.5 570c93559b feat(paperless): restrict FTP and Syncthing LB ingress to their client networks
ci/woodpecker/pr/ci-manifests-test Pipeline is running
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ftp-scanner and syncthing-sync accepted any world source.

- ftp-scanner: IoT and Privat. Over the last 7 days the vsftpd log shows
  uploads from the BR5C scanner via fd02:30:: and the Brother via
  10.2.50.170.
- syncthing-sync: Privat and VPN. Over the last 7 days peers connected
  from 10.2.50.0/24 and fd02:50::/64.

host and remote-node stay allowed. Both LBs are ETP=Local.

Refs S28-19

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 11:18:11 +01:00
k.renner 0ab63f46d4 Merge pull request 'feat(trmnl): restrict device HTTP ingress to the IoT network' (#1584) from feat/trmnl-device-cidr into main
Reviewed-on: #1584
2026-11-07 07:00:20 +00:00
claude-botandClaude Opus 5.5 cbc538ec7d feat(trmnl): restrict device HTTP ingress to the IoT network
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
allow-device-http admitted world on 2345/TCP. The TRMNL panel lives on
IoT (terminus log: GET /api/display from 10.2.30.244 every ~5 min) and
terminus-device is ETP=Local, so the client address reaches the pod.
Source is now net-iot; host/remote-node stay for probes.

Depends on #1583 (CiliumCIDRGroup net-iot).

Refs S28-19

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:54:41 +01:00
k.renner c541190e8e Merge pull request 'feat(network-policies): CiliumCIDRGroup per client network' (#1583) from feat/cidr-groups-per-vlan into main
Reviewed-on: #1583
2026-11-06 14:47:46 +00:00
claude-botandClaude Opus 5.5 6de87eba4c feat(network-policies): CiliumCIDRGroup per client network
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
One group per UDM client network (ULA + IPv4; ISP-delegated GUA
prefixes are not stable and are left out), so LoadBalancer CNPs can
restrict sources via fromCIDRSet.cidrGroupRef instead of repeating
subnets. Adds no policy; no traffic changes.

Refs S28-19

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:45:15 +01:00
k.renner 2beb9a4e14 Merge pull request 'docs(inventory): ntp_cluster has two members, edge-1 and edge-2' (#1582) from docs/ntp-cluster-edge2 into main
Reviewed-on: #1582
2026-11-06 13:51:17 +00:00
k.renner 767b3cdac4 Merge pull request 'docs(hairpin): list k8s VLANs 46/47/48 as deliberately absent' (#1581) from docs/hairpin-absent-vlans into main
ci/woodpecker/push/deploy-infrastructure Pipeline was successful
Reviewed-on: #1581
2026-11-06 13:50:58 +00:00
claude-botandClaude Opus 5.5 ccb69bb109 docs(inventory): ntp_cluster has two members, edge-1 and edge-2
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
edge-2 is in [edge_host] and in the ntp_servers map of install-ntp-ha.yml
(BACKUP, prio 60) and answers NTP (stratum 2) on 10.2.30.184. The comment
still called edge-1 the only member.

Refs S28-14

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:46:43 +01:00
claude-botandClaude Opus 5.5 69f7075c7c docs(hairpin): list k8s VLANs 46/47/48 as deliberately absent
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
The nodes hold addresses on 46 (node0), 47 (ingress0) and 48 (pod0),
the same asymmetric-reply reason that excludes 40 and 49. (42) Ingress
no longer exists; its idx slot stays reserved.

Refs S28-13

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:38:37 +01:00
k.renner 1b06a922a9 Merge pull request 'fix(pacman-cache): empty the pacman package cache before the edge image pulls' (#1580) from fix/common-pacman-cache into main
Reviewed-on: #1580
2026-11-06 13:01:23 +00:00
claude-botandClaude Opus 5.5 d457c7e639 refactor(pacman-cache): own role instead of common
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
The pacman cache policy is its own concern, not base configuration of
every host. The new role pacman-cache runs in install-edge-host after
common and before docker-install; common is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:45:30 +01:00
claude-botandClaude Opus 5.5 a29e10e313 fix(common): empty the pacman package cache in the base play
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
The cache cleanup from #1579 sat in os-update, which site.yml does not
run, so site.yml still failed on edge-1 with "no space left on device"
when pulling the Alloy image. The hook and the cleanup move to common,
which site.yml runs on every host before install-edge-host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:26:34 +01:00
k.renner 15d98d2702 Merge pull request 'fix(os-update): keep the pacman package cache empty' (#1579) from fix/edge-pacman-cache into main
ci/woodpecker/cron/e2e Pipeline failed
ci/woodpecker/cron/security-scan Pipeline was successful
Reviewed-on: #1579
2026-11-06 12:03:23 +00:00
10 changed files with 227 additions and 58 deletions
+5 -14
View File
@@ -49,20 +49,11 @@ edge-1.iot.renner.uno
zwave_gateway
heating_can
# NTP servers. edge-1 was added here so the role would survive the PVE hosts
# being retired for bare-metal Talos, which cannot run chrony or keepalived
# itself. That retirement has now happened, so edge-1 is the ONLY member.
#
# THIS IS NO LONGER HIGHLY AVAILABLE. The group was jupiter (MASTER, prio 100),
# saturn (90), neptune (80) and edge-1 (BACKUP, 70); the first three are gone.
# A single VRRP member takes the address regardless of its configured state, so
# the service address is still served, but by one thin client with
# no failover behind it. install-ntp-ha.yml still carries the four-host
# ntp_servers priority map, which now describes hosts that do not exist.
#
# Restoring redundancy needs a second chrony host that is not a Talos node.
# See plays/install-ntp-ha.yml for why keepalived is scoped more narrowly than
# chrony within this group.
# NTP servers: every edge host (edge-1 BACKUP prio 70, edge-2 BACKUP prio 60;
# see the ntp_servers map in plays/install-ntp-ha.yml). Talos nodes cannot run
# chrony or keepalived. The jupiter/saturn/neptune entries in that map
# describe retired PVE hosts. See plays/install-ntp-ha.yml for why keepalived
# is scoped more narrowly than chrony within this group.
[ntp_cluster:children]
edge_host
+1
View File
@@ -34,6 +34,7 @@
roles:
- certificates
- common
- pacman-cache
- edge-flash-wear
- docker-install
- edge-log-shipper
-28
View File
@@ -25,34 +25,6 @@
check_mode: false
when: os_update_docker.stat.exists
# pacman keeps every downloaded package; on the edge hosts' 7 GB eMMC root
# the cache grows to gigabytes.
- name: Ensure the pacman hook directory exists (Arch)
ansible.builtin.file:
path: /etc/pacman.d/hooks
state: directory
mode: "0755"
when: ansible_facts['os_family'] == 'Archlinux'
- name: Empty the package cache after every transaction (Arch)
ansible.builtin.copy:
src: pacman-cache.hook
dest: /etc/pacman.d/hooks/pacman-cache.hook
mode: "0644"
when: ansible_facts['os_family'] == 'Archlinux'
- name: Empty the package cache (Arch)
ansible.builtin.command:
argv: [find, /var/cache/pacman/pkg, -mindepth, "1", -delete, -print]
register: os_update_pacman_cache
changed_when: os_update_pacman_cache.stdout | length > 0
when: ansible_facts['os_family'] == 'Archlinux'
- name: Re-read mounts after the cache cleanup
ansible.builtin.setup:
gather_subset: [mounts]
when: os_update_pacman_cache is changed
- name: Assert at least 1 GiB free on /
ansible.builtin.assert:
that: >-
@@ -1,4 +1,4 @@
# Managed by ansible (roles/os-update).
# Managed by ansible (roles/pacman-cache).
# Downloaded packages are not kept; older versions come from the Arch Linux
# Archive.
[Trigger]
+21
View File
@@ -0,0 +1,21 @@
---
# Keep the pacman package cache empty. pacman keeps every downloaded package;
# on a 7 GB eMMC root the cache grows to gigabytes. Arch hosts only.
- name: Ensure the pacman hook directory exists
ansible.builtin.file:
path: /etc/pacman.d/hooks
state: directory
mode: "0755"
- name: Empty the package cache after every transaction
ansible.builtin.copy:
src: pacman-cache.hook
dest: /etc/pacman.d/hooks/pacman-cache.hook
mode: "0644"
- name: Empty the package cache
ansible.builtin.command:
argv: [find, /var/cache/pacman/pkg, -mindepth, "1", -delete, -print]
register: pacman_cache_clean
changed_when: pacman_cache_clean.stdout | length > 0
@@ -0,0 +1,150 @@
# Client networks as Cilium CIDR groups, referenced from CNPs via
# fromCIDRSet.cidrGroupRef. ULA + IPv4 only: the ISP-delegated GUA prefixes
# change and are not listed.
---
apiVersion: cilium.io/v2
kind: CiliumCIDRGroup
metadata:
name: net-default
spec:
externalCIDRs:
- fd00::/64
- 192.168.8.0/24
---
apiVersion: cilium.io/v2
kind: CiliumCIDRGroup
metadata:
name: net-management
spec:
externalCIDRs:
- fd02:10::/64
- 10.2.10.0/24
---
apiVersion: cilium.io/v2
kind: CiliumCIDRGroup
metadata:
name: net-radius
spec:
externalCIDRs:
- fd02:11::/64
- 10.2.11.0/24
---
apiVersion: cilium.io/v2
kind: CiliumCIDRGroup
metadata:
name: net-video
spec:
externalCIDRs:
- 10.2.20.0/24
---
apiVersion: cilium.io/v2
kind: CiliumCIDRGroup
metadata:
name: net-iot
spec:
externalCIDRs:
- fd02:30::/64
- 10.2.30.0/24
---
apiVersion: cilium.io/v2
kind: CiliumCIDRGroup
metadata:
name: net-server
spec:
externalCIDRs:
- fd02:40::/64
- fd40::/64
- 10.2.40.0/24
---
apiVersion: cilium.io/v2
kind: CiliumCIDRGroup
metadata:
name: net-k8s-node
spec:
externalCIDRs:
- fd02:46::/64
- 10.2.46.0/24
---
apiVersion: cilium.io/v2
kind: CiliumCIDRGroup
metadata:
name: net-k8s-ingress
spec:
externalCIDRs:
- fd02:47::/64
- 10.2.47.0/24
---
apiVersion: cilium.io/v2
kind: CiliumCIDRGroup
metadata:
name: net-k8s-pod
spec:
externalCIDRs:
- fd02:48::/64
- 10.2.48.0/24
---
apiVersion: cilium.io/v2
kind: CiliumCIDRGroup
metadata:
name: net-privat
spec:
externalCIDRs:
- fd02:50::/64
- 10.2.50.0/24
---
apiVersion: cilium.io/v2
kind: CiliumCIDRGroup
metadata:
name: net-phone
spec:
externalCIDRs:
- 10.2.60.0/24
---
apiVersion: cilium.io/v2
kind: CiliumCIDRGroup
metadata:
name: net-access
spec:
externalCIDRs:
- 10.2.70.0/24
---
apiVersion: cilium.io/v2
kind: CiliumCIDRGroup
metadata:
name: net-vpn
spec:
externalCIDRs:
- 10.2.80.0/24
- 192.168.32.0/24
---
apiVersion: cilium.io/v2
kind: CiliumCIDRGroup
metadata:
name: net-guest
spec:
externalCIDRs:
- 10.2.90.0/24
---
apiVersion: cilium.io/v2
kind: CiliumCIDRGroup
metadata:
name: net-multi-transfer
spec:
externalCIDRs:
- 10.2.122.0/24
---
apiVersion: cilium.io/v2
kind: CiliumCIDRGroup
metadata:
name: net-multi-lan
spec:
externalCIDRs:
- 10.2.123.0/24
---
apiVersion: cilium.io/v2
kind: CiliumCIDRGroup
metadata:
name: net-mutti
spec:
externalCIDRs:
- 10.2.200.0/24
@@ -1,6 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- cidr-groups.yaml
- default-deny.yaml
- allow-ingress.yaml
- ingress-isolation.yaml
@@ -112,7 +112,7 @@ spec:
- { type: 136, family: IPv6 }
- { type: 143, family: IPv6 }
---
# Allow FTP access (from scanners on the LAN) to the ftp-scanner pod.
# FTP from the scanners (IoT and Privat) to the ftp-scanner pod.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
@@ -123,8 +123,24 @@ spec:
matchLabels:
app.kubernetes.io/name: ftp-scanner
ingress:
- fromCIDRSet:
- cidrGroupRef: net-iot
- cidrGroupRef: net-privat
toPorts:
- ports:
- port: "21"
protocol: TCP
- port: "21100"
protocol: TCP
- port: "21101"
protocol: TCP
- port: "21102"
protocol: TCP
- port: "21103"
protocol: TCP
- port: "21104"
protocol: TCP
- fromEntities:
- world
- host
- remote-node
toPorts:
@@ -142,7 +158,7 @@ spec:
- port: "21104"
protocol: TCP
---
# Allow Syncthing peer-to-peer sync from the LAN/world.
# Allow Syncthing peer-to-peer sync from Privat and VPN.
# - 22000 TCP+QUIC: actual sync traffic between peers.
# - 21027 UDP: local-discovery broadcast packets from peers on the LAN.
# UI (8384) is intentionally NOT listed — authentik-gate-sync generates the
@@ -157,8 +173,18 @@ spec:
matchLabels:
app.kubernetes.io/name: syncthing
ingress:
- fromCIDRSet:
- cidrGroupRef: net-privat
- cidrGroupRef: net-vpn
toPorts:
- ports:
- port: "22000"
protocol: TCP
- port: "22000"
protocol: UDP
- port: "21027"
protocol: UDP
- fromEntities:
- world
- host
- remote-node
toPorts:
@@ -62,9 +62,10 @@ spec:
# CA, so this bypasses Traefik entirely and the namespace default-deny has
# to be opened for it — same shape as paperless's allow-ftp-scanner.
#
# Scoped to the terminus pods and port 2345 only. Requests still have to
# carry a known device MAC in the `ID` header plus its api_key; this rule
# opens the port, not the application.
# Scoped to the terminus pods, port 2345 and the IoT network (the panel's
# network; the LB is ETP=Local, so the client address reaches the pod).
# Requests still have to carry a known device MAC in the `ID` header plus
# its api_key; this rule opens the port, not the application.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
@@ -75,8 +76,13 @@ spec:
matchLabels:
app.kubernetes.io/name: terminus
ingress:
- fromCIDRSet:
- cidrGroupRef: net-iot
toPorts:
- ports:
- port: "2345"
protocol: TCP
- fromEntities:
- world
- host
- remote-node
toPorts:
+8 -7
View File
@@ -68,13 +68,14 @@ locals {
# idx is a static per-network slot for rule_index derivation — never
# reuse a freed idx for a new network.
#
# Deliberately absent: (40) Server idx=4, (42) Ingress idx=5, and
# (49) k8s Ceph idx=6. The cluster nodes have interfaces on those VLANs,
# so a DNATed flow's reply would go directly over the shared L2 and
# bypass the UDM's conntrack (asymmetric, connection drops) — and
# clients there are the nodes themselves, whose DNS chain (CoreDNS ->
# k8s_gateway) already resolves the public names in-cluster. Their idx
# slots stay reserved.
# Deliberately absent: (40) Server idx=4, (49) k8s Ceph idx=6,
# (46) k8s Node, (47) k8s Ingress and (48) k8s Pod. The cluster nodes
# have interfaces on those VLANs (mgmt0, node0, ingress0, pod0), so a
# DNATed flow's reply would go directly over the shared L2 and bypass
# the UDM's conntrack (asymmetric, connection drops) — and clients there
# are the nodes themselves, whose DNS chain (CoreDNS -> k8s_gateway)
# already resolves the public names in-cluster. idx=5 stays reserved
# (its network, (42) Ingress, no longer exists).
hairpin_networks = {
"Default" = { id = "5d4fba77ce63ee614841d53d", idx = 0 }
"(10) Management" = { id = "600334abf9ee0c059da6be8f", idx = 1 }