Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ env:
DESECSTACK_NSMASTER_ALSO_NOTIFY:
DESECSTACK_NSMASTER_APIKEY: LLq1orOQuXCINUz4TV
DESECSTACK_NSMASTER_TSIGKEY: +++undefined/undefined/undefined/undefined/undefined/undefined/undefined/undefined+++A==
DESECSTACK_NSLORD_KNOT_UPDATE_KEY_SECRET: insecure
DESECSTACK_IPV4_REAR_PREFIX16: 172.16
DESECSTACK_IPV6_SUBNET: bade:affe:dead:beef:b011::/80
DESECSTACK_IPV6_ADDRESS: bade:affe:dead:beef:b011:0642:ac10:0080
Expand All @@ -53,6 +54,19 @@ jobs:
- name: Test desecapi formatting
run: ruff format --check api/

test-watcher:
# runs Knot watcher unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install pytest
run: python3 -m pip install pytest
- name: Run watcher tests
run: python3 -m pytest nslord_knot/tests/test_zone_watch.py

test-missing-migrations:
# test if Django migrations are missing
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ api/venv
# IDE files
.idea

# Local caches
.cache

# development helper scripts
/*.sh

Expand All @@ -14,3 +17,4 @@ api/venv
# Webapp development files
node_modules
package-lock.json
www/webapp/.vite/
27 changes: 27 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## desec-stack agent notes

### Structure
- `api/`: Django REST API and celery worker code.
- `www/webapp/`: Vue/Vite frontend.
- `www/`: nginx configs and static site.
- `docker-compose.yml`: main stack definition.

### Common tasks
- API tests (fast local):
- `docker compose -f docker-compose.yml -f docker-compose.test-api.yml up -d dbapi`
- `cd api`
- `export DJANGO_SETTINGS_MODULE=api.settings_quick_test`
- `python3 manage.py test`
- API formatting:
- `ruff format api/desecapi/`
- Webapp dev/build:
- `cd www/webapp`
- `npm install`
- `npm run dev` (hot reload)
- `npm run build`
- `npm run lint`

### Notes
- Prefer running API tests outside docker with the test DB container.
- Keep changes in `api/` formatted with Ruff before committing.
- e2e2 tests can intermittently hit a 504 on `POST /api/v1/domains/` during startup; a clean `docker compose ... down -v` and rerun resolved it.
17 changes: 16 additions & 1 deletion api/api/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import pprint

import django.utils.log
from django.apps import apps as django_apps
from celery import Celery
from celery.signals import task_failure

Expand All @@ -26,8 +27,23 @@ def debug_task(self):
print("Request: {0!r}".format(self.request))


logger = logging.getLogger(__name__)


def _configure_logger():
if getattr(_configure_logger, "configured", False):
return
if not django_apps.ready:
return
handler = django.utils.log.AdminEmailHandler()
handler.setFormatter(CeleryFormatter())
logger.addHandler(handler)
_configure_logger.configured = True


@task_failure.connect()
def task_failure(task_id, exception, args, kwargs, traceback, einfo, **other_kwargs):
_configure_logger()
try:
sender = other_kwargs.get("sender").name
except AttributeError:
Expand All @@ -49,7 +65,6 @@ def task_failure(task_id, exception, args, kwargs, traceback, einfo, **other_kwa
)


django.setup()
logger = logging.getLogger(__name__)
handler = django.utils.log.AdminEmailHandler()
handler.setFormatter(CeleryFormatter())
Expand Down
29 changes: 29 additions & 0 deletions api/api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,34 @@
NSLORD_PDNS_API_TOKEN = os.environ["DESECSTACK_NSLORD_APIKEY"]
NSMASTER_PDNS_API = "http://nsmaster:8081/api/v1/servers/localhost"
NSMASTER_PDNS_API_TOKEN = os.environ["DESECSTACK_NSMASTER_APIKEY"]
NSLORD_KNOT_HOST = os.environ.get("DESECSTACK_NSLORD_KNOT_HOST", "nslord_knot")
NSLORD_KNOT_PORT = int(os.environ.get("DESECSTACK_NSLORD_KNOT_PORT", "53"))
NSLORD_KNOT_TIMEOUT = float(os.environ.get("DESECSTACK_NSLORD_KNOT_TIMEOUT", "5"))
NSLORD_KNOT_KEY_READY_TIMEOUT = float(
os.environ.get("DESECSTACK_NSLORD_KNOT_KEY_READY_TIMEOUT", "30")
)
NSLORD_KNOT_IMPORT_DIR = os.environ.get(
"DESECSTACK_NSLORD_KNOT_IMPORT_DIR", "/knot-import"
)
NSLORD_KNOT_UPDATE_KEY_NAME = os.environ.get(
"DESECSTACK_NSLORD_KNOT_UPDATE_KEY_NAME", "nslord-update"
)
NSLORD_KNOT_UPDATE_KEY_SECRET = os.environ.get(
"DESECSTACK_NSLORD_KNOT_UPDATE_KEY_SECRET", ""
)
NSLORD_KNOT_UPDATE_KEY_ALGORITHM = os.environ.get(
"DESECSTACK_NSLORD_KNOT_UPDATE_KEY_ALGORITHM", "hmac-sha256"
)
NSLORD_KNOT_TRANSFER_KEY_NAME = os.environ.get(
"DESECSTACK_NSLORD_KNOT_TRANSFER_KEY_NAME", "nsmaster-xfr"
)
NSLORD_KNOT_TRANSFER_KEY_SECRET = os.environ.get(
"DESECSTACK_NSLORD_KNOT_TRANSFER_KEY_SECRET",
os.environ.get("DESECSTACK_NSMASTER_TSIGKEY", ""),
)
NSLORD_KNOT_TRANSFER_KEY_ALGORITHM = os.environ.get(
"DESECSTACK_NSLORD_KNOT_TRANSFER_KEY_ALGORITHM", "hmac-sha256"
)
CATALOG_ZONE = "catalog.internal"

# Celery
Expand All @@ -193,6 +221,7 @@
# pdns accepts request payloads of this size.
# This will hopefully soon be configurable: https://github.com/PowerDNS/pdns/pull/7550
PDNS_MAX_BODY_SIZE = 16 * 1024 * 1024
PDNS_API_TIMEOUT = float(os.environ.get("DESECSTACK_PDNS_API_TIMEOUT", "10"))

# SEPA direct debit settings
SEPA = {
Expand Down
55 changes: 55 additions & 0 deletions api/desecapi/dnssec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import base64
import re

import dns.dnssec
from cryptography.hazmat.primitives.asymmetric import ec


def parse_csk_private_key(private_key: str) -> dict:
if not private_key or not private_key.strip():
raise ValueError("Missing private key material")

algorithm = None
private_b64 = None
for line in private_key.strip().splitlines():
line = line.strip()
if not line:
continue
if line.lower().startswith("algorithm:"):
match = re.search(r"\b(\d+)\b", line)
if match:
algorithm = int(match.group(1))
elif line.lower().startswith("privatekey:"):
private_b64 = line.split(":", 1)[1].strip()

if algorithm is None:
raise ValueError("Missing algorithm in private key")
if private_b64 is None:
raise ValueError("Missing PrivateKey in private key")

if algorithm != 13:
raise ValueError("Unsupported algorithm")

try:
private_bytes = base64.b64decode(private_b64, validate=True)
except Exception as exc:
raise ValueError("Invalid base64 private key") from exc

if len(private_bytes) > 32:
raise ValueError("Invalid private key length")
if len(private_bytes) < 32:
private_bytes = private_bytes.rjust(32, b"\x00")

private_value = int.from_bytes(private_bytes, "big")
if private_value == 0:
raise ValueError("Invalid private key value")

private_key_obj = ec.derive_private_key(private_value, ec.SECP256R1())
dnskey = dns.dnssec.make_dnskey(
private_key_obj.public_key(), algorithm=13, flags=257, protocol=3
).to_text()

return {
"algorithm": algorithm,
"dnskey": dnskey,
}
3 changes: 2 additions & 1 deletion api/desecapi/exception_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from rest_framework.views import exception_handler as drf_exception_handler

from desecapi import metrics
from desecapi.exceptions import PDNSException
from desecapi.exceptions import KnotException, PDNSException


def exception_handler(exc, context):
Expand Down Expand Up @@ -39,6 +39,7 @@ def _500():
IntegrityError: _409,
OSError: _500, # OSError happens on system-related errors, like full disk or getaddrinfo() failure.
PDNSException: _500, # nslord/nsmaster returned an error
KnotException: _500, # knot returned an error
}

for exception_class, handler in handlers.items():
Expand Down
4 changes: 4 additions & 0 deletions api/desecapi/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ class PCHException(ExternalAPIException):
pass


class KnotException(APIException):
pass


class ConcurrencyException(APIException):
status_code = status.HTTP_429_TOO_MANY_REQUESTS
default_detail = "Too many concurrent requests."
Expand Down
Loading
Loading