sops-encrypt TURN password + Authentik client secret; homelab render-vps-configs

The IONOS netbird VPS held two credentials in plaintext that were the last
holdouts from the homelab's sops+age secrets pattern:
  - /root/turn-pass.txt  (coturn long-term-credential password)
  - PKCEAuthorizationFlow.ProviderConfig.ClientSecret inline in
    /opt/management.json (Authentik OIDC client secret)

This commit moves both into sops-encrypted YAML in the repo and adds a render
command that recreates the VPS config files from templates + decrypted secrets:

* secrets/turn-shared-secret.yaml      — encrypted `password: <coturn pwd>`
* secrets/netbird-authentik-oidc.yaml  — encrypted `client_secret: <...>`
  Both recipients = hubris + apps + republic-laptop (same 3 as hello.yaml).
* vps/turnserver.conf.tmpl + vps/management.json.tmpl — templates with
  {{TURN_PASSWORD}} + {{AUTHENTIK_CLIENT_SECRET}} placeholders.
* bin/homelab new subcommand `render-vps-configs`:
  - Decrypts both secrets locally (works on any recipient).
  - Substitutes placeholders into templates.
  - Diffs against current VPS state via ssh, prompts, applies atomically
    (write `.new` then mv), restarts coturn + netbird-mgmt.
  - --dry-run + -y flags. Hops through hubris when not running on hubris
    itself, since VPS sshd is locked to hubris's pubkey.
* inventory.yaml adds the VPS as `kind: external` (new kind; reserved for
  ssh-managed hosts that aren't homelab clients themselves — no age key,
  no /opt/homelab-context). hosts/netbird-vps.yaml regenerated.
* SHARED_SECRETS list includes both new secrets so re-keys on enrollment
  changes pick them up automatically.

After this lands + the 5-min sync propagates to hubris, run from hubris PVE
shell (or any client; hubris just skips the extra ssh hop):

  homelab render-vps-configs --dry-run    # see plan, no changes
  homelab render-vps-configs -y           # apply + restart services

Once verified working, the plaintext `/root/turn-pass.txt` should be deleted
on the VPS (the rendered /etc/turnserver.conf no longer needs it as a
reference).
This commit is contained in:
2026-05-21 22:21:06 +02:00
parent 21063015c7
commit 8ef17dba3d
9 changed files with 420 additions and 4 deletions

View File

@@ -43,4 +43,23 @@ creation_rules:
# LXCs that run a webhook receiver.
age: >-
# placeholder — fill with age_pubkey of: apps, caddy, claudio-bot, claudio-monitor host
- path_regex: ^secrets/turn-shared-secret\.yaml$
# coturn TURN long-term-credential password. Consumed by hubris (which
# renders /etc/turnserver.conf + /opt/management.json on the VPS via
# `homelab render-vps-configs`). Other recipients are convenience for
# operator debugging — only hubris's pubkey is strictly required.
age: >-
age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6,
age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0,
age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6
- path_regex: ^secrets/netbird-authentik-oidc\.yaml$
# Authentik OIDC client secret for the netbird-dashboard provider.
# Consumed by hubris to render /opt/management.json on the VPS
# (PKCEAuthorizationFlow.ProviderConfig.ClientSecret).
age: >-
age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6,
age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0,
age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6
# webhook noop 2026-05-20T18:16:57+02:00

View File

