Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c068f735d | ||
|
|
ec91122621 | ||
|
|
fc720823db | ||
|
|
8d47e085cb | ||
|
|
54ae8ce463 | ||
|
|
cd2e7864a4 | ||
|
|
fec9f6e0e5 | ||
|
|
55fce76a2d | ||
|
|
5fcd6dc5c4 | ||
|
|
3a35e8481a | ||
|
|
a5be653044 | ||
|
|
1d5a262a84 | ||
|
|
ad6fc66875 | ||
|
|
861e9a3193 |
@@ -29,7 +29,7 @@ Build and boot the dedicated installer image — see
|
||||
`images/edge-host-iso/README.md`. Short version:
|
||||
|
||||
1. `sudo pacman -S archiso && sudo images/edge-host-iso/build.sh`
|
||||
2. Write ISO to a USB stick, boot the target box from it, run `edge-install`
|
||||
2. Write ISO to a USB stick, boot the target box from it, run `edge-install <hostname>`
|
||||
(partitions the internal disk — NVMe if present, else the eMMC; the
|
||||
installer autodetects — installs the ansible-ready base system with SSH
|
||||
keys, DHCP, docker — asks for confirmation before wiping).
|
||||
|
||||
@@ -42,10 +42,11 @@ for the CA key; rebuild the ISO after a CA rotation.
|
||||
2. Boot the target from it (UEFI, Secure Boot off; one-time F12/setup
|
||||
visit if the box doesn't already boot USB first)
|
||||
3. Either on the console (auto-login as root) or headless —
|
||||
`ssh root@<dhcp-ip>` with an owner certificate — run `edge-install`. It auto-detects the internal disk
|
||||
(NVMe, else eMMC — covers the Wyse 3040 — else first non-USB disk),
|
||||
shows the target and requires typing the hostname before wiping
|
||||
anything. Override with `edge-install /dev/sdY`.
|
||||
`ssh root@<dhcp-ip>` with an owner certificate — run
|
||||
`edge-install <hostname>` (e.g. `edge-install edge-2`). It auto-detects
|
||||
the internal disk (NVMe, else eMMC — covers the Wyse 3040 — else first
|
||||
non-USB disk), shows the target and requires typing the hostname before
|
||||
wiping anything. Override with `edge-install <hostname> /dev/sdY`.
|
||||
4. Reboot. Confirm the box is reachable by name — `ssh root@<host>` — before
|
||||
going further; it is identified by name everywhere and its address is
|
||||
never recorded. Then add it to `[edge_host]` in
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Installer for edge-1 (bare-metal edge host, replaces the k8s-edge VM).
|
||||
# Installer for the bare-metal edge hosts (edge-1, edge-2, ...).
|
||||
# Runs from the edge-host ISO live environment. WIPES the target disk.
|
||||
# Usage: edge-install [disk]
|
||||
# Default: the internal disk — first NVMe, else eMMC (Wyse 3040), else
|
||||
# Usage: edge-install <hostname> [disk]
|
||||
# Default disk: the internal disk — first NVMe, else eMMC (Wyse 3040), else
|
||||
# the first non-USB, non-optical disk. The live USB stick itself is
|
||||
# excluded via the ISO9660 label mount.
|
||||
set -euo pipefail
|
||||
|
||||
HOST=edge-1
|
||||
usage() { echo "usage: edge-install <hostname> [disk]"; exit 1; }
|
||||
[ $# -ge 1 ] && [ $# -le 2 ] || usage
|
||||
HOST=$1
|
||||
# RFC 1123 label: what systemd-networkd sends as the DHCP hostname.
|
||||
[[ "$HOST" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]] \
|
||||
|| { echo "invalid hostname: $HOST"; exit 1; }
|
||||
shift
|
||||
|
||||
detect_disk() {
|
||||
# eMMC needs mmc_block before any /dev/mmcblk* exists (see
|
||||
@@ -51,7 +57,7 @@ mount --mkdir "$(part 1)" /mnt/boot
|
||||
|
||||
# Everything the edge-host ansible role expects to find; the role itself
|
||||
# is applied over SSH after first boot (see ansible/docs/edge-host.md).
|
||||
pacstrap -K /mnt base linux linux-firmware intel-ucode openssh python git \
|
||||
pacstrap -K /mnt base linux linux-firmware intel-ucode openssh sudo python git \
|
||||
docker docker-compose
|
||||
|
||||
genfstab -U /mnt >> /mnt/etc/fstab
|
||||
|
||||
@@ -34,7 +34,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: wlan-setup
|
||||
image: gitea.renner.uno/gitea_admin/wlan-setup:2026-11-05-1352 # {"$imagepolicy": "flux-system:wlan-setup"}
|
||||
image: gitea.renner.uno/gitea_admin/wlan-setup:2026-11-05-1517 # {"$imagepolicy": "flux-system:wlan-setup"}
|
||||
env:
|
||||
# Client secret of the "WLAN setup" OAuth2 provider.
|
||||
- name: WLAN_SETUP_CLIENT_SECRET
|
||||
|
||||
@@ -23,7 +23,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: gitea-mcp
|
||||
image: docker.gitea.com/gitea-mcp-server:1.12.0 # {"$imagepolicy": "flux-system:gitea-mcp"}
|
||||
image: docker.gitea.com/gitea-mcp-server:1.13.0 # {"$imagepolicy": "flux-system:gitea-mcp"}
|
||||
# Image has no ENTRYPOINT (binary is in CMD), so args alone would replace it.
|
||||
command:
|
||||
- /app/gitea-mcp
|
||||
|
||||
@@ -30,7 +30,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: controller
|
||||
image: gitea.renner.uno/gitea_admin/kuma-sync:2026-10-30-1314 # {"$imagepolicy": "flux-system:kuma-sync"}
|
||||
image: gitea.renner.uno/gitea_admin/kuma-sync:2026-11-05-1355 # {"$imagepolicy": "flux-system:kuma-sync"}
|
||||
env:
|
||||
- name: KUMA_URL
|
||||
value: https://kuma.renner.uno
|
||||
|
||||
@@ -22,7 +22,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: gotenberg
|
||||
image: gotenberg/gotenberg:8.39.0 # {"$imagepolicy": "flux-system:gotenberg"}
|
||||
image: gotenberg/gotenberg:8.40.0 # {"$imagepolicy": "flux-system:gotenberg"}
|
||||
args:
|
||||
- "gotenberg"
|
||||
- "--chromium-disable-javascript=true"
|
||||
|
||||
@@ -155,7 +155,7 @@ spec:
|
||||
# exist on both registries — hence konairius/, which matches the ghcr
|
||||
# namespace. Steady state still pulls from Gitea and never leaves the
|
||||
# network.
|
||||
image: gitea.renner.uno/konairius/alpine-openssl:2026-10-29-1348 # {"$imagepolicy": "flux-system:alpine-openssl"}
|
||||
image: gitea.renner.uno/konairius/alpine-openssl:2026-11-05-1353 # {"$imagepolicy": "flux-system:alpine-openssl"}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
|
||||
@@ -76,6 +76,10 @@ spec:
|
||||
value: "netbird-ingress-$(NODE_NAME)"
|
||||
- name: NB_LOG_LEVEL
|
||||
value: "info"
|
||||
# stdout only; the image default also writes client.log into the
|
||||
# 16Mi state tmpfs.
|
||||
- name: NB_LOG_FILE
|
||||
value: "console"
|
||||
# rootless image needs a writable HOME for its state.
|
||||
- name: USER
|
||||
value: "netbird"
|
||||
|
||||
@@ -32,7 +32,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: backup-verify
|
||||
image: gitea.renner.uno/gitea_admin/ci-backup-verify:2026-10-29 # {"$imagepolicy": "flux-system:backup-verify"}
|
||||
image: gitea.renner.uno/gitea_admin/ci-backup-verify:2026-11-05 # {"$imagepolicy": "flux-system:backup-verify"}
|
||||
command: ["bash", "/script/backup-verify.sh"]
|
||||
env:
|
||||
- name: HOME
|
||||
|
||||
@@ -200,10 +200,10 @@ func addText(parent *etree.Element, tag, ns, text string) *etree.Element {
|
||||
}
|
||||
|
||||
// windowsProfile is the unsigned CarrierProvisioning document: one Hotspot 2.0 WLAN profile
|
||||
// matched by domain name, EAP-TTLS with inner PAP, and the user's credentials. The
|
||||
// CarrierControl WLAN schema allows authentication up to WPA2 (Enterprise); Windows adds the
|
||||
// PMF that the WPA3-Enterprise network requires. Elements follow Microsoft's signed Passpoint
|
||||
// provisioning samples.
|
||||
// (domain and NAI realm renner.uno), WPA3-Enterprise, EAP-TTLS with inner PAP, and the user's
|
||||
// credentials. Valid against the schemas Windows ships in %SYSTEMROOT%\schemas\provisioning
|
||||
// (TestWindowsProfileSchema); SubscriberId is base:SubscriberType, i.e. \w+ and at most 20
|
||||
// characters.
|
||||
func windowsProfile(user, password string) *etree.Document {
|
||||
doc := etree.NewDocument()
|
||||
doc.WriteSettings = etree.WriteSettings{CanonicalEndTags: true, CanonicalText: true, CanonicalAttrVal: true}
|
||||
@@ -213,16 +213,18 @@ func windowsProfile(user, password string) *etree.Document {
|
||||
|
||||
global := add(root, "Global", "")
|
||||
addText(global, "CarrierId", "", carrierID)
|
||||
addText(global, "SubscriberId", "", "renner-unifi")
|
||||
addText(global, "SubscriberId", "", "rennerunifi")
|
||||
|
||||
wlan := add(add(root, "WLANProfiles", ""), "WLANProfile", nsWLAN)
|
||||
addText(wlan, "name", "", "Renner UniFi")
|
||||
addText(add(add(wlan, "SSIDConfig", ""), "SSID", ""), "name", "", "Renner UniFi")
|
||||
addText(add(wlan, "Hotspot2", ""), "DomainName", "", "renner.uno")
|
||||
hotspot2 := add(wlan, "Hotspot2", "")
|
||||
addText(hotspot2, "DomainName", "", "renner.uno")
|
||||
addText(add(hotspot2, "NAIRealm", ""), "name", "", "renner.uno")
|
||||
|
||||
security := add(add(wlan, "MSM", ""), "security", "")
|
||||
auth := add(security, "authEncryption", "")
|
||||
addText(auth, "authentication", "", "WPA2")
|
||||
addText(auth, "authentication", "", "WPA3ENT")
|
||||
addText(auth, "encryption", "", "AES")
|
||||
addText(auth, "useOneX", "", "true")
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
@@ -644,6 +645,61 @@ func TestCallbackWindows(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestWindowsProfileSchema validates the signed document with xmllint against the schemas
|
||||
// Windows ships in %SYSTEMROOT%\schemas\provisioning. They are Microsoft's files and not in the
|
||||
// repo: point WLAN_SETUP_PROVISIONING_XSD at a copy of that folder to run it.
|
||||
func TestWindowsProfileSchema(t *testing.T) {
|
||||
dir := os.Getenv("WLAN_SETUP_PROVISIONING_XSD")
|
||||
if dir == "" {
|
||||
t.Skip("WLAN_SETUP_PROVISIONING_XSD not set")
|
||||
}
|
||||
xmllint, err := exec.LookPath("xmllint")
|
||||
if err != nil {
|
||||
t.Skip("xmllint not installed")
|
||||
}
|
||||
tc := writeSigningFiles(t)
|
||||
signer, err := loadSigner(tc.keyFile, tc.certFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body, err := signer.Sign(windowsProfile("Alice.Smith", testKey))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// The schemas import each other by namespace only; a wrapper schema gives libxml2 the
|
||||
// locations. Dependencies first, so the later imports find their namespaces loaded.
|
||||
wrapper := etree.NewDocument()
|
||||
schema := wrapper.CreateElement("xs:schema")
|
||||
schema.CreateAttr("xmlns:xs", "http://www.w3.org/2001/XMLSchema")
|
||||
schema.CreateAttr("targetNamespace", "urn:wlan-setup:schema-test")
|
||||
for _, s := range []struct{ ns, file string }{
|
||||
{"http://www.microsoft.com/networking/CarrierControl/Base/v1", "provisioning_base_v1.xsd"},
|
||||
{"http://www.microsoft.com/networking/CarrierControl/Plans/v1", "provisioning_plans_v1.xsd"},
|
||||
{"http://www.microsoft.com/networking/CarrierControl/WWAN/v1", "provisioning_wwan_v1.xsd"},
|
||||
{"http://www.microsoft.com/networking/CarrierControl/WLAN/v1", "provisioning_wlan_v1.xsd"},
|
||||
{dsig.Namespace, "provisioning_sig_v1.xsd"},
|
||||
{nsCarrierControl, "provisioning_v1.xsd"},
|
||||
} {
|
||||
imp := schema.CreateElement("xs:import")
|
||||
imp.CreateAttr("namespace", s.ns)
|
||||
imp.CreateAttr("schemaLocation", "file://"+filepath.Join(dir, s.file))
|
||||
}
|
||||
tmp := t.TempDir()
|
||||
wrapperFile := filepath.Join(tmp, "wrapper.xsd")
|
||||
if err := wrapper.WriteToFile(wrapperFile); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
docFile := filepath.Join(tmp, "profile.xml")
|
||||
if err := os.WriteFile(docFile, body, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := exec.Command(xmllint, "--noout", "--schema", wrapperFile, docFile).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("xmllint: %v\n%s", err, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWindowsProfileDocument(t *testing.T) {
|
||||
tc := writeSigningFiles(t)
|
||||
signer, err := loadSigner(tc.keyFile, tc.certFile)
|
||||
@@ -676,11 +732,12 @@ func TestWindowsProfileDocument(t *testing.T) {
|
||||
)
|
||||
for path, want := range map[string]string{
|
||||
"Global/CarrierId": carrierID,
|
||||
"Global/SubscriberId": "renner-unifi",
|
||||
"Global/SubscriberId": "rennerunifi",
|
||||
wlan + "name": "Renner UniFi",
|
||||
wlan + "SSIDConfig/SSID/name": "Renner UniFi",
|
||||
wlan + "Hotspot2/DomainName": "renner.uno",
|
||||
sec + "authEncryption/authentication": "WPA2",
|
||||
wlan + "Hotspot2/NAIRealm/name": "renner.uno",
|
||||
sec + "authEncryption/authentication": "WPA3ENT",
|
||||
sec + "authEncryption/encryption": "AES",
|
||||
sec + "authEncryption/useOneX": "true",
|
||||
sec + "OneX/authMode": "user",
|
||||
|
||||
Reference in New Issue
Block a user