Archived
Require karl prefixed bearer secrets
This commit is contained in:
1 parent
8f07bbde84
commit
d2cad52ae9
6 files changed
+33
-29
No files matched your search
@@ -305,7 +305,7 @@ The create response includes `registration_url`, `update_url`, and `dashboard_ur
|
||||
Use the dashboard secret to rotate the update URL:
|
||||
|
||||
```bash
|
||||
curl -sS -X POST https://karldns.de/api/v1/hostnames/<dashboard-secret>/rotate-update-url
|
||||
curl -sS -X POST https://karldns.de/api/v1/hostnames/<karl_dashboard_secret>/rotate-update-url
|
||||
```
|
||||
|
||||
The response contains a replacement `update_url`. The old update URL stops working immediately.
|
||||
|
||||
@@ -4,8 +4,8 @@ from typing import Annotated
|
||||
|
||||
from fastapi import Path
|
||||
|
||||
TOKEN_URLSAFE_PATTERN = r"^[A-Za-z0-9_-]+$"
|
||||
TOKEN_MIN_LENGTH = 32
|
||||
TOKEN_URLSAFE_PATTERN = r"^karl_[A-Za-z0-9_-]+$"
|
||||
TOKEN_MIN_LENGTH = 37
|
||||
TOKEN_MAX_LENGTH = 128
|
||||
|
||||
UpdateSlugPath = Annotated[
|
||||
@@ -14,7 +14,7 @@ UpdateSlugPath = Annotated[
|
||||
min_length=TOKEN_MIN_LENGTH,
|
||||
max_length=TOKEN_MAX_LENGTH,
|
||||
pattern=TOKEN_URLSAFE_PATTERN,
|
||||
description="Private update URL value.",
|
||||
description="Private update URL value. Must start with karl_.",
|
||||
),
|
||||
]
|
||||
ManagementSlugPath = Annotated[
|
||||
@@ -23,6 +23,6 @@ ManagementSlugPath = Annotated[
|
||||
min_length=TOKEN_MIN_LENGTH,
|
||||
max_length=TOKEN_MAX_LENGTH,
|
||||
pattern=TOKEN_URLSAFE_PATTERN,
|
||||
description="Private dashboard URL value.",
|
||||
description="Private dashboard URL value. Must start with karl_.",
|
||||
),
|
||||
]
|
||||
@@ -159,10 +159,10 @@ def _render_api_docs_page(settings: DdnsSettings, request_ip: str | None = None)
|
||||
base_url = settings.public_base_url.rstrip("/")
|
||||
example_ipv4, _ = _example_update_ips(request_ip)
|
||||
create_example = f"curl -sS -X POST {base_url}/api/v1/hostnames"
|
||||
register_example = f"curl -sS -X POST '{base_url}/api/v1/router/register/<dashboard-secret>?myip={example_ipv4}'"
|
||||
update_example = f"curl -sS '{base_url}/api/v1/router/update/<update-secret>?myip={example_ipv4}'"
|
||||
status_example = f"curl -sS '{base_url}/api/v1/hostnames/<dashboard-secret>?history_limit=10'"
|
||||
rotate_example = f"curl -sS -X POST {base_url}/api/v1/hostnames/<dashboard-secret>/rotate-update-url"
|
||||
register_example = f"curl -sS -X POST '{base_url}/api/v1/router/register/<karl_dashboard_secret>?myip={example_ipv4}'"
|
||||
update_example = f"curl -sS '{base_url}/api/v1/router/update/<karl_update_secret>?myip={example_ipv4}'"
|
||||
status_example = f"curl -sS '{base_url}/api/v1/hostnames/<karl_dashboard_secret>?history_limit=10'"
|
||||
rotate_example = f"curl -sS -X POST {base_url}/api/v1/hostnames/<karl_dashboard_secret>/rotate-update-url"
|
||||
schema_example = f"curl -sS {base_url}/openapi.json"
|
||||
description = "Small KarlDNS API guide for creating, activating, updating, reading, and deleting generated Dynamic DNS hostnames."
|
||||
return _page(
|
||||
@@ -183,11 +183,11 @@ def _render_api_docs_page(settings: DdnsSettings, request_ip: str | None = None)
|
||||
<h2>Endpoints</h2>
|
||||
<ol class="simple-steps text-start">
|
||||
<li><strong>POST /api/v1/hostnames.</strong> Create a pending hostname and private setup values. The response includes <code>registration_url</code>.</li>
|
||||
<li><strong>GET or POST /api/v1/router/register/<secret>.</strong> Register the hostname after secrets are saved. Uses the request IP, or <code>myip</code>/<code>myipv6</code>, for the initial DNS record.</li>
|
||||
<li><strong>GET or POST /api/v1/router/update/<secret>.</strong> Router-friendly update endpoint. Supports <code>myip</code>, <code>ipaddr</code>, <code>myipv6</code>, and <code>ip6addr</code>.</li>
|
||||
<li><strong>GET /api/v1/hostnames/<secret>.</strong> Read hostname status and recent history.</li>
|
||||
<li><strong>POST /api/v1/hostnames/<secret>/rotate-update-url.</strong> Replace the private router update URL.</li>
|
||||
<li><strong>DELETE /api/v1/hostnames/<secret>.</strong> Delete hostname and DNS records.</li>
|
||||
<li><strong>GET or POST /api/v1/router/register/<karl_secret>.</strong> Register the hostname after secrets are saved. Uses the request IP, or <code>myip</code>/<code>myipv6</code>, for the initial DNS record.</li>
|
||||
<li><strong>GET or POST /api/v1/router/update/<karl_secret>.</strong> Router-friendly update endpoint. Supports <code>myip</code>, <code>ipaddr</code>, <code>myipv6</code>, and <code>ip6addr</code>.</li>
|
||||
<li><strong>GET /api/v1/hostnames/<karl_secret>.</strong> Read hostname status and recent history.</li>
|
||||
<li><strong>POST /api/v1/hostnames/<karl_secret>/rotate-update-url.</strong> Replace the private router update URL.</li>
|
||||
<li><strong>DELETE /api/v1/hostnames/<karl_secret>.</strong> Delete hostname and DNS records.</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
+5
-5
@@ -30,10 +30,10 @@ def test_custom_api_docs_include_only_current_api_surfaces(tmp_path: Path) -> No
|
||||
assert redoc.status_code == 404
|
||||
assert "Small API surface." in docs.text
|
||||
assert "POST /api/v1/hostnames" in docs.text
|
||||
assert "GET or POST /api/v1/router/register/<secret>" in docs.text
|
||||
assert "GET or POST /api/v1/router/update/<secret>" in docs.text
|
||||
assert "GET /api/v1/hostnames/<secret>" in docs.text
|
||||
assert "POST /api/v1/hostnames/<secret>/rotate-update-url" in docs.text
|
||||
assert "GET or POST /api/v1/router/register/<karl_secret>" in docs.text
|
||||
assert "GET or POST /api/v1/router/update/<karl_secret>" in docs.text
|
||||
assert "GET /api/v1/hostnames/<karl_secret>" in docs.text
|
||||
assert "POST /api/v1/hostnames/<karl_secret>/rotate-update-url" in docs.text
|
||||
assert "Rotate update URL" in docs.text
|
||||
assert "Operator API" not in docs.text
|
||||
assert "/api/v1/admin" not in docs.text
|
||||
@@ -242,7 +242,7 @@ def test_hostname_api_history_is_scoped_to_dashboard_secret(tmp_path: Path) -> N
|
||||
assert "source_ip" not in payload["history"][0]
|
||||
assert "other.ddns.example.net" not in response.text
|
||||
assert client.get(f"/api/v1/hostnames/{other['management_slug']}", params={"history_limit": 101}).status_code == 422
|
||||
assert client.get(f"/api/v1/hostnames/{'a' * 32}").status_code == 404
|
||||
assert client.get(f"/api/v1/hostnames/karl_{'a' * 32}").status_code == 404
|
||||
|
||||
|
||||
def test_registration_publishes_initial_dns_when_provider_is_configured(tmp_path: Path, monkeypatch) -> None:
|
||||
|
||||
@@ -104,9 +104,9 @@ printf 'updater placeholder\\n'
|
||||
EOS
|
||||
;;
|
||||
*'/api/v1/hostnames'*)
|
||||
printf '%s\\n' '{{"hostname":"abc.karldns.test","status":"pending","update_url":"https://karldns.test/api/v1/router/update/update-secret?myip=<ipaddr>&myipv6=<ip6addr>","dashboard_url":"https://karldns.test/m/manage-secret","registration_url":"https://karldns.test/api/v1/router/register/manage-secret"}}'
|
||||
printf '%s\\n' '{{"hostname":"abc.karldns.test","status":"pending","update_url":"https://karldns.test/api/v1/router/update/karl_update_secret_12345678901234567890?myip=<ipaddr>&myipv6=<ip6addr>","dashboard_url":"https://karldns.test/m/karl_manage_secret_12345678901234567890","registration_url":"https://karldns.test/api/v1/router/register/karl_manage_secret_12345678901234567890"}}'
|
||||
;;
|
||||
*'/api/v1/router/register/manage-secret'*)
|
||||
*'/api/v1/router/register/karl_manage_secret_12345678901234567890'*)
|
||||
printf '%s\\n' 'good registered'
|
||||
;;
|
||||
*)
|
||||
@@ -136,11 +136,11 @@ esac
|
||||
|
||||
log = log_file.read_text(encoding="utf-8")
|
||||
assert "/api/v1/hostnames" in log
|
||||
assert "/api/v1/router/register/manage-secret" in log
|
||||
assert "/api/v1/router/register/karl_manage_secret_12345678901234567890" in log
|
||||
assert "KarlDNS: registering hostname" in result.stdout
|
||||
config = home_dir / ".config" / "karldns" / "updater.env"
|
||||
config_text = config.read_text(encoding="utf-8")
|
||||
assert "KARLDNS_UPDATE_URL='https://karldns.test/api/v1/router/update/update-secret'" in config_text
|
||||
assert "KARLDNS_UPDATE_URL='https://karldns.test/api/v1/router/update/karl_update_secret_12345678901234567890'" in config_text
|
||||
assert "KARLDNS_HOSTNAME='abc.karldns.test'" in config_text
|
||||
|
||||
|
||||
@@ -172,9 +172,9 @@ printf 'updater placeholder\\n'
|
||||
EOS
|
||||
;;
|
||||
*'/api/v1/hostnames'*)
|
||||
printf '%s\\n' '{{"hostname":"abc.karldns.test","status":"pending","update_url":"https://karldns.test/api/v1/router/update/update-secret?myip=<ipaddr>&myipv6=<ip6addr>","dashboard_url":"https://karldns.test/m/manage-secret","registration_url":"https://karldns.test/api/v1/router/register/manage-secret"}}'
|
||||
printf '%s\\n' '{{"hostname":"abc.karldns.test","status":"pending","update_url":"https://karldns.test/api/v1/router/update/karl_update_secret_12345678901234567890?myip=<ipaddr>&myipv6=<ip6addr>","dashboard_url":"https://karldns.test/m/karl_manage_secret_12345678901234567890","registration_url":"https://karldns.test/api/v1/router/register/karl_manage_secret_12345678901234567890"}}'
|
||||
;;
|
||||
*'/api/v1/router/register/manage-secret'*)
|
||||
*'/api/v1/router/register/karl_manage_secret_12345678901234567890'*)
|
||||
printf '%s\\n' 'good registered'
|
||||
;;
|
||||
*)
|
||||
@@ -220,7 +220,7 @@ cp "$1" {crontab_file}
|
||||
assert "KARLDNS_CONFIG=" in cron_text
|
||||
assert "karldns-update" in cron_text
|
||||
assert log.count("/api/v1/hostnames") == 1
|
||||
assert log.count("/api/v1/router/register/manage-secret") == 1
|
||||
assert log.count("/api/v1/router/register/karl_manage_secret_12345678901234567890") == 1
|
||||
assert (home_dir / ".local" / "bin" / "karldns-update").exists()
|
||||
|
||||
run_script("sh", str(ROOT / "scripts" / "uninstall-linux.sh"), "--mode", "user", env=env)
|
||||
|
||||
@@ -21,7 +21,7 @@ def test_generated_hostname_is_not_an_update_secret(tmp_path: Path) -> None:
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1/router/update/{'a' * 43}",
|
||||
f"/api/v1/router/update/karl_{'a' * 43}",
|
||||
params={"hostname": "public-target.ddns.example.net", "myip": "203.0.113.9"},
|
||||
)
|
||||
|
||||
@@ -56,8 +56,8 @@ def test_bearer_link_routes_share_rate_limit_bucket(tmp_path: Path) -> None:
|
||||
)
|
||||
client = TestClient(app)
|
||||
|
||||
first = client.get(f"/m/{'a' * 43}")
|
||||
second = client.get(f"/m/{'b' * 43}")
|
||||
first = client.get(f"/m/karl_{'a' * 43}")
|
||||
second = client.get(f"/m/karl_{'b' * 43}")
|
||||
|
||||
assert first.status_code == 404
|
||||
assert second.status_code == 429
|
||||
@@ -92,6 +92,10 @@ def test_redacted_access_log_masks_bearer_paths_and_query_strings(tmp_path: Path
|
||||
"/api/v1/router/register/short",
|
||||
"/api/v1/hostnames/short",
|
||||
"/m/short",
|
||||
"/api/v1/router/update/not_karl_but_long_enough_1234567890",
|
||||
"/api/v1/router/register/not_karl_but_long_enough_1234567890",
|
||||
"/api/v1/hostnames/not_karl_but_long_enough_1234567890",
|
||||
"/m/not_karl_but_long_enough_1234567890",
|
||||
],
|
||||
)
|
||||
def test_secret_path_tokens_are_constrained(tmp_path: Path, path: str) -> None:
|
||||
|
||||
Reference in new issue
Block a user