@@ -16,6 +16,7 @@ from __future__ import annotations
import argparse
import json
import os
import shlex
import shutil
import socket
import subprocess
@@ -189,8 +190,13 @@ def push_inventory(message: str, extra_paths: list[str] | None = None) -> None:
# Secrets every enrolled client should be a recipient on. Each entry is
# (secret-file-path-relative-to-CONTEXT, path_regex used in .sops.yaml).
SHARED_SECRETS = [
("secrets/hello.yaml", "^secrets/hello\\.yaml$"),
("secrets/gitea-pat.yaml", "^secrets/gitea-pat\\.yaml$"),
("secrets/hello.yaml", "^secrets/hello\\.yaml$"),
("secrets/gitea-pat.yaml", "^secrets/gitea-pat\\.yaml$"),
# VPS-render secrets — only strictly required on hubris (the rendering
# proxy), but kept on the SHARED list so any operator can `homelab secret
# turn-shared-secret` / `... netbird-authentik-oidc` for debugging.
("secrets/turn-shared-secret.yaml", "^secrets/turn-shared-secret\\.yaml$"),
("secrets/netbird-authentik-oidc.yaml", "^secrets/netbird-authentik-oidc\\.yaml$"),
]
@@ -588,6 +594,31 @@ def cmd_status(args: argparse.Namespace) -> int:
return 0
def _decrypt_secret(name: str) -> dict:
"""Decrypt secrets/<name>.yaml and parse as YAML. Re-execs via sudo when
invoked as a non-root user (the age key at /etc/age/key.txt is 0600 root)."""
path = CONTEXT / "secrets" / f"{name}.yaml"
if not path.exists():
die(f"no secret '{name}' (looked for {path})")
if os.geteuid() != 0:
proc = subprocess.run(
["sudo", "-E", "env", f"SOPS_AGE_KEY_FILE={AGE_KEY}",
"sops", "-d", str(path)],
capture_output=True, text=True,
)
else:
if not AGE_KEY.exists():
die(f"no age key at {AGE_KEY} — has bootstrap run?")
env = {**os.environ, "SOPS_AGE_KEY_FILE": str(AGE_KEY)}
proc = subprocess.run(
["sops", "-d", str(path)],
capture_output=True, text=True, env=env,
)
if proc.returncode != 0:
die(f"sops decrypt of '{name}' failed: {proc.stderr.strip() or '(no stderr)'}")
return yaml.safe_load(proc.stdout)
def cmd_secret(args: argparse.Namespace) -> int:
name = args.name
path = CONTEXT / "secrets" / f"{name}.yaml"
@@ -608,6 +639,134 @@ def cmd_secret(args: argparse.Namespace) -> int:
return subprocess.call(["sops", "-d", str(path)], env=env)
# ---------- render-vps-configs ----------
#
# The IONOS netbird VPS hosts plaintext credentials that we now keep encrypted
# in secrets/. This command re-renders the affected config files on the VPS by
# decrypting the secrets locally and pushing the rendered output. Idempotent —
# safe to re-run; it shows a diff and prompts before applying.
# Map of (template path in repo, target path on VPS, mode, restart cmd after).
_VPS_RENDER_TARGETS = [
{
"tmpl": "vps/turnserver.conf.tmpl",
"remote": "/etc/turnserver.conf",
"mode": "0644",
"restart": "systemctl restart coturn",
},
{
"tmpl": "vps/management.json.tmpl",
"remote": "/opt/management.json",
"mode": "0600",
"restart": "docker restart netbird-mgmt",
},
]
def _vps_remote_cmd(remote_cmd: str) -> list[str]:
"""Build the argv that, when run, executes `remote_cmd` on the IONOS VPS.
The VPS only accepts hubris's pubkey (per infrastructure/vps-hardening.md),
so when we're not on hubris we hop through it: laptop → ssh hubris → ssh
netbird-vps. On hubris we go direct.
"""
direct = ssh_base("netbird-vps") # ["ssh", "root@netbird-ionos..."]
try:
on_hubris = socket.gethostname() == "hubris"
except Exception:
on_hubris = False
if on_hubris:
return direct + [remote_cmd]
# laptop → hubris → vps. shlex-quote the remote_cmd so it survives
# hubris's shell parsing as a single argument to the inner ssh.
inner = " ".join(direct) + " " + shlex.quote(remote_cmd)
return ssh_base("hubris") + [inner]
def _vps_send(remote_path: str, content: str, mode: str) -> None:
"""Atomically write `content` to `remote_path` on the VPS with given mode.
Writes to <path>.new, chmods, then mv-replaces."""
new_path = f"{remote_path}.new"
write_cmd = f"umask 077 && cat > {new_path} && chmod {mode} {new_path}"
proc = subprocess.run(
_vps_remote_cmd(write_cmd),
input=content, text=True, capture_output=True,
)
if proc.returncode != 0:
die(f"failed writing {new_path}: {proc.stderr.strip()}")
mv = subprocess.run(_vps_remote_cmd(f"mv {new_path} {remote_path}"),
capture_output=True, text=True)
if mv.returncode != 0:
die(f"failed renaming {new_path} → {remote_path}: {mv.stderr.strip()}")
def cmd_render_vps_configs(args: argparse.Namespace) -> int:
"""Re-render /etc/turnserver.conf + /opt/management.json on the IONOS netbird
VPS from templates in vps/, substituting secrets decrypted from sops."""
# Decrypt the two sops files we need.
turn = _decrypt_secret("turn-shared-secret")
auth = _decrypt_secret("netbird-authentik-oidc")
subs = {
"{{TURN_PASSWORD}}": turn["password"],
"{{AUTHENTIK_CLIENT_SECRET}}": auth["client_secret"],
}
# Render each template and compare against current VPS content.
plans = []
for t in _VPS_RENDER_TARGETS:
tmpl_path = CONTEXT / t["tmpl"]
if not tmpl_path.exists():
die(f"missing template {tmpl_path}")
rendered = tmpl_path.read_text()
for needle, value in subs.items():
rendered = rendered.replace(needle, value)
cat = subprocess.run(_vps_remote_cmd(f"cat {t['remote']} 2>/dev/null"),
capture_output=True, text=True)
current = cat.stdout
plans.append({
**t,
"rendered": rendered,
"changed": current != rendered,
"current_present": cat.returncode == 0 and bool(current),
})
# Summarize.
print("Render plan for IONOS netbird VPS:")
any_changes = False
for p in plans:
status = "(changed)" if p["changed"] else "(unchanged)"
print(f" {p['remote']:<28} {status}")
if p["changed"]:
any_changes = True
if not any_changes:
print("Nothing to do — every target matches the rendered template.")
return 0
if args.dry_run:
print("\n--- dry-run: not applying ---")
return 0
if not args.yes:
if not confirm("apply changes + restart services on netbird-vps?"):
return 1
# Apply changed targets, then restart their services.
for p in plans:
if not p["changed"]:
continue
print(f"writing {p['remote']} ({p['mode']}) ...")
_vps_send(p["remote"], p["rendered"], p["mode"])
print(f" → {p['restart']}")
rc = subprocess.run(_vps_remote_cmd(p["restart"]),
capture_output=True, text=True)
if rc.returncode != 0:
print(f" ! restart failed: {rc.stderr.strip()}", file=sys.stderr)
return 1
print("done.")
return 0
def cmd_doctor(args: argparse.Namespace) -> int:
"""Run health checks for the homelab-context client setup."""
results: list[tuple[str, str, str]] = [] # (status, label, detail)
@@ -1286,6 +1445,14 @@ def main() -> int:
sp.add_argument("name")
sp.set_defaults(func=cmd_secret)
sp = sub.add_parser("render-vps-configs",
help="re-render /etc/turnserver.conf + /opt/management.json on the IONOS netbird VPS from templates + sops secrets")
sp.add_argument("--dry-run", action="store_true",
help="show what would change; don't apply or restart")
sp.add_argument("-y", "--yes", action="store_true",
help="skip the confirmation prompt")
sp.set_defaults(func=cmd_render_vps_configs)
sp = sub.add_parser("sync", help="manually trigger homelab-context-sync")
sp.set_defaults(func=cmd_sync)

