diff --git a/.gitignore b/.gitignore index 600b988..55734e1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ kubeconfig # Retired dev edge credentials (cirrus_dev), kept locally only old/ + +# Ignition output embeds the secrets above +*.ign diff --git a/coreos/cirrus.yaml b/coreos/cirrus.yaml index 842cf14..110cd57 100755 --- a/coreos/cirrus.yaml +++ b/coreos/cirrus.yaml @@ -1,29 +1,37 @@ variant: fcos version: 1.6.0 -# cirrus: 5001 admin sshd | 5002 sish SSH | 22,80,443 raw TCP -> k8s gateway +# cirrus: 5001 admin sshd | 5002 sish SSH | 80 HTTP by Host | 443 TLS by SNI (passthrough) +# 22, 20000-20099 raw TCP forwards passwd: users: - name: core ssh_authorized_keys: - - ssh-ed25519 AAAA_REPLACE_ADMIN_KEY admin + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILL4RXmGVhbcCdh3a6TdgR+7EER250UUDk0VtrwUvb9E philipp@philipp-laptop storage: directories: - - path: /var/lib/sish/keys # sish host key, generated on first start + - path: /var/lib/sish/keys mode: 0700 user: { id: 65532 } group: { id: 65532 } files: + # Edge SSH host key (connectors pin its public half). Kept only locally in coreos/.secrets/ + # (gitignored), so back it up outside this folder. Build: butane --files-dir coreos ... + - path: /var/lib/sish/keys/ssh_host_ed25519_key + mode: 0400 + user: { id: 65532 } + group: { id: 65532 } + contents: + local: .secrets/ssh_host_ed25519_key - path: /var/lib/sish/pubkeys/clients # k8s tunnel client keys (authorized_keys format) mode: 0644 contents: inline: | - ssh-ed25519 AAAA_REPLACE_CLIENT_KEY k8s-tunnel + ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEsHP4H4HLHCEhycaAV+YZZV/HOr7HSiDkgpMA+WLO56 connector-cumulus - path: /etc/ssh/sshd_config.d/10-cirrus.conf mode: 0644 contents: - inline: | Port 5001 AuthenticationMethods publickey @@ -34,6 +42,65 @@ storage: contents: inline: | (allow sshd_t commplex_link_port_t (tcp_socket (name_bind))) + # Same sish config as kubernetes/base/sish/config.yml (+ the cirrus overlay values), + # only the SSH port differs (5002 instead of 2222). + - path: /etc/sish/config.yml + mode: 0644 + contents: + inline: | + # Listeners + ssh-address: ":5002" + http-address: ":80" + https: false # sish never terminates TLS; :443 is an SNI passthrough listener + + # Single tenant: connectors may claim any hostname containing a dot, wildcards included + # (*.example.com). Only a name without a dot falls back to .. + bind-any-host: true + verify-dns: false # _sish TXT ownership checks, pointless with bind-any-host + domain: tunnel.traberph.de # the edge's own name (A/AAAA -> VPS) + + # SNI passthrough + multiple connectors per hostname + sni-proxy: true + sni-load-balancer: true + tcp-load-balancer: true + http-load-balancer: true + + # Connectors get exactly what they ask for, or the bind fails + bind-random-ports: false + bind-random-subdomains: false + bind-random-aliases: false + force-requested-subdomains: true + force-requested-ports: true + bind-wildcards: true + # Ports connectors may claim. Must match the nftables rule below, otherwise a claimed + # port is silently unreachable. + # 22 gitea ssh, 443 SNI, 20000-20099 reserved for raw tcp forwards. + port-bind-range: "22,443,20000-20099" + + # PROXY header version for connectors that request it (sish-client default: v2) + proxy-protocol: true + proxy-protocol-version: "2" + + # Default is 5s, which kills idle websockets/SSE/slow uploads + idle-connection-timeout: 1h + + # Auth: public keys only + authentication: true + authentication-keys-directory: /pubkeys + private-keys-directory: /keys + + # No web UI / consoles + redirect-root: false + admin-console: false + service-console: false + load-templates: false + # Default -1 makes the HTTP muxer io.ReadAll() every request/response body into memory + # (for the console), even with consoles disabled: large uploads OOM-kill sish. + # 0 = never buffer, stream bodies through. + service-console-max-content-length: 0 + + log-to-stdout: true + log-to-file: false - path: /etc/containers/systemd/sish.container mode: 0644 contents: @@ -51,30 +118,65 @@ storage: AddCapability=CAP_NET_BIND_SERVICE NoNewPrivileges=true ReadOnly=true - Volume=/var/lib/sish/keys:/keys:Z + Volume=/etc/sish:/config:ro,Z + Volume=/var/lib/sish/keys:/keys:ro,Z Volume=/var/lib/sish/pubkeys:/pubkeys:ro,Z - Exec=--ssh-address=:5002 \ - --domain=cirrus.example.com \ - --private-keys-directory=/keys \ - --authentication-keys-directory=/pubkeys \ - --http-address=127.0.0.1:5080 \ - --http-request-port-override=5080 \ - --https-request-port-override=5080 \ - --port-bind-range=22,80,443 \ - --bind-random-ports=false \ - --force-requested-ports=true \ - --force-tcp-address=true \ - --tcp-load-balancer=true \ - --idle-connection=false \ - --proxy-protocol=true \ - --proxy-protocol-version=userdefined + Exec=--config=/config/config.yml [Service] Restart=always RestartSec=5 + MemoryMax=256M [Install] WantedBy=multi-user.target + # OS updates: Zincati stages new FCOS releases automatically, this limits the reboot to a window + - path: /etc/zincati/config.d/55-updates-strategy.toml + mode: 0644 + contents: + inline: | + [updates] + strategy = "periodic" + [[updates.periodic.window]] + days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + start_time = "03:00" # UTC + length_minutes = 60 + # sish updates: no floating tags upstream (only vX.Y.Z), so podman auto-update can't follow a + # version line. This bumps Image= to the newest tag within TRACK and rolls back if sish + # doesn't come up again. + - path: /usr/local/bin/sish-update + mode: 0755 + contents: + inline: | + #!/bin/bash + set -euo pipefail + TRACK=v2. # "v2." = minor + patch releases, "v2.24." = patch releases only + unit=/etc/containers/systemd/sish.container + repo=ghcr.io/antoniomika/sish + + current=$(sed -n "s|^Image=$repo:||p" "$unit") + latest=$(podman search --list-tags --limit 10000 --format '{{.Tag}}' "$repo" \ + | grep -E "^${TRACK//./\\.}[0-9]+(\.[0-9]+)*$" | sort -V | tail -n1) + if [[ -z $latest || $(printf '%s\n' "$current" "$latest" | sort -V | tail -n1) == "$current" ]]; then + exit 0 + fi + + set_image() { + sed -i "s|^Image=.*|Image=$repo:$1|" "$unit" + systemctl daemon-reload + systemctl restart sish.service + } + + echo "sish: $current -> $latest" + podman pull -q "$repo:$latest" >/dev/null + set_image "$latest" + sleep 30 + if ! ss -Htln 'sport = :5002' | grep -q .; then + echo "sish $latest not listening on :5002, rolling back to $current" >&2 + set_image "$current" + exit 1 + fi + podman image prune -af >/dev/null - path: /etc/sysconfig/nftables.conf mode: 0600 overwrite: true @@ -91,7 +193,7 @@ storage: meta l4proto { icmp, ipv6-icmp } accept udp sport 67 udp dport 68 accept ip6 saddr fe80::/10 udp sport 547 udp dport 546 accept - tcp dport { 22, 80, 443, 5001, 5002 } accept + tcp dport { 22, 80, 443, 5001, 5002, 20000-20099 } accept # sish ports: see port-bind-range } } @@ -114,4 +216,27 @@ systemd: WantedBy=multi-user.target - name: nftables.service enabled: true + - name: sish-update.service + contents: | + [Unit] + Description=Update sish within its major version + Wants=network-online.target + After=network-online.target sish.service + + [Service] + Type=oneshot + ExecStart=/usr/local/bin/sish-update + - name: sish-update.timer + enabled: true + contents: | + [Unit] + Description=Daily sish update check + + [Timer] + OnCalendar=*-*-* 05:00:00 UTC + RandomizedDelaySec=30m + Persistent=true + + [Install] + WantedBy=timers.target