- Python 62.5%
- Jinja 26.9%
- Shell 6.7%
- HCL 2.5%
- HTML 1.1%
- Other 0.2%
* [target v2.26.2] skgraph, skvector: opt-in local-disk data on a pinned node (#149)
* test(skgraph,skvector): local-disk data knobs, red first
Render, guard and directory-task tests for skgraph.DATA_NODE/DATA_PATH and
skvector.DATA_NODE/STORAGE_PATH/SNAPSHOTS_PATH, plus baselines of the
compose render with the render gate example vars, generated from the
skstacks-v2.26.1 templates before any change, so unset knobs are proven
byte-identical to the release.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(skgraph,skvector): opt-in local-disk data on a pinned node
Same design as skhub DATA_NODE (#139). skgraph.DATA_NODE/DATA_PATH and
skvector.DATA_NODE/STORAGE_PATH/SNAPSHOTS_PATH: a path outside /var/data
is local disk, created on DATA_NODE (not recursive, same owner/mode as the
shared-storage copy), the old /var/data path is no longer created, and the
play stops if a local path is set without DATA_NODE. DATA_NODE appends
node.hostname == <DATA_NODE> after the existing constraints (skvector does
not add it twice). skvector named volume devices follow the paths. Unset
knobs render byte-identical to v2.26.1. New READMEs with the key table and
the migration procedure; render gate set-pass vars for both stacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* release: skstacks-v2.26.2 (changelog)
Date the v2.26.2 changelog section before the release-gate run, so the
commit the gate tests is the commit that gets tagged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* [target v2.26.3] skhub: post-deploy URLs use the router hosts; TURN_DOMAIN reaches talk-hpb (#151)
* fix(skhub): post-deploy URLs follow SKHUB_HOSTNAME / COLLABORA_HOSTNAME
notify_push:setup, notify_push base_endpoint, whiteboard collabBackendUrl,
Talk spreed signaling_servers and richdocuments wopi_url were hardcoded to
skhub./collabora.<CLUSTERNAME>.<DOMAIN>, while the Traefik routers and
OVERWRITEHOST use SKHUB_HOSTNAME / COLLABORA_HOSTNAME. On an instance
whose public hosts lack the cluster label, every deploy pointed all five
at names that did not resolve or hit another route (Talk HPB "Unknown
error"). They now use the same keys, falling back to the old pattern, so
with the keys unset every line renders byte-identical to v2.26.2. README
"Public hostnames" and an upgrade note: set the keys before the next
skhub deploy when the public host differs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(skhub): post-deploy URLs use the router hosts; TURN_DOMAIN reaches talk-hpb
Broaden the fix: the five post-deploy settings now use the exact host
expressions of the Traefik routers and OVERWRITEHOST (SKHUB_HOSTNAME,
default skhub[-dev|-staging].<base domain>; COLLABORA_HOSTNAME, default
collabora.<base domain>; base domain honours CLOUDFLARED), so a
CLOUDFLARED instance needs no new vault key. A prod instance without
CLOUDFLARED and without the keys still renders byte-identical to v2.26.2.
talk-hpb.env renders TURN_DOMAIN when set; #145 only fed it to Nextcloud
turn_servers, so each deploy rebuilt talk-hpb without it and Janus fell
back to NC_DOMAIN for TURN. Unset renders as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* [target v2.26.3] skhub: default talk-hpb to a digest-pinned current aio-talk (#152)
aio-talk:20260122_105751 ignores TURN_DOMAIN and writes no TURN server
into janus.jcfg, so Janus offered only its container address and calls
failed ICE on an instance that fell back to the default. Default to the
pinned current image (its start.sh writes turn_server and still writes
the relay_ipv4_addr line the relay wrapper rewrites). It is dinit-based,
so TALK_HPB_CMD defaults to its own CMD when TALK_HPB_IMAGE is unset; a
pinned TALK_HPB_IMAGE keeps the old supervisord default.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* release: skstacks-v2.26.3 (changelog)
Date the v2.26.3 changelog section before the release-gate run, so the
commit the gate tests is the commit that gets tagged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* [target v2.26.3] skhub: talk-hpb relay CMD follows the image, not the pin (#153)
#152 chose dinit only when TALK_HPB_IMAGE was unset, so an instance that
pins TALK_HPB_IMAGE to the new (dinit-based) default digest without
TALK_HPB_CMD would render supervisord and crash-loop. Key the default on
which image runs: dinit for the default digest (unset or pinned to it),
supervisord for any other pinned image. An explicit TALK_HPB_CMD wins.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* [target v2.26.3] skhub: talk-recording one replica; ECH off in the recording Firefox (#154)
Several talk-recording replicas behind one service VIP split the start and
stop of a recording across replicas ("Trying to stop unknown recording",
orphaned recorder). One recording server runs concurrent recordings
itself and the image has no concurrency setting, so replicas is fixed at
1 and TALK_RECORDING_MAX_CONCURRENT is ignored with a deploy warning.
The recording Firefox intermittently sent the ECH outer SNI to the
instance's own Traefik on a Cloudflare-proxied host with an ech= HTTPS
record, got the default certificate and failed with InsecureCertificate.
Render a locked policies.json (ECH, HTTP/3 ECH and native HTTPS-record
lookups off) and bind-mount it read-only into the Firefox distribution
dir. Always on.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(sksso): postgres healthcheck as the configured user; empty data paths mean unset (#157)
The postgres healthcheck ran pg_isready -U postgres. It passed, but on an
instance whose database user is not postgres every probe made the server
log FATAL: role "postgres" does not exist every 10 seconds. It now probes
with the container's own POSTGRES_USER and POSTGRES_DB ($$, never rendered
into the compose file) when sksso.postgres_user is not postgres; with
postgres the render is byte-identical to skstacks-v2.26.3.
The local-disk knobs (DATA_NODE, POSTGRES_DATA_PATH, REDIS_DATA_PATH) have
existed since v2.23.0. An empty data path now means unset, as for skgraph
and skvector; before it rendered a bind with no source and the guard
stopped the play with a misleading message. Unset or empty knobs render
byte-identical to v2.26.3, tested against fixtures rendered from the
v2.26.3 template before this change.
README "Migrating a running instance" rewritten for moving a live instance
off NFS: stop order, rsync -aHAX --numeric-ids, verify by regular-file
count and byte sum (not du --apparent-size), deploy, rename the old dirs
to *.STALE-moved-to-<node>-local-<date>, never delete.
CHANGELOG: skstacks-v2.26.4 section, carrying the skbackup not-release-gated
disclosure forward.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(skhealth): lightweight host-level health checks with alerts [target v2.26.5] (#159)
* feat(skhealth): lightweight host-level health checks with deduplicated alerts
New optional role v1/ansible/optional/skhealth: one stdlib Python script on
a systemd timer (every 5 min) on every swarm node, no daemon or container.
Checks swarm replicas and crash loops, nodes, filesystems and hung NFS
mounts, load/memory/PSI, OOM kills, failed units, SMART and NVMe wear,
ZFS/md RAID, remote read-only hosts over a forced-command SSH wrapper, and
TLS expiry. Telegram and/or webhook alerts on state change, re-alert,
RECOVERED. Verification pending (full suite, lab gate).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(single-manager): a documented every-node exemption for node-local agents
skhealth runs on every swarm node by design. The new `# skstacks: every-node`
marker exempts it from the one-selected-manager rule only with a reason and
only while the playbook stays node-local: no /var/data, no docker stack or
service commands (own test).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(skpayment): optional role for the SKPayment Stripe gateway [target v2.26.5] (#161)
* feat(skpayment): framework optional role for the SKPayment Stripe gateway
New optional role v1/ansible/optional/skpayment reproducing the
hand-deployed gateway: digest-pinned Python base image, app code fetched on
the controller at a pinned APP_REF and verified against APP_SHA256 (fail
closed), Stripe keys and apps.json from the vault in 0600 files, data path
and data node knobs, Traefik router from the framework host expressions,
MIDDLEWARES knob defaulting to default-no-crowdsec@file (flagged in the
README). Verification pending (full suite, lab gate).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(skpayment): image pin visible to list_images; guarded restart form
The compose template writes the default image digest out (list_images.py and
the image canary resolve only inline defaults), kept equal to the playbook
skp_default_image by a test. The restart after a changed code/env/apps.json
uses the standard "preexisted <svc> && ... || true" form that the
first-install guard test checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* sksec: LAPI allowlist sync from ALLOWLIST_CIDRS; per-router TRAEFIK_MIDDLEWARES knobs (#160) (#162)
* feat(v1): per-router TRAEFIK_MIDDLEWARES knobs for ten optional services
skgit, skorch, skpdf, skpeek, skport, skseek, skstor, skvector (HTTP and
gRPC), skpulse and skboard hardcoded their router chain to
default-no-crowdsec@file (skpdf and skvector with their own upload or
buffering middleware). Putting one behind sksec's crowdsec-bouncer@file
meant hand-editing live labels, reverted by the next deploy.
Each router now reads <svc>.TRAEFIK_MIDDLEWARES (skvector's gRPC router
skvector.TRAEFIK_MIDDLEWARES_GRPC), the skgallery convention. The value
replaces the whole chain; unset or empty renders byte-identical to
v2.26.4 (baselines rendered from the v2.26.4 templates before the change).
Documented in docs/v1-service-catalog.md ("Router middleware overrides")
and in the service READMEs that exist; the render gate's set pass now
drives every knob down its set path.
* feat(sksec): sync ALLOWLIST_CIDRS into a CrowdSec LAPI allowlist
The s02-enrich whitelist parser only drops LOCAL events. Community
blocklist (CAPI), console blocklist and `cscli decisions add` decisions
still reached the bouncer and could ban the operator's own ranges.
CrowdSec 1.6.8+ applies a centralized LAPI allowlist to every decision.
After the stack deploy, all three playbooks now include
tasks/lapi_allowlist.yml: resolve the node running the crowdsec task
(docker service ps; TRAEFIK_HA_MODE pins it to the ACME master), delegate
there, wait (bounded by LAPI_WAIT_SECONDS, default 300) for `cscli lapi
status`, then make allowlist LAPI_ALLOWLIST_NAME (default sksec-operator)
hold exactly ALLOWLIST_CIDRS: create it, add missing entries, remove
unlisted ones, verify. Empty list empties it; other allowlists are never
touched; changed only when something changed. cscli runs via argv, no
shell. LAPI_ALLOWLIST_ENABLED (default true) turns it off.
Tests run the real task file through ansible-playbook against a docker
test double modelled on crowdsecurity/crowdsec:v1.6.11 (fresh, idempotent,
drift, empty, rename, LAPI wait, LAPI never ready, no running task,
non-list input). README "Allowlist" rewritten; render gate stubs added.
* docs(changelog): Unreleased entries for the sksec LAPI allowlist and router middleware knobs
* fix: skorch knob reads skorch_cfg; sksec allowlist parse via set_fact
skorch templates read the combined skorch_cfg fact (never skorch.*, see
test_skorch_extra_vars_precedence.py). The LAPI allowlist comparison no
longer uses task-level vars, which the static playbook walkers render
with plain Jinja (no from_json).
---------
(cherry picked from commit
|
||
|---|---|---|
| .github/workflows | ||
| .skred | ||
| assets | ||
| coturn | ||
| docs | ||
| scripts | ||
| templates/config-repo | ||
| tools | ||
| v1 | ||
| v2 | ||
| v3 | ||
| .gitignore | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| LICENSE | ||
| README.md | ||
| renovate.json | ||
SKStacks — Sovereign Infrastructure Configs
Infrastructure-as-code configurations for the SKWorld sovereign stack.
Versions
| Version | Status | Description |
|---|---|---|
| v2/ | 🟢 Active development | Multi-platform (Swarm + K8s + RKE2), pluggable secret backends |
| v1/ | 🟢 Published, one service at a time | Docker Swarm + Ansible, the framework current production clusters run on |
See v2/README.md for the full v2 architecture and quick start, or v1/README.md + the v1 service catalog for v1.
Framework / instance model (v1 and v2 alike)
This repo is the framework: the Ansible roles/playbooks (v1) or
Helm/Kustomize/Terraform (v2) that define each service, with no estate-specific
values baked in. A separate, private instance repo pins this framework as
a framework/ git submodule at a released tag (skstacks-vMAJOR.MINOR.PATCH),
and supplies everything specific to that estate: the inventory, the vault
tree with real secrets, and which optional services it deploys. Upgrading an
instance means bumping the submodule to a newer tag, not editing framework
code in place. See v2/docs/INSTANCE-MODEL.md
for the full model (it predates v1's publication but applies the same way).
v2 Highlights
- 3 secret backends: vault-file (Ansible) · HashiCorp Vault · CapAuth/PGP sovereign
- 3 platforms: Docker Swarm · Kubernetes · RKE2 (Rancher, CIS-hardened)
- 4 CI/CD targets: Forgejo Actions · GitHub Actions · GitLab CI · ArgoCD GitOps
- Sanitized public templates — no real secrets, all
CHANGEME_*placeholders
Standalone Services
| Directory | Service | Purpose |
|---|---|---|
coturn/ |
coturn TURN/STUN server | WebRTC NAT traversal relay for turn.skworld.io |
coturn — Sovereign TURN Server
STUN/TURN server for WebRTC ICE negotiation. Used by SKComm's WebRTC transport and SKChat voice/data channels as the NAT traversal fallback when direct P2P fails.
Domain: turn.skworld.io
Architecture
WebRTC Peer A coturn WebRTC Peer B
│ │ │
│── STUN binding req ──────→ │
│←── mapped address ──────── │
│ │ │
│ (direct ICE fails) │ │
│── TURN Allocate ──────────→ │
│←── relay address ───────── │
│── TURN CreatePermission ──→ (for Peer B's IP) │
│ │ │
│══ encrypted DTLS-SRTP through relay ════════════════│
For Tailscale-connected peers, the Tailscale 100.x IP is used as a host ICE candidate and Tailscale DERP handles relay — coturn is only needed for non-tailnet peers.
Quick Setup
1. Install coturn:
# Debian/Ubuntu
sudo apt install coturn
# Arch/Manjaro
sudo pacman -S coturn
2. Generate a shared secret:
openssl rand -hex 32
# → copy this into turnserver.conf static-auth-secret AND
# into ~/.skcomm/config.yml transports.webrtc.settings.turn_secret
# (or set SKCOMM_TURN_SECRET env var)
3. Deploy the config:
sudo cp coturn/turnserver.conf /etc/coturn/turnserver.conf
# Edit: set static-auth-secret, uncomment external-ip=YOUR_PUBLIC_IP
4. Set up TLS (recommended):
certbot certonly --standalone -d turn.skworld.io
# Then uncomment the cert/pkey lines in turnserver.conf
5. Open firewall ports:
ufw allow 3478/udp # STUN/TURN
ufw allow 3478/tcp # TURN over TCP
ufw allow 5349/tcp # TURN over TLS
ufw allow 49152:65535/udp # UDP relay range
6. Start and enable:
sudo systemctl enable --now coturn
sudo systemctl status coturn
Configuration Summary
| Setting | Value |
|---|---|
| STUN/TURN port | 3478 (UDP + TCP) |
| TLS TURN port | 5349 |
| UDP relay range | 49152–65535 |
| Realm | turn.skworld.io |
| Auth | HMAC-SHA1 time-limited credentials |
| Credential TTL | 86400s (24h) |
| Private IP relay | Denied (SSRF protection) |
Client Integration (SKComm)
# ~/.skcomm/config.yml
transports:
webrtc:
enabled: true
priority: 1
settings:
turn_server: "turn:turn.skworld.io:3478"
turn_secret: "${SKCOMM_TURN_SECRET}" # same secret as static-auth-secret
The SKComm WebRTC transport generates HMAC-SHA1 time-limited credentials automatically:
# credential = HMAC-SHA1(secret, f"{expiry}:{username}")
# username = f"{expiry}:{agent_name}"
Testing
# Test STUN binding (should return your public IP)
stun turn.skworld.io
# Test TURN allocation (requires valid credentials)
turnutils_uclient -u "<expiry>:<username>" -w "<credential>" turn.skworld.io
# Check logs
journalctl -u coturn -f
tail -f /var/log/coturn/turnserver.log
License
AGPL-3.0-or-later. Part of the smilinTux sovereign stack.