29
hosts/netbird-vps.yaml Normal file
View File

@@ -0,0 +1,29 @@
# Generated by mcp/build_host_files.py from inventory.yaml.
# Do NOT edit by hand — your changes will be overwritten.
# Source of truth: ../inventory.yaml
name: netbird-vps
kind: external
os: linux
role: netbird-mgmt
mesh:
netbird:
ip: 100.122.165.149
fqdn: netbird-ionos.netbird.selfhosted
mesh_globals:
primary: netbird
accepted:
- netbird
- tailscale
ssh:
user: root
notes:
- "Public IONOS VPS \u2014 hosts the vanilla netbird mgmt+signal+relay+dashboard stack + host coturn (see\
\ infrastructure/vps-hardening.md + infrastructure/mesh.md changelog 2026-05-21)."
- NOT a homelab client. No /etc/age/key.txt, no /opt/homelab-context clone. Managed via ssh from hubris;
sshd is locked to hubris's pubkey.
- Public IPv4 82.165.190.79. Auto-patching via unattended-upgrades.
- Configs rendered by `homelab render-vps-configs` from vps/turnserver.conf.tmpl + vps/management.json.tmpl,
with secrets decrypted from secrets/turn-shared-secret.yaml + secrets/netbird-authentik-oidc.yaml on
hubris.
mcp_endpoint: https://mcp.hubris.network/sse
secrets_issuance_endpoint: https://secrets.hubris.network/issue

