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:
19
.sops.yaml
19
.sops.yaml
@@ -43,4 +43,23 @@ creation_rules:
|
|||||||
# LXCs that run a webhook receiver.
|
# LXCs that run a webhook receiver.
|
||||||
age: >-
|
age: >-
|
||||||
# placeholder — fill with age_pubkey of: apps, caddy, claudio-bot, claudio-monitor host
|
# 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
|
# webhook noop 2026-05-20T18:16:57+02:00
|
||||||
|
|||||||
171
bin/homelab
171
bin/homelab
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
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
|
# Secrets every enrolled client should be a recipient on. Each entry is
|
||||||
# (secret-file-path-relative-to-CONTEXT, path_regex used in .sops.yaml).
|
# (secret-file-path-relative-to-CONTEXT, path_regex used in .sops.yaml).
|
||||||
SHARED_SECRETS = [
|
SHARED_SECRETS = [
|
||||||
("secrets/hello.yaml", "^secrets/hello\\.yaml$"),
|
("secrets/hello.yaml", "^secrets/hello\\.yaml$"),
|
||||||
("secrets/gitea-pat.yaml", "^secrets/gitea-pat\\.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
|
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:
|
def cmd_secret(args: argparse.Namespace) -> int:
|
||||||
name = args.name
|
name = args.name
|
||||||
path = CONTEXT / "secrets" / f"{name}.yaml"
|
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)
|
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:
|
def cmd_doctor(args: argparse.Namespace) -> int:
|
||||||
"""Run health checks for the homelab-context client setup."""
|
"""Run health checks for the homelab-context client setup."""
|
||||||
results: list[tuple[str, str, str]] = [] # (status, label, detail)
|
results: list[tuple[str, str, str]] = [] # (status, label, detail)
|
||||||
@@ -1286,6 +1445,14 @@ def main() -> int:
|
|||||||
sp.add_argument("name")
|
sp.add_argument("name")
|
||||||
sp.set_defaults(func=cmd_secret)
|
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 = sub.add_parser("sync", help="manually trigger homelab-context-sync")
|
||||||
sp.set_defaults(func=cmd_sync)
|
sp.set_defaults(func=cmd_sync)
|
||||||
|
|
||||||
|
|||||||
29
hosts/netbird-vps.yaml
Normal file
29
hosts/netbird-vps.yaml
Normal 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
|
||||||
@@ -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:** `82.165.190.79` (`ens6`).
|
||||||
- **Public DNS:** IONOS wildcard `*.hubris.network → 82.165.190.79`.
|
- **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)).
|
- **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
|
## SSH
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,10 @@
|
|||||||
# - hostname keys MUST match the actual `hostname` of the machine (on
|
# - hostname keys MUST match the actual `hostname` of the machine (on
|
||||||
# macOS: `scutil --get LocalHostName` if set).
|
# macOS: `scutil --get LocalHostName` if set).
|
||||||
# - `os:` one of: linux, macos
|
# - `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
|
# - `mesh:` lists addresses the host is reachable at. Both `netbird` and
|
||||||
# `tailscale` are accepted during the migration (see infrastructure/mesh.md).
|
# `tailscale` are accepted during the migration (see infrastructure/mesh.md).
|
||||||
# Prefer netbird FQDNs over raw IPs.
|
# Prefer netbird FQDNs over raw IPs.
|
||||||
@@ -360,3 +363,24 @@ hosts:
|
|||||||
# ssh.user defaults to root; uncomment + set to the actual login user
|
# 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.
|
# before relying on `homelab ssh ludo-mini` or netbird-ssh INTO this host.
|
||||||
# ssh: { user: ludo }
|
# 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.
|
||||||
|
|||||||
39
secrets/netbird-authentik-oidc.yaml
Normal file
39
secrets/netbird-authentik-oidc.yaml
Normal 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
|
||||||
39
secrets/turn-shared-secret.yaml
Normal file
39
secrets/turn-shared-secret.yaml
Normal 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
75
vps/management.json.tmpl
Normal 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
23
vps/turnserver.conf.tmpl
Normal 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
|
||||||
Reference in New Issue
Block a user