Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e66d793d5 | ||
|
|
0996609ce4 | ||
|
|
4b27a5f273 | ||
|
|
ea195d0e4f | ||
|
|
1ae259e474 | ||
|
|
796d8459b6 | ||
|
|
a6be8247eb | ||
|
|
826042f3ea | ||
|
|
aa2543369c | ||
|
|
5add98190f | ||
|
|
1643b305b2 | ||
|
|
657e3d0d92 | ||
|
|
ac2d5d13a1 | ||
|
|
ed3be3156b | ||
|
|
335c423ca6 |
@@ -35,25 +35,25 @@ when:
|
||||
steps:
|
||||
- name: model-selection-tests
|
||||
depends_on: []
|
||||
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.316
|
||||
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.318
|
||||
commands:
|
||||
- sh scripts/ci/test-model-selection.sh
|
||||
|
||||
- name: pr-review-mistral-tests
|
||||
depends_on: []
|
||||
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.316
|
||||
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.318
|
||||
commands:
|
||||
- sh scripts/ci/test-pr-review-mistral.sh
|
||||
|
||||
- name: promtool-rules-guard-tests
|
||||
depends_on: []
|
||||
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.316
|
||||
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.318
|
||||
commands:
|
||||
- sh scripts/ci/test-promtool-rules.sh
|
||||
|
||||
- name: yaml-citations-guard-tests
|
||||
depends_on: []
|
||||
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.316
|
||||
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.318
|
||||
commands:
|
||||
- sh scripts/ci/test-yaml-citations.sh
|
||||
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ steps:
|
||||
# fetch-models.sh response mapping. No cluster, no secrets, no network — the
|
||||
# ci-claude image just provides sh + jq. Keeps the flag-injection regression
|
||||
# test from rotting (it exercises the exact injection shapes).
|
||||
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.316
|
||||
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.318
|
||||
commands:
|
||||
- sh scripts/ci/test-model-selection.sh
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ steps:
|
||||
# this step is skipped — otherwise we'd fork-bomb PRs.
|
||||
when:
|
||||
- branch: main
|
||||
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.316
|
||||
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.318
|
||||
environment:
|
||||
CLAUDE_CODE_OAUTH_TOKEN:
|
||||
from_secret: claude_code_oauth_token
|
||||
|
||||
@@ -30,7 +30,7 @@ when:
|
||||
|
||||
steps:
|
||||
- name: update-audit-agent
|
||||
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.316
|
||||
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.318
|
||||
environment:
|
||||
CLAUDE_CODE_OAUTH_TOKEN:
|
||||
from_secret: claude_code_oauth_token
|
||||
|
||||
@@ -134,11 +134,13 @@ Afterwards:
|
||||
|
||||
## Day-2 notes
|
||||
|
||||
- Image bumps: edit `edge_zwavejs_image` / `edge_sml2mqtt_image` in
|
||||
defaults and re-run the play (no flux image-automation out here —
|
||||
deliberate for sml2mqtt, acceptable for zwave-js). If a zwave-js bump
|
||||
changes its Alpine major, also update `edge_zwave_alpine_base` and refresh
|
||||
the preloaded `.so` (see "Logging & eMMC" → zwave-js).
|
||||
- Image bumps: Renovate opens a PR for `edge_*_image` in
|
||||
`roles/*/defaults/main.yml` (tag + digest, `customManagers` in
|
||||
`renovate.json`). No pipeline deploys to edge-1, so after the merge run
|
||||
`ansible-playbook plays/install-zwave-gateway.yml` from `ansible/` on a
|
||||
mesh-connected workstation. The play reads the image's Alpine release
|
||||
itself and rebuilds the preloaded `.so` when it changes (see
|
||||
"Logging & eMMC" → zwave-js).
|
||||
- open3e / E3onCAN updates: both are pinned to commits in
|
||||
`roles/heating-can/defaults/main.yml`, so re-running the play does NOT pick
|
||||
up upstream changes -- that is deliberate, these drive the heat pump. The
|
||||
@@ -190,7 +192,7 @@ journal commit).
|
||||
|
||||
- **Fix:** the `zwave-js` container preloads **`libeatmydata`**
|
||||
(`LD_PRELOAD=/usr/lib/libeatmydata.so`, mounted read-only from
|
||||
`zwave-js/libeatmydata.so`). This makes `fsync`/`fdatasync` no-ops *for that
|
||||
`zwave-js/libeatmydata-alpine<X.Y>.so`). This makes `fsync`/`fdatasync` no-ops *for that
|
||||
one process*, so the kernel batches the writes via normal writeback (~30 s)
|
||||
instead of forcing each one. The WS/MQTT value stream to Home Assistant and
|
||||
the on-disk cache are **unchanged** — full data resolution, no re-interview
|
||||
@@ -199,9 +201,9 @@ journal commit).
|
||||
batch it); the only mount-level alternative is putting the cache on tmpfs,
|
||||
which makes the *whole* store volatile → re-interview on every restart.
|
||||
- **The `.so` is musl** (the image is Alpine-based), extracted from
|
||||
`edge_zwave_alpine_base` — keep that var in sync with the image's Alpine
|
||||
version; to refresh after a major bump, `rm zwave-js/libeatmydata.so` and
|
||||
re-run.
|
||||
`alpine:<X.Y>`, where `X.Y` is read from the image's `/etc/alpine-release`
|
||||
on every run. A new Alpine release gives a new file name, so the play
|
||||
extracts it, recreates the container and deletes the old file.
|
||||
- **Durability guard:** because `fsync` is neutralized, a hard power-loss
|
||||
during the (rare) settings write could truncate `settings.json`, which holds
|
||||
the **Z-Wave network keys**. `edge-zwave-settings-backup.timer` keeps a
|
||||
|
||||
@@ -13,12 +13,8 @@ zwave_base_dir: "{{ edge_base_dir }}/zwave-js"
|
||||
# survives moving the stick between machines.
|
||||
edge_zwave_device: /dev/serial/by-id/usb-Nabu_Casa_ZWA-2_80B54EE15720-if00
|
||||
|
||||
edge_zwavejs_image: ghcr.io/zwave-js/zwave-js-ui:11.21.1
|
||||
|
||||
# Alpine version the zwave-js-ui image is built on -- the source for the musl
|
||||
# libeatmydata.so preloaded into that container. Keep in sync if the image
|
||||
# bumps its Alpine major (check: docker exec zwave-js cat /etc/os-release).
|
||||
edge_zwave_alpine_base: alpine:3.22
|
||||
# Renovate-tracked (customManagers in renovate.json).
|
||||
edge_zwavejs_image: ghcr.io/zwave-js/zwave-js-ui:11.25.2@sha256:ac4874a3cbea61c0c60002a8fd8ae05104cc8eb2eae91aa05bd184972aa06853
|
||||
|
||||
# Secret from inventory/group_vars/edge_host/vault.yml:
|
||||
# edge_zwavejs_session_secret
|
||||
|
||||
@@ -15,17 +15,31 @@
|
||||
- "{{ zwave_base_dir }}"
|
||||
- "{{ zwave_base_dir }}/store"
|
||||
|
||||
# The preloaded .so is musl and must come from the image's own Alpine release.
|
||||
- name: Read the Alpine release of the zwave-js-ui image
|
||||
ansible.builtin.command:
|
||||
cmd: docker run --rm --entrypoint cat {{ edge_zwavejs_image }} /etc/alpine-release
|
||||
register: zwave_alpine_release
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
|
||||
- name: Name libeatmydata.so after that Alpine release
|
||||
ansible.builtin.set_fact:
|
||||
zwave_alpine_minor: "{{ zwave_alpine_release.stdout.split('.')[:2] | join('.') }}"
|
||||
zwave_libeatmydata: "libeatmydata-alpine{{ zwave_alpine_release.stdout.split('.')[:2] | join('.') }}.so"
|
||||
|
||||
- name: Extract musl libeatmydata.so (matches the zwave-js-ui Alpine base)
|
||||
ansible.builtin.shell:
|
||||
cmd: |
|
||||
set -euo pipefail
|
||||
tmp="$(mktemp)"
|
||||
trap 'rm -f "$tmp"' EXIT
|
||||
docker run --rm --network=host {{ edge_zwave_alpine_base }} \
|
||||
docker run --rm --network=host alpine:{{ zwave_alpine_minor }} \
|
||||
sh -c 'apk add --no-cache libeatmydata >/dev/null 2>&1 && cat /usr/lib/libeatmydata.so' > "$tmp"
|
||||
test "$(stat -c%s "$tmp")" -ge 8000 # sanity: real .so is ~14 KB, guards against a failed/empty pull
|
||||
install -m 0644 "$tmp" {{ zwave_base_dir }}/libeatmydata.so
|
||||
creates: "{{ zwave_base_dir }}/libeatmydata.so"
|
||||
install -m 0644 "$tmp" {{ zwave_base_dir }}/{{ zwave_libeatmydata }}
|
||||
creates: "{{ zwave_base_dir }}/{{ zwave_libeatmydata }}"
|
||||
executable: /bin/bash
|
||||
notify: Restart zwave-js
|
||||
|
||||
# Because fsync is neutralized, a hard power-loss during the (rare) settings
|
||||
@@ -93,3 +107,18 @@
|
||||
zwave-js could not start -- the ZWA-2 radio is not present:
|
||||
{{ zwave_compose_up.stderr_lines | select('search', 'device') | list }}
|
||||
when: zwave_compose_up.rc != 0
|
||||
|
||||
- name: Find libeatmydata.so builds of other Alpine releases
|
||||
ansible.builtin.find:
|
||||
paths: "{{ zwave_base_dir }}"
|
||||
patterns: "libeatmydata*.so"
|
||||
excludes: "{{ zwave_libeatmydata }}"
|
||||
register: zwave_stale_libeatmydata
|
||||
|
||||
- name: Remove libeatmydata.so builds of other Alpine releases
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
state: absent
|
||||
loop: "{{ zwave_stale_libeatmydata.files }}"
|
||||
loop_control:
|
||||
label: "{{ item.path }}"
|
||||
|
||||
@@ -31,7 +31,7 @@ services:
|
||||
- "{{ edge_zwave_device }}:/dev/zwave"
|
||||
volumes:
|
||||
# musl libeatmydata.so extracted from the image's Alpine base (ro)
|
||||
- ./libeatmydata.so:/usr/lib/libeatmydata.so:ro
|
||||
- ./{{ zwave_libeatmydata }}:/usr/lib/libeatmydata.so:ro
|
||||
# Persistent store: settings.json, device DB, network keys. Path is
|
||||
# unchanged from the previous shared project, so nothing is migrated.
|
||||
- ./store:/usr/src/app/store
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24-alpine@sha256:8a32c8cf649a49915a66e81e7a609223c708d0adabf288ee3f4077d765542f85
|
||||
FROM node:26-alpine@sha256:c6ad9e9c1d80f6eb607c26b4b0a132ff7a2344fad1c78ea3baa2a7d4c8de6d94
|
||||
|
||||
# Default to the latest Claude Code. Pinning a fixed version is the wrong
|
||||
# default here: old releases eventually stop working against the API, so a
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PayloadContent</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.wifi.managed</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>uno.renner.wifi.renner-test.network</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>c7814579-fc2b-4f38-bcc8-77e1123ab710</string>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>Renner Test Wi-Fi</string>
|
||||
<key>PayloadOrganization</key>
|
||||
<string>Renner</string>
|
||||
<key>SSID_STR</key>
|
||||
<string>Renner Test</string>
|
||||
<key>HIDDEN_NETWORK</key>
|
||||
<false/>
|
||||
<key>AutoJoin</key>
|
||||
<true/>
|
||||
<key>EncryptionType</key>
|
||||
<string>Any</string>
|
||||
<key>EAPClientConfiguration</key>
|
||||
<dict>
|
||||
<key>OuterIdentity</key>
|
||||
<string>anonymous</string>
|
||||
<key>AcceptEAPTypes</key>
|
||||
<array>
|
||||
<integer>21</integer>
|
||||
</array>
|
||||
<key>TTLSInnerAuthentication</key>
|
||||
<string>PAP</string>
|
||||
<key>TLSTrustedServerNames</key>
|
||||
<array>
|
||||
<string>radius.renner.uno</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>PayloadType</key>
|
||||
<string>Configuration</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>uno.renner.wifi.renner-test</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>b35513d4-97e5-4804-a77b-5ca1f723a408</string>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>Renner Test Wi-Fi (802.1X)</string>
|
||||
<key>PayloadOrganization</key>
|
||||
<string>Renner</string>
|
||||
</dict>
|
||||
</plist>
|
||||
+11
-6
@@ -143,7 +143,13 @@ For IPv6 set `LIVE_SOURCE` to the workstation's `fd02:50::` address and use
|
||||
`fd99::91`. `group-id=[3530]` is VLAN `50`, `tunnel-type=[0000000d]` VLAN,
|
||||
`medium=[00000006]` IEEE-802.
|
||||
|
||||
## Client setup (SSID "Renner Test")
|
||||
## Client setup (SSID "Renner UniFi")
|
||||
|
||||
Setup page: `https://wlan.renner.uno` (behind the Authentik gate, any
|
||||
authentik user; `kubernetes/apps/authentik/wlan-setup*.yaml`). It serves the
|
||||
Apple profile (caller's username filled in), a Windows file that imports the
|
||||
WLAN profile with `netsh`, and the Linux `nmcli` command. No file carries a password. Android has no
|
||||
import path without Passpoint or an app; it uses the manual values below.
|
||||
|
||||
All clients use EAP-TTLS with inner PAP. The server certificate is a public
|
||||
Let's Encrypt certificate for `radius.renner.uno`, so no CA needs to be
|
||||
@@ -175,7 +181,7 @@ inside the TLS tunnel to FreeRADIUS and is never sent in clear over the air.
|
||||
|
||||
Settings > Network & Internet > Wi-Fi > Manage known networks > Add network:
|
||||
|
||||
- Network name: `Renner Test`
|
||||
- Network name: `Renner UniFi`
|
||||
- Security type: WPA2-Enterprise AES
|
||||
- EAP method: EAP-TTLS
|
||||
- Authentication method: Unencrypted password (PAP)
|
||||
@@ -191,10 +197,9 @@ wireless network" instead.
|
||||
|
||||
### Apple (iOS, iPadOS, macOS)
|
||||
|
||||
Apple devices need the configuration profile
|
||||
`docs/radius/clients/renner-test.mobileconfig`. Install it via AirDrop, Files
|
||||
or Settings (Profile Downloaded). The device asks for username and password on
|
||||
the first join. The profile sets security type Any (WPA2/WPA3), outer identity
|
||||
Apple devices need a configuration profile; the setup page serves it with the
|
||||
username filled in. Install it via Settings (Profile Downloaded). The device
|
||||
asks for the password on the first join. The profile sets security type Any (WPA2/WPA3), outer identity
|
||||
`anonymous` and trusted server name `radius.renner.uno`; it contains no
|
||||
password and no certificate. Apple
|
||||
devices may show a one-time "trust certificate" prompt for `radius.renner.uno`
|
||||
|
||||
@@ -16,6 +16,8 @@ resources:
|
||||
- freeradius-network-attachment.yaml
|
||||
- freeradius.yaml
|
||||
- freeradius-cnp.yaml
|
||||
- wlan-setup-config.yaml
|
||||
- wlan-setup.yaml
|
||||
# ak-pg CNPG Postgres backend (moved here from the retired seed-authentik KS;
|
||||
# each carries prune: disabled so ownership transferred without a teardown).
|
||||
# authentik has no data PVCs — all state lives in ak-pg. This KS depends on
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
# wlan.renner.uno: setup files for "Renner UniFi" (802.1X, EAP-TTLS/PAP
|
||||
# against freeradius, outer identity anonymous, server name radius.renner.uno).
|
||||
# Served by wlan-setup (wlan-setup.yaml) behind the Authentik gate; the
|
||||
# username comes from X-authentik-username. No file carries a password.
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: wlan-setup
|
||||
namespace: authentik
|
||||
data:
|
||||
default.conf: |
|
||||
# Only plain usernames reach a generated file; anything else leaves the
|
||||
# field empty and the device asks for it.
|
||||
map $http_x_authentik_username $wifi_user {
|
||||
"~^[A-Za-z0-9._@+-]{1,64}$" $http_x_authentik_username;
|
||||
default "";
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
listen [::]:8080;
|
||||
server_tokens off;
|
||||
root /usr/share/nginx/html;
|
||||
add_header Cache-Control "no-store" always;
|
||||
|
||||
location = / {
|
||||
try_files /index.html =404;
|
||||
}
|
||||
|
||||
location = /whoami {
|
||||
default_type text/plain;
|
||||
return 200 $wifi_user;
|
||||
}
|
||||
|
||||
# Apple: com.apple.wifi.managed, TTLS (21) with inner PAP. Fixed
|
||||
# PayloadIdentifier/UUIDs so a reinstall replaces the old profile.
|
||||
location = /renner-unifi.mobileconfig {
|
||||
default_type application/x-apple-aspen-config;
|
||||
add_header Cache-Control "no-store" always;
|
||||
add_header Content-Disposition 'attachment; filename="renner-unifi.mobileconfig"' always;
|
||||
return 200 '<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PayloadContent</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.wifi.managed</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>uno.renner.wifi.renner-unifi.network</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>824959bf-c55d-4ed2-b569-c33f3759c2b6</string>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>Renner UniFi</string>
|
||||
<key>PayloadOrganization</key>
|
||||
<string>Renner</string>
|
||||
<key>SSID_STR</key>
|
||||
<string>Renner UniFi</string>
|
||||
<key>HIDDEN_NETWORK</key>
|
||||
<false/>
|
||||
<key>AutoJoin</key>
|
||||
<true/>
|
||||
<key>EncryptionType</key>
|
||||
<string>Any</string>
|
||||
<key>EAPClientConfiguration</key>
|
||||
<dict>
|
||||
<key>UserName</key>
|
||||
<string>$wifi_user</string>
|
||||
<key>OuterIdentity</key>
|
||||
<string>anonymous</string>
|
||||
<key>AcceptEAPTypes</key>
|
||||
<array>
|
||||
<integer>21</integer>
|
||||
</array>
|
||||
<key>TTLSInnerAuthentication</key>
|
||||
<string>PAP</string>
|
||||
<key>TLSTrustedServerNames</key>
|
||||
<array>
|
||||
<string>radius.renner.uno</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>PayloadType</key>
|
||||
<string>Configuration</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>uno.renner.wifi.renner-unifi</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>7175a2a3-ddd3-42ea-b4ad-b7e5501892f3</string>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>WLAN Renner UniFi</string>
|
||||
<key>PayloadOrganization</key>
|
||||
<string>Renner</string>
|
||||
</dict>
|
||||
</plist>
|
||||
';
|
||||
}
|
||||
|
||||
location = /renner-unifi-wlan.cmd {
|
||||
default_type application/octet-stream;
|
||||
add_header Cache-Control "no-store" always;
|
||||
add_header Content-Disposition 'attachment; filename="renner-unifi-wlan.cmd"' always;
|
||||
try_files /renner-unifi-wlan.cmd =404;
|
||||
}
|
||||
|
||||
location = /healthz {
|
||||
access_log off;
|
||||
default_type text/plain;
|
||||
return 200 "ok\n";
|
||||
}
|
||||
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
|
||||
# Windows: extracts the WLAN profile (everything after line 8 of itself)
|
||||
# into %TEMP% and imports it for the current user with netsh. EAP-TTLS with
|
||||
# PAPAuthentication, outer identity anonymous, server name
|
||||
# radius.renner.uno, trust anchor ISRG Root X1 (SHA-1 thumbprint; the
|
||||
# Let's Encrypt chain of radius-tls ends there). Windows asks for username
|
||||
# and password on connect. CRLF line endings: cmd.exe needs them.
|
||||
renner-unifi-wlan.cmd: "@echo off\r\nrem WLAN Renner UniFi einrichten (802.1X, Anmeldung mit dem authentik-Konto)\r\nset \"WLANXML=%TEMP%\\renner-unifi-wlan.xml\"\r\nmore +8 \"%~f0\" > \"%WLANXML%\"\r\nnetsh wlan add profile filename=\"%WLANXML%\" user=current\r\ndel \"%WLANXML%\"\r\nnetsh wlan connect name=\"Renner UniFi\"\r\npause & exit /b\r\n<?xml version=\"1.0\"?><WLANProfile xmlns=\"http://www.microsoft.com/networking/WLAN/profile/v1\"><name>Renner UniFi</name><SSIDConfig><SSID><hex>52656E6E657220556E694669</hex><name>Renner UniFi</name></SSID><nonBroadcast>false</nonBroadcast></SSIDConfig><connectionType>ESS</connectionType><connectionMode>auto</connectionMode><MSM><security><authEncryption><authentication>WPA2</authentication><encryption>AES</encryption><useOneX>true</useOneX></authEncryption><OneX xmlns=\"http://www.microsoft.com/networking/OneX/v1\"><cacheUserData>true</cacheUserData><authMode>user</authMode><EAPConfig><EapHostConfig xmlns=\"http://www.microsoft.com/provisioning/EapHostConfig\"><EapMethod><Type xmlns=\"http://www.microsoft.com/provisioning/EapCommon\">21</Type><VendorId xmlns=\"http://www.microsoft.com/provisioning/EapCommon\">0</VendorId><VendorType xmlns=\"http://www.microsoft.com/provisioning/EapCommon\">0</VendorType><AuthorId xmlns=\"http://www.microsoft.com/provisioning/EapCommon\">311</AuthorId></EapMethod><Config xmlns=\"http://www.microsoft.com/provisioning/EapHostConfig\"><EapTtls xmlns=\"http://www.microsoft.com/provisioning/EapTtlsConnectionPropertiesV1\"><ServerValidation><ServerNames>radius.renner.uno</ServerNames><TrustedRootCAHash>ca bd 2a 79 a1 07 6a 31 f2 1d 25 36 35 cb 03 9d 43 29 a5 e8</TrustedRootCAHash><DisablePrompt>false</DisablePrompt></ServerValidation><Phase2Authentication><PAPAuthentication/></Phase2Authentication><Phase1Identity><IdentityPrivacy>true</IdentityPrivacy><AnonymousIdentity>anonymous</AnonymousIdentity></Phase1Identity></EapTtls></Config></EapHostConfig></EAPConfig></OneX></security></MSM></WLANProfile>\r\n"
|
||||
|
||||
index.html: |
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>WLAN einrichten</title>
|
||||
<style>
|
||||
:root { --bg: #ffffff; --fg: #1d1d1f; --muted: #5f6368; --card: #f4f5f7; --accent: #0b63ce; --code: #e9ebef; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root { --bg: #121314; --fg: #e8eaed; --muted: #9aa0a6; --card: #1e1f22; --accent: #7cacf8; --code: #2a2b2f; }
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--bg); color: var(--fg); font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
|
||||
main { max-width: 42rem; margin: 0 auto; padding: 1.5rem 1rem 3rem; }
|
||||
h1 { font-size: 1.6rem; margin: 0 0 .25rem; }
|
||||
h2 { font-size: 1.15rem; margin: 0 0 .5rem; }
|
||||
p { margin: .5rem 0; }
|
||||
.muted { color: var(--muted); }
|
||||
section { background: var(--card); border-radius: 12px; padding: 1rem 1.1rem; margin: 1rem 0; }
|
||||
section.current { outline: 2px solid var(--accent); }
|
||||
a.button, button { display: inline-block; background: var(--accent); color: #fff; border: 0; border-radius: 8px; padding: .6rem 1rem; font: inherit; text-decoration: none; cursor: pointer; }
|
||||
@media (prefers-color-scheme: dark) { a.button, button { color: #121314; } }
|
||||
ol { padding-left: 1.3rem; margin: .5rem 0; }
|
||||
pre { background: var(--code); border-radius: 8px; padding: .75rem; overflow-x: auto; white-space: pre-wrap; word-break: break-all; font-size: .85rem; margin: .5rem 0; }
|
||||
details > summary { cursor: pointer; color: var(--accent); margin: 1.5rem 0 .5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>WLAN einrichten</h1>
|
||||
<p>WLAN <strong>Renner UniFi</strong>. Anmeldung mit deinem Konto <strong id="user">…</strong> und deinem authentik-Passwort.</p>
|
||||
|
||||
<div id="detected"></div>
|
||||
|
||||
<details id="others">
|
||||
<summary>Andere Geräte</summary>
|
||||
<div id="rest"></div>
|
||||
</details>
|
||||
|
||||
<template id="t-apple">
|
||||
<section data-os="apple">
|
||||
<h2>iPhone, iPad, Mac</h2>
|
||||
<p><a class="button" href="/renner-unifi.mobileconfig">Profil laden</a></p>
|
||||
<ol>
|
||||
<li>iPhone/iPad: Einstellungen → „Profil geladen“ → Installieren. Mac: Systemeinstellungen → Allgemein → Geräteverwaltung → Profil doppelklicken.</li>
|
||||
<li>Beim ersten Verbinden mit „Renner UniFi“ dein Passwort eingeben.</li>
|
||||
</ol>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template id="t-windows">
|
||||
<section data-os="windows">
|
||||
<h2>Windows</h2>
|
||||
<p><a class="button" href="/renner-unifi-wlan.cmd">Einrichtungsdatei laden</a></p>
|
||||
<ol>
|
||||
<li>Die geladene Datei doppelklicken. Fragt Windows nach („Der Computer wurde durch Windows geschützt“): „Weitere Informationen“ → „Trotzdem ausführen“.</li>
|
||||
<li>Beim Verbinden Benutzername <strong class="u">…</strong> und dein Passwort eingeben.</li>
|
||||
</ol>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template id="t-linux">
|
||||
<section data-os="linux">
|
||||
<h2>Linux (NetworkManager)</h2>
|
||||
<p>Im Terminal ausführen, danach fragt NetworkManager nach dem Passwort:</p>
|
||||
<pre id="nmcli"></pre>
|
||||
<p><button type="button" id="copy-nmcli">Befehl kopieren</button></p>
|
||||
</section>
|
||||
</template>
|
||||
</main>
|
||||
<script>
|
||||
(function () {
|
||||
var ua = navigator.userAgent;
|
||||
var os = "other";
|
||||
if (/Android/i.test(ua)) os = "android";
|
||||
else if (/iPhone|iPad|iPod|Macintosh/i.test(ua)) os = "apple";
|
||||
else if (/Windows/i.test(ua)) os = "windows";
|
||||
else if (/Linux|X11/i.test(ua)) os = "linux";
|
||||
|
||||
var detected = document.getElementById("detected");
|
||||
var rest = document.getElementById("rest");
|
||||
["apple", "windows", "linux"].forEach(function (name) {
|
||||
var node = document.getElementById("t-" + name).content.cloneNode(true);
|
||||
if (name === os) { node.querySelector("section").classList.add("current"); detected.appendChild(node); }
|
||||
else rest.appendChild(node);
|
||||
});
|
||||
if (os !== "apple" && os !== "windows" && os !== "linux") document.getElementById("others").open = true;
|
||||
|
||||
function quote(s) { return "'" + s.replace(/'/g, "'\\''") + "'"; }
|
||||
function render(user) {
|
||||
var shown = user || "(dein Benutzername)";
|
||||
document.getElementById("user").textContent = shown;
|
||||
document.querySelectorAll(".u").forEach(function (el) { el.textContent = shown; });
|
||||
document.getElementById("nmcli").textContent =
|
||||
"nmcli connection add type wifi con-name 'Renner UniFi' ssid 'Renner UniFi' " +
|
||||
"wifi-sec.key-mgmt wpa-eap 802-1x.eap ttls 802-1x.phase2-auth pap " +
|
||||
"802-1x.anonymous-identity anonymous 802-1x.identity " + quote(user || "BENUTZERNAME") + " " +
|
||||
"802-1x.domain-suffix-match radius.renner.uno 802-1x.system-ca-certs yes " +
|
||||
"802-1x.password-flags 2 && nmcli --ask connection up 'Renner UniFi'";
|
||||
}
|
||||
render("");
|
||||
fetch("/whoami", { credentials: "same-origin" })
|
||||
.then(function (r) { return r.ok ? r.text() : ""; })
|
||||
.then(function (t) { render(t.trim()); })
|
||||
.catch(function () {});
|
||||
|
||||
document.getElementById("copy-nmcli").addEventListener("click", function () {
|
||||
var text = document.getElementById("nmcli").textContent;
|
||||
var btn = this;
|
||||
navigator.clipboard.writeText(text).then(function () {
|
||||
btn.textContent = "Kopiert";
|
||||
setTimeout(function () { btn.textContent = "Befehl kopieren"; }, 2000);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,133 @@
|
||||
# wlan.renner.uno: setup page for the 802.1X WLAN "Renner UniFi" (Apple
|
||||
# profile, Windows import file, Linux nmcli command; config and files in
|
||||
# wlan-setup-config.yaml). Public, so a device can reach it before it is in the
|
||||
# WLAN (mobile data, Renner Gast). Any Authentik user passes the gate (no
|
||||
# group): the page holds no secrets, only the caller's own username.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: wlan-setup
|
||||
namespace: authentik
|
||||
labels:
|
||||
app.kubernetes.io/name: wlan-setup
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: wlan-setup
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: wlan-setup
|
||||
annotations:
|
||||
configmap.reloader.stakater.com/reload: wlan-setup
|
||||
spec:
|
||||
automountServiceAccountToken: false
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 101
|
||||
runAsGroup: 101
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginxinc/nginx-unprivileged:1.31-alpine@sha256:b5047b9b76845eac124358373c6852377c52e4b05834a47647e955dd91cb8f1e
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
resources:
|
||||
requests:
|
||||
cpu: 5m
|
||||
memory: 16Mi
|
||||
limits:
|
||||
memory: 64Mi
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/nginx/conf.d
|
||||
readOnly: true
|
||||
- name: html
|
||||
mountPath: /usr/share/nginx/html
|
||||
readOnly: true
|
||||
# pid file and temp paths of the unprivileged image.
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: wlan-setup
|
||||
items:
|
||||
- key: default.conf
|
||||
path: default.conf
|
||||
- name: html
|
||||
configMap:
|
||||
name: wlan-setup
|
||||
items:
|
||||
- key: index.html
|
||||
path: index.html
|
||||
- key: renner-unifi-wlan.cmd
|
||||
path: renner-unifi-wlan.cmd
|
||||
- name: tmp
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 16Mi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: wlan-setup
|
||||
namespace: authentik
|
||||
labels:
|
||||
app.kubernetes.io/name: wlan-setup
|
||||
spec:
|
||||
ipFamilyPolicy: PreferDualStack
|
||||
selector:
|
||||
app.kubernetes.io/name: wlan-setup
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: wlan-setup
|
||||
namespace: authentik
|
||||
labels:
|
||||
# Public: Cloudflare CNAME → NetBird proxy → Traefik.
|
||||
dns.renner.uno/public: "true"
|
||||
netbird.renner.uno/public: "true"
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.middlewares: authentik-gate@kubernetescrd
|
||||
cert-manager.io/cluster-issuer: letsencrypt-cluster
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts:
|
||||
- wlan.renner.uno
|
||||
secretName: wlan-setup-tls
|
||||
rules:
|
||||
- host: wlan.renner.uno
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: wlan-setup
|
||||
port:
|
||||
number: 80
|
||||
@@ -64,7 +64,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: shelly-manager
|
||||
image: gitea.renner.uno/konairius/shelly-manager:2026-11-02-1412 # {"$imagepolicy": "flux-system:shelly-manager"}
|
||||
image: gitea.renner.uno/konairius/shelly-manager:2026-11-05-0732 # {"$imagepolicy": "flux-system:shelly-manager"}
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
|
||||
@@ -128,7 +128,7 @@ spec:
|
||||
memory: 32Mi
|
||||
containers:
|
||||
- name: home-assistant
|
||||
image: ghcr.io/home-assistant/home-assistant:2026.10.4 # {"$imagepolicy": "flux-system:home-assistant"}
|
||||
image: ghcr.io/home-assistant/home-assistant:2026.11.0 # {"$imagepolicy": "flux-system:home-assistant"}
|
||||
env:
|
||||
- name: TZ
|
||||
value: Europe/Berlin
|
||||
|
||||
@@ -32,7 +32,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: immich-machine-learning
|
||||
image: ghcr.io/immich-app/immich-machine-learning:v3.3.5 # {"$imagepolicy": "flux-system:immich-machine-learning"}
|
||||
image: ghcr.io/immich-app/immich-machine-learning:v3.3.6 # {"$imagepolicy": "flux-system:immich-machine-learning"}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 3003
|
||||
|
||||
@@ -87,7 +87,7 @@ spec:
|
||||
mountPath: /ca
|
||||
containers:
|
||||
- name: immich-server
|
||||
image: ghcr.io/immich-app/immich-server:v3.3.5 # {"$imagepolicy": "flux-system:immich-server"}
|
||||
image: ghcr.io/immich-app/immich-server:v3.3.6 # {"$imagepolicy": "flux-system:immich-server"}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 3001
|
||||
|
||||
@@ -134,7 +134,7 @@ spec:
|
||||
# is a device-bricking class of risk. Bumps are a reviewed, manual
|
||||
# act — see kubernetes/infrastructure/image-automation/README.md.
|
||||
- name: terminus
|
||||
image: ghcr.io/usetrmnl/terminus:0.79.0 # {"$imagepolicy": "flux-system:terminus"}
|
||||
image: ghcr.io/usetrmnl/terminus:0.80.0 # {"$imagepolicy": "flux-system:terminus"}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 2345
|
||||
@@ -197,7 +197,7 @@ spec:
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
- name: sidekiq
|
||||
image: ghcr.io/usetrmnl/terminus:0.79.0 # {"$imagepolicy": "flux-system:terminus"}
|
||||
image: ghcr.io/usetrmnl/terminus:0.80.0 # {"$imagepolicy": "flux-system:terminus"}
|
||||
command: ["bundle", "exec", "sidekiq", "-r", "./config/sidekiq.rb"]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
|
||||
+30
-1
@@ -7,7 +7,8 @@
|
||||
"flux",
|
||||
"kubernetes",
|
||||
"woodpecker",
|
||||
"dockerfile"
|
||||
"dockerfile",
|
||||
"custom.regex"
|
||||
],
|
||||
"ignorePaths": [
|
||||
"kubernetes/clusters/homelab/flux-system/**",
|
||||
@@ -23,6 +24,20 @@
|
||||
"/^kubernetes/.+\\.ya?ml$/"
|
||||
]
|
||||
},
|
||||
"customManagers": [
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "Container images of the Ansible roles: `edge_<name>_image: <image>:<tag>@<digest>` in ansible/roles/*/defaults/main.yml. No other manager reads these files.",
|
||||
"managerFilePatterns": [
|
||||
"/^ansible/roles/[^/]+/defaults/main\\.ya?ml$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"(?:^|\\n)edge_[a-z0-9_]+_image:[ \\t]*[\"']?(?<depName>[^\\s:@\"']+):(?<currentValue>[^\\s@\"']+)(?:@(?<currentDigest>sha256:[a-f0-9]{64}))?"
|
||||
],
|
||||
"datasourceTemplate": "docker",
|
||||
"versioningTemplate": "docker"
|
||||
}
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Flux image automation (kubernetes/infrastructure/image-automation) already bumps these images. Renovate must not open a second, competing update. Keep this list equal to the ImageRepository images there.",
|
||||
@@ -134,6 +149,20 @@
|
||||
"groupName": "CI non-major",
|
||||
"groupSlug": "ci-minor-patch"
|
||||
},
|
||||
{
|
||||
"description": "Edge-host images (ansible/): no pipeline deploys them, the play runs from a workstation after the merge. One PR of its own, so the merge and the play run go together.",
|
||||
"matchFileNames": [
|
||||
"ansible/**"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"patch",
|
||||
"minor",
|
||||
"digest",
|
||||
"pin"
|
||||
],
|
||||
"groupName": "edge hosts (ansible)",
|
||||
"groupSlug": "edge-hosts-ansible"
|
||||
},
|
||||
{
|
||||
"description": "Stateful apps and the CNI: one PR each, never in a group (REN-315). A git revert does not undo a DB migration or a CNI change, so each needs its own review and, where it can disrupt the cluster, a window.",
|
||||
"matchUpdateTypes": [
|
||||
|
||||
Reference in New Issue
Block a user