fix(assist): join link defaults to the public hub URL, not hostname.local

The joinlink base_url defaulted to https://assist.local (LAN-only), so a
shared assistance join link was unreachable over the internet. Derive the
public hub from [api].sso_cookie_domain in secubox.conf
(.gk2.secubox.in -> https://admin.gk2.secubox.in); ASSIST_BASE_URL/--base-url
still override; falls back to assist.local only when no domain is configured.
Same public-base-URL pattern as the Nextcloud _public_base_url fix.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
This commit is contained in:
CyberMind-FR 2026-07-26 10:16:24 +02:00
parent aec4353657
commit 9776112de1
4 changed files with 74 additions and 8 deletions

View File

@ -8,8 +8,14 @@ secubox-assist (0.2.1-1~bookworm1) bookworm; urgency=medium
running as `secubox` via the API, can reach the node signing key to sign
journal ops (offer/request/session) — the shared parent's root:secubox-toolbox
0750 blocked it. Never chowns/tightens the parent; subdirs keep 0700.
* joinlink: the shared join URL now defaults to the PUBLIC hub
(https://admin.<domain>, derived from [api].sso_cookie_domain in
secubox.conf) instead of the LAN-only https://assist.local, so an
assistance join link is reachable over the internet. ASSIST_BASE_URL /
--base-url still override; falls back to assist.local only when no domain
is configured.
-- Gerald KERMA <devel@cybermind.fr> Sat, 26 Jul 2026 06:00:00 +0200
-- Gerald KERMA <devel@cybermind.fr> Sun, 26 Jul 2026 10:10:00 +0200
secubox-assist (0.2.0-1~bookworm1) bookworm; urgency=medium

View File

@ -79,9 +79,11 @@ Env (override; keys are NEVER generated here):
DRYRUN=1 print {"dryrun": true, "would": ...}; write nothing —
no journal append, nothing minted, `list` afterwards
is unchanged.
ASSIST_BASE_URL default https://assist.local — base url `joinlink`
prefixes the single-use token with (override with
--base-url).
ASSIST_BASE_URL public base url `joinlink` prefixes the single-use token
with (override with --base-url). Default: derived from
[api].sso_cookie_domain in secubox.conf -> the public hub
https://admin.<domain>; falls back to https://assist.local
only when no domain is configured.
"""
from __future__ import annotations
@ -156,6 +158,33 @@ def _dryrun_report(would: str, **fields) -> None:
print(json.dumps({"dryrun": True, "would": would, **fields}))
CONF_PATH = os.environ.get("SECUBOX_CONF", "/etc/secubox/secubox.conf")
def _default_base_url() -> str:
"""Public base URL that fronts /assist/join/<tok> — the canonical admin/hub
vhost, so a shared join link is reachable over the internet, not just the
LAN. A hostname.local link only works on the local segment.
Precedence: an explicit ASSIST_BASE_URL env wins; else derive the hub from
[api].sso_cookie_domain in secubox.conf (".gk2.secubox.in" -> the hub is
admin.gk2.secubox.in); else fall back to the LAN-only https://assist.local.
"""
env = os.environ.get("ASSIST_BASE_URL")
if env:
return env
try:
import tomllib
with open(CONF_PATH, "rb") as fh:
conf = tomllib.load(fh)
base = str((conf.get("api", {}) or {}).get("sso_cookie_domain", "")).lstrip(".").strip()
if base and "." in base:
return f"https://admin.{base}"
except Exception:
pass
return "https://assist.local"
# --------------------------------------------------------------------------- #
# commands
# --------------------------------------------------------------------------- #
@ -509,8 +538,7 @@ def main(argv=None) -> int:
jl = sub.add_parser("joinlink", help="mint a single-use, time-boxed join link")
jl.add_argument("--for", dest="for_", required=True, help="opaque reference (e.g. match_id)")
jl.add_argument("--ttl", type=int, required=True)
jl.add_argument("--base-url", dest="base_url",
default=os.environ.get("ASSIST_BASE_URL", "https://assist.local"))
jl.add_argument("--base-url", dest="base_url", default=_default_base_url())
jl.set_defaults(fn=cmd_joinlink)
jo = sub.add_parser("join", help="redeem a join link (plans escalate argv, does not exec)")

View File

@ -67,3 +67,35 @@ def test_join_does_not_leak_private_key(tmp_path):
assert not (len(stripped) == 64 and all(c in "0123456789abcdef"
for c in stripped.lower())), (
f"raw 64-hex secret leaked in join output: {stripped!r}")
def _joinlink_base_url(tmp_path, conf_body=None, extra_env=None):
env = _env(tmp_path); env["DRYRUN"] = "1"
if conf_body is not None:
conf = tmp_path / "secubox.conf"; conf.write_text(conf_body)
env["SECUBOX_CONF"] = str(conf)
else:
env["SECUBOX_CONF"] = str(tmp_path / "does-not-exist.conf")
env.pop("ASSIST_BASE_URL", None)
if extra_env:
env.update(extra_env)
r = subprocess.run([sys.executable, CTL, "joinlink", "--for", "m1", "--ttl", "600"],
env=env, capture_output=True, text=True)
assert r.returncode == 0, r.stderr
return json.loads(r.stdout)["base_url"]
def test_joinlink_base_url_derived_from_sso_cookie_domain(tmp_path):
# a hostname.local link only works on the LAN; derive the public hub instead
url = _joinlink_base_url(tmp_path, '[api]\nsso_cookie_domain = ".gk2.secubox.in"\n')
assert url == "https://admin.gk2.secubox.in"
def test_joinlink_base_url_falls_back_to_local_without_domain(tmp_path):
assert _joinlink_base_url(tmp_path, conf_body=None) == "https://assist.local"
def test_joinlink_base_url_env_override_wins(tmp_path):
url = _joinlink_base_url(tmp_path, '[api]\nsso_cookie_domain = ".gk2.secubox.in"\n',
extra_env={"ASSIST_BASE_URL": "https://custom.example.org"})
assert url == "https://custom.example.org"

View File

@ -69,6 +69,6 @@ def test_postinst_ephemeral_block_only_touches_its_own_subdirectory():
assert "chmod -R /etc/secubox" not in post
def test_changelog_bumped_to_0_2_0():
def test_changelog_bumped_to_0_2_1():
changelog = (ROOT / "debian" / "changelog").read_text()
assert changelog.startswith("secubox-assist (0.2.0-1~bookworm1)")
assert changelog.startswith("secubox-assist (0.2.1-1~bookworm1)")