View File

@@ -9,7 +9,8 @@ IONOS VPS that runs the Netbird control plane and the [public ingress traefik](i
- **Public:** `82.165.190.79` (`ens6`).
- **Public DNS:** IONOS wildcard `*.hubris.network → 82.165.190.79`.
- **Docker stack** at `/opt/docker-compose.yml`: `traefik` (TLS/ACME) + `dashboard` + `mgmt` + `signal` + `relay` + `proxy` — netbird-mgmt 0.71.3 vanilla deploy since 2026-05-21 (see [mesh.md changelog](mesh.md#changelog)).
- **Host services (outside docker):** `coturn` (TURN-TCP on :3478, long-term creds at `/root/turn-pass.txt`, used by mgmt's `TURNConfig`).
- **Host services (outside docker):** `coturn` (TURN-TCP on :3478, long-term creds rendered into `/etc/turnserver.conf` by `homelab render-vps-configs` from sops-encrypted `secrets/turn-shared-secret.yaml`).
- **Config rendering:** `/etc/turnserver.conf` + `/opt/management.json` are generated from templates in `vps/*.tmpl` on this repo by `homelab render-vps-configs`. Secret placeholders (`{{TURN_PASSWORD}}`, `{{AUTHENTIK_CLIENT_SECRET}}`) are substituted from sops-encrypted secrets decrypted on hubris and pushed over ssh. **Do not hand-edit those two files on the VPS** — the next render will overwrite them.
## SSH

View File

@@ -7,7 +7,10 @@
# - hostname keys MUST match the actual `hostname` of the machine (on
# macOS: `scutil --get LocalHostName` if set).
# - `os:` one of: linux, macos
# - `kind:` one of: proxmox-host, lxc, vm, workstation
# - `kind:` one of: proxmox-host, lxc, vm, workstation, external
# ("external" is reserved for hosts the homelab CLI manages via ssh but
# that aren't homelab clients themselves — e.g. the IONOS netbird VPS
# with no /etc/age/key.txt and no /opt/homelab-context clone.)
# - `mesh:` lists addresses the host is reachable at. Both `netbird` and
# `tailscale` are accepted during the migration (see infrastructure/mesh.md).
# Prefer netbird FQDNs over raw IPs.
@@ -360,3 +363,24 @@ hosts:
# ssh.user defaults to root; uncomment + set to the actual login user
# before relying on `homelab ssh ludo-mini` or netbird-ssh INTO this host.
# ssh: { user: ludo }
netbird-vps:
kind: external
os: linux
role: netbird-mgmt
mesh:
netbird:
ip: 100.122.165.149
fqdn: netbird-ionos.netbird.selfhosted
ssh:
user: root
notes:
- Public IONOS VPS — hosts the vanilla netbird mgmt+signal+relay+dashboard
stack + host coturn (see infrastructure/vps-hardening.md +
infrastructure/mesh.md changelog 2026-05-21).
- NOT a homelab client. No /etc/age/key.txt, no /opt/homelab-context
clone. Managed via ssh from hubris; sshd is locked to hubris's pubkey.
- Public IPv4 82.165.190.79. Auto-patching via unattended-upgrades.
- Configs rendered by `homelab render-vps-configs` from
vps/turnserver.conf.tmpl + vps/management.json.tmpl, with secrets
decrypted from secrets/turn-shared-secret.yaml +
secrets/netbird-authentik-oidc.yaml on hubris.

View File

@@ -0,0 +1,39 @@
client_secret: ENC[AES256_GCM,data:p8csBIMAuDb1BYLLL9aYIifShpw9uIWm1Z0c59SlzBDP3SJD4I8Z6ipYBETisgxcECr+78zs2wtuebtcHZ3DBqF5PZaG87TJEGotTkjOVSjDLKgelMq0TDfnpWyR1yedsi6/SiRhm4ggTz0Vwwo+TLDNkw3Gv7EP7fZQphYdMyA=,iv:MZcvhiBwDc8FWcNqQvCiTL+9OVEfHpZrzt1kmcKcxdA=,tag:FmEIrw7u2wYiF3ksymC5lA==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzVVZIQzlPak4xTVVPMm9w
M2tFbTFoQmpRUGFETjF1ZTQ3ajlEdGxWQmtZCkdkZzdhblFybGV3OEUrWERBWFUv
bTdXSnZFSU8vY1JJVTJjL3dLdWpiNGsKLS0tIHVlVmcrcTNZUGs4MmJtWkR0aTh3
Z0pTOWhsZWYyVW5TeXk5ZHhaQnZvZ3MKrnBt5T7WjSxGYvRc1olfhuMN6nOEJbbX
xoliPcKkGsBExXVgkpood+OdlH8dNAaT0z1+INzNiBAZ8SazZA4p9g==
-----END AGE ENCRYPTED FILE-----
- recipient: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjYk1SOVZxUTBWYUw4aUhy
WG93N2xVWDlEWnBqQ0JTRWcxc2tXUnppSFF3Cmp6L1g4OUliVlNFYUo2bEtNOHhm
aytHK0RFWmNpSWFMaGc1cnNXNlEwbTQKLS0tIHMxU3VXYTZHNWk3cWZFNXBNRElD
bE1CUHc1TFlsbGU2aTJjSmhwZENuUDgKvGHg2Df/eBw5akRPYFLvXhzh7P6jTOgj
E56n29EJ+p4kTkC8yVBci01qpMioL+Wx2Rt+X+0LWrGBzu5fic2U5g==
-----END AGE ENCRYPTED FILE-----
- recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwSGdBYzlsYmc4dVJ5ZE8r
Q1FMR1NQL2lzZ1lmaTg1bGROa0hOcVk2ZTJvCllFMUtJdG5JVFhseWc5eXFsYk9p
Q2tIVm5yelAreVRrVkQrTnJ1N3FLclEKLS0tIGQ5dTdqZ3lKWU1KcHdHK1UwajIw
Znh5aXkwNmdyVi9EZlBzMUpTTW1VdjgKVVNGjiiKw3nLxq4YsCWoTS4R8wUD8wqD
awXSIOvZj5xiz6NvFk1X5T3H4XeEm6tKKzOpBvoVn5yl3zgn/bh5pA==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-05-21T20:13:20Z"
mac: ENC[AES256_GCM,data:bi4o17skIOtZoxJGzLFJ7IAv+X265qIhgnQz6wr4bch2xofwPZZfzECck0I0xDhcwxcDtx3VZ5E2VBdvHWk+iSan+clRq7735k9+DIpjdWaGxIHuylmelZl6aE5kE36UO7fQdnwmiqCPHxiVq/sKuqknFXkl1+FvO1ryrtSHQsU=,iv:xxm+5rK7o2c43iGS1j2Q274QCRsbDn+k+wskpp5tzeU=,tag:sZHk9eth/TwqM8tIWSrv3w==,type:str]
pgp: []
unencrypted_suffix: _unencrypted
version: 3.9.4

View File

@@ -0,0 +1,39 @@
password: ENC[AES256_GCM,data:C9uA7LHAc/SmWJsICGMNLu2OOYyowIhLq/sTfuNo/w==,iv:211oFm7MdphnXwYszRxWrHLeKw7qeh0BNzP0yomTUTk=,tag:zfhfkzLm0AivKwD7TZTEqw==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiaFhuUlZJMkxERlR1T2xL
dnVKMXNMNVBIdDFuYzJtTjJWZE9JZnlaQ1dJCnNNbzJKc3VXOUZ1QUFzRWhSUkdF
WVJlNUtJSytSS2h5NGQyMHA4d2U0c28KLS0tIFVhSGJQOWlEQlp6SlRmS0ZMZzJ4
Y2ZoVVVkeVNiYUNwWFFLd2VGaTR1VUkK9Dpk0kjuKoUh3zfVQV7qs/YTTg2BaOkg
kNYY01k9MftEwdtvpKk1ogzdHyhGFJ1yEepwK7se6W5KHDJXyVfA8w==
-----END AGE ENCRYPTED FILE-----
- recipient: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4N2xmZEM3R2tMQjA4a1Fr
cWZ5a2lwUWlJZDdVSExnaHA3cEpubTVMWG1BCjNjTEJzL01PWm9xRWlEWUlWend5
TUdMMjUzNE1RN0h5bzJuQnl5QkJXVzAKLS0tIDZ6aVh6dXlxTTVPZDRKQUkyZmlk
a2FLZ2s1NTA1ZUljeFBhSDZEcGZLNlkKcX5b3dXcJZejeSP4TLr1cOXQj7YEjj0G
znXBdDt6c42qE5XS+LGciunf3MmYxt0xXDb11cnSSEVgn0VyqE2x6w==
-----END AGE ENCRYPTED FILE-----
- recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBybWtTZEw3NTdwMWZhOUMv
L1RNWGduU1BSM0FodWMwUEtHNkxSY3MvdGhzCm1qTGlnM2ZRM2RoekhZbjFzZ2tE
Qk1DNjlnMk01R1ZzUDBubTMvR2hPNmcKLS0tIGsxZ2lQMi9vY0Zpb3pwRHBSSm5t
OWtkeCtNNnpTVXV6S1p2Y2E0V2toL1EK3RI0RgM0SudRguOpOimke7niuX3cIIVi
X+zK75QbvLN7pe1pIIC2UOEmRB9BhgGMVssHxnLzTJv0LUKjDMGdbw==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-05-21T20:13:20Z"
mac: ENC[AES256_GCM,data:PXXEpDbJIoIwWuNwMetpALtxrcB7yJhDYqp6LSKV8WFQI2TJSDgDHiNQ2cngfB9PXKmUNNhlvhmB52Hi6WQSWQdXjiy79T1fOjMhbUAKmykTaZDrwvlrDGkPgiZ1cFSb8+hx/5aZw7YhdQO9+7LAmymeIldZIFEpEO2pvuPZ6MM=,iv:mJb3K8TnsnY183OX7o9pF7oVMDcwVU/rOgX74KTRpr4=,tag:f+M+FT0Vjozm7a8LFDI8lA==,type:str]
pgp: []
unencrypted_suffix: _unencrypted
version: 3.9.4

75
vps/management.json.tmpl Normal file
View File

@@ -0,0 +1,75 @@
{
"_comment": "Rendered from this template by `homelab render-vps-configs`. DO NOT edit /opt/management.json on the VPS directly; it is recreated from this template on each render. Secret placeholders are {{TURN_PASSWORD}} and {{AUTHENTIK_CLIENT_SECRET}}.",
"Stuns": [
{"Proto": "udp", "URI": "stun:stun.l.google.com:19302", "Username": "", "Password": null},
{"Proto": "udp", "URI": "stun:stun1.l.google.com:19302", "Username": "", "Password": null},
{"Proto": "udp", "URI": "stun:stun.cloudflare.com:3478", "Username": "", "Password": null}
],
"TURNConfig": {
"Turns": [
{"Proto": "tcp", "URI": "turn:netbird.hubris.network:3478?transport=tcp", "Username": "netbird", "Password": "{{TURN_PASSWORD}}"}
],
"CredentialsTTL": "12h",
"Secret": "not-used-when-time-based-false",
"TimeBasedCredentials": false
},
"Relay": {
"Addresses": ["rels://netbird.hubris.network:443"],
"CredentialsTTL": "24h",
"Secret": "f6vaBSTqv53Jl9Fr+zUkzJ6iIsKv0RoYt++hKRqq58Q"
},
"Signal": {
"Proto": "https",
"URI": "netbird.hubris.network:443",
"Username": "",
"Password": null
},
"ReverseProxy": {
"TrustedHTTPProxies": ["172.30.0.0/24"],
"TrustedHTTPProxiesCount": 0,
"TrustedPeers": ["0.0.0.0/0"]
},
"Datadir": "",
"DataStoreEncryptionKey": "U60qK19PEpe6LSocYs1OR+qeoE2rUq6tN+W20NAC+gs=",
"StoreConfig": {"Engine": "sqlite"},
"DisableDefaultPolicy": false,
"HttpConfig": {
"Address": "0.0.0.0:80",
"AuthIssuer": "https://auth.hubris.network/application/o/netbird/",
"AuthAudience": "netbird-dashboard",
"AuthUserIDClaim": "sub",
"AuthKeysLocation": "https://auth.hubris.network/application/o/netbird/jwks/",
"OIDCConfigEndpoint": "https://auth.hubris.network/application/o/netbird/.well-known/openid-configuration",
"IdpSignKeyRefreshEnabled": true,
"CertFile": "",
"CertKey": ""
},
"IdpManagerConfig": {
"ManagerType": "none"
},
"DeviceAuthorizationFlow": {
"Provider": "hosted",
"ProviderConfig": {
"ClientID": "netbird-dashboard",
"Audience": "netbird-dashboard",
"Domain": "auth.hubris.network",
"TokenEndpoint": "https://auth.hubris.network/application/o/token/",
"DeviceAuthEndpoint": "https://auth.hubris.network/application/o/device/",
"Scope": "openid profile email offline_access",
"UseIDToken": false
}
},
"PKCEAuthorizationFlow": {
"ProviderConfig": {
"ClientID": "netbird-dashboard",
"ClientSecret": "{{AUTHENTIK_CLIENT_SECRET}}",
"Audience": "netbird-dashboard",
"Domain": "auth.hubris.network",
"TokenEndpoint": "https://auth.hubris.network/application/o/token/",
"AuthorizationEndpoint": "https://auth.hubris.network/application/o/authorize/",
"Scope": "openid profile email offline_access",
"UseIDToken": false,
"RedirectURLs": ["http://localhost:53000/"]
}
}
}

23
vps/turnserver.conf.tmpl Normal file
View File

@@ -0,0 +1,23 @@
# coturn for netbird symmetric-NAT peers — rendered from this template by
# `homelab render-vps-configs`. DO NOT edit /etc/turnserver.conf on the VPS
# directly; that file is recreated from this template on each render.
listening-port=3478
listening-ip=0.0.0.0
relay-ip=82.165.190.79
external-ip=82.165.190.79
min-port=49152
max-port=49999
fingerprint
lt-cred-mech
realm=netbird.hubris.network
user=netbird:{{TURN_PASSWORD}}
no-stun
no-multicast-peers
no-cli
no-loopback-peers
no-tlsv1
no-tlsv1_1
no-udp