No description
  • Go 68.6%
  • PLpgSQL 14%
  • Shell 9.9%
  • TypeScript 3.6%
  • JavaScript 3.3%
  • Other 0.5%
Find a file
Stefan Tanta 2bda60c2cb
Some checks failed
Build agent release / build (push) Successful in 1m21s
Publish control-plane images / publish (push) Successful in 2m49s
Continuous verification / verify (push) Failing after 2h21m13s
P8.8: expose guarded ownership retry to administrator owners
2026-09-04 22:57:42 +02:00
.forgejo/workflows P10.9: remove agent release backfill 2026-09-02 10:28:03 +02:00
cmd P10.8: complete operational integration 2026-09-01 13:41:37 +02:00
deploy P4.10/P7.9/P8.9: add existing zone onboarding 2026-09-04 15:29:13 +02:00
docs P8.8: expose guarded ownership retry to administrator owners 2026-09-04 22:57:42 +02:00
internal P8.8: expose guarded ownership retry to administrator owners 2026-09-04 22:57:42 +02:00
migrations P8.1: authorize queued proof checks independently of login sessions 2026-09-04 22:38:47 +02:00
proofs P4.8/P4.9: add local MFA and API tokens 2026-09-02 17:28:22 +02:00
proto/agent/v1 P4.10/P7.9/P8.9: add existing zone onboarding 2026-09-04 15:29:13 +02:00
scripts P8.1: authorize queued proof checks independently of login sessions 2026-09-04 22:38:47 +02:00
tests P8.1: authorize queued proof checks independently of login sessions 2026-09-04 22:38:47 +02:00
web P8.8: expose guarded ownership retry to administrator owners 2026-09-04 22:57:42 +02:00
.dockerignore P1.7: build hardened OCI images 2026-08-23 04:36:32 +00:00
.gitignore P11.2: add Forgejo release artifacts 2026-09-02 02:40:50 +02:00
AGENTS.md add canonical KnotOps logo 2026-08-22 17:07:57 +00:00
buf.gen.yaml P7.1: checkpoint agent protocol implementation 2026-08-26 19:44:18 +00:00
buf.yaml P7.1: checkpoint agent protocol implementation 2026-08-26 19:44:18 +00:00
go.mod P4.8/P4.9: add local MFA and API tokens 2026-09-02 17:28:22 +02:00
go.sum P4.8/P4.9: add local MFA and API tokens 2026-09-02 17:28:22 +02:00
KnotOps-KICKOFF-PROMPT.md P4.8: define local authentication controls 2026-09-02 12:56:59 +02:00
knotops-logo.svg add canonical KnotOps logo 2026-08-22 17:07:57 +00:00
README.md P10.9: harden registry publication verification 2026-09-04 20:12:50 +02:00
TASKS.md P8.8: expose guarded ownership retry to administrator owners 2026-09-04 22:57:42 +02:00
toolchain.lock.json P10.9: publish permanent agent releases 2026-09-02 10:25:48 +02:00

KnotOps

KnotOps logo

KnotOps is a self-hosted control plane for authoritative DNS domains managed by Knot DNS. It manages domain records, zone lifecycle, DNSSEC, and TSIG within explicit tenant and server boundaries.

Components

  • api serves the HTTP API and the compiled web frontend.
  • worker performs durable DNS work and verifies authoritative results.
  • controller accepts the typed, authenticated agent protocol.
  • migrator applies the database schema as a one-shot process.
  • agent runs on a Knot host as a constrained systemd service.

The frontend is not a separate runtime image. The API image contains the exact frontend build for the same release.

Authentication

KnotOps supports optional Authelia OIDC and invitation-only local accounts. A new installation has one local administrator bootstrap. Its protected one-use secret creates the first platform administrator, the default organization, and the owner membership. The administrator must enroll TOTP before sign-in.

Authelia users in sso_dns_portal_users get a KnotOps user on their first valid MFA login. KnotOps derives platform-administrator status only from the exact sso_dns_portal_admins group. A platform administrator creates later local accounts and gives each one-use enrollment secret through a separate secure channel. Public registration and knowledge-based recovery are not available.

Users can create API tokens for one organization. Each token has a fixed dns.read or dns.read plus dns.write scope and expires within 90 days. KnotOps displays a new token once and stores only its keyed digest. API tokens cannot manage credentials, accounts, membership, or destructive zone lifecycle actions. Current organization membership and role still apply to each token request. Local suspension, recovery, password changes, and local platform-role changes revoke that user's tokens.

Forgejo releases

Two tag-only Forgejo Actions workflows create release artifacts:

  • Publish control-plane images builds, checks, and publishes the API, worker, controller, and migrator images.
  • Build agent release builds the production systemd agent twice, compares the binaries, creates a deterministic archive, publishes its checksum, and attaches the verified files to a permanent Forgejo Release.

Configure a trusted Forgejo runner with the docker label. Give the runner a dedicated Docker daemon. Do not share the daemon with untrusted repositories. Add these repository Actions secrets:

  • REGISTRY_USERNAME: a dedicated Forgejo package publisher
  • REGISTRY_TOKEN: its limited package-write token

Allow only trusted release maintainers to create version tags. Protect those tags against deletion and replacement. Push a final semantic version tag to run both workflows:

git tag v1.2.3
git push origin v1.2.3

Manual workflow dispatch is accepted only when the selected ref is a valid release tag. Pull requests and branches cannot publish a release. The workflow does not create a mutable latest tag.

The release page contains the agent archive, SHA256SUMS, the verified content list, and public build metadata. Actions also retains the same files for 30 days as workflow evidence.

The control-plane workflow publishes these Linux AMD64 images:

git.step1.ro/stanta/knotops/api:1.2.3
git.step1.ro/stanta/knotops/worker:1.2.3
git.step1.ro/stanta/knotops/controller:1.2.3
git.step1.ro/stanta/knotops/migrator:1.2.3

Deploy the manifest digest from the workflow evidence, not the version tag. If a run publishes only some images, treat the release as failed. Do not replace the existing tags. Review the uploaded release.env and retry with a new version after the cause is fixed. Remote preflight accepts only an explicit, target-bound missing-manifest response as proof that a tag is absent. The workflow stops on an authentication, network, proxy, or timeout error. After each push, the Docker daemon pulls the exact returned digest and verifies its local repository digest before the workflow records success.

Control-plane installation

Production deployment needs PostgreSQL, Traefik, telemetry, and the fixed network flows in deploy/operations. Use the configuration examples in deploy/config. Store secret values only in protected files or mounts. Do not put them in image settings or command arguments.

Use this order for a first production deployment:

  1. Complete the read-only P11.1 inventory and compare it with the fixed topology.
  2. Create the database roles, protected secret mounts, and runtime configuration files.
  3. Resolve every candidate image to the digest recorded by Forgejo.
  4. Run the migrator once with its own database role.
  5. Start the API, worker, and controller with writes disabled.
  6. Verify health, identity, logs, metrics, database access, and authoritative DNS over UDP and TCP.
  7. Keep production DNS writes disabled until the later P11 gates pass.

The exact update, rollback, emergency mode, and verification procedure is in the operations runbook. It does not authorize a production DNS mutation.

The API needs local-authentication-keys in its protected secret directory. The file protects TOTP seeds, local enrollment digests, and API-token digests through separate derived keys. It contains exactly 32 random bytes for a new installation. During a planned key rotation, it can contain 64 bytes: the new key first and the prior key second. Keep this file out of logs, backups without encryption, command arguments, and the repository.

Create a separate initial administrator secret as unpadded base64url text. Do not print it in a terminal transcript:

umask 077
openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\n' \
  > bootstrap-administrator-secret

Mount this 43-character file as bootstrap-administrator-secret. Keep it available to the API after enrollment. PostgreSQL stores only its keyed digest and rejects reuse. Open the KnotOps site, select "Set up the first administrator", and finish TOTP enrollment. Set both oidc and oidc_client_secret to null when the installation has no OIDC provider.

Agent installation

The agent workflow produces knotops-agent-VERSION-linux-amd64.tar.gz, SHA256SUMS, CONTENTS, and release.env. Copy the archive and checksum to the Knot host through an authenticated channel. Verify them before installation:

sha256sum --check SHA256SUMS
tar --list --gzip --file knotops-agent-1.2.3-linux-amd64.tar.gz

Inspect CONTENTS and confirm that the version is the approved release. Extract the fixed package tree only after that check:

sudo tar --extract --gzip \
  --file knotops-agent-1.2.3-linux-amd64.tar.gz \
  --directory / \
  --strip-components=1 \
  --no-same-owner
sudo systemd-sysusers knotops-agent.conf
sudo systemd-tmpfiles --create knotops-agent.conf
sudo install -d -o root -g root -m 0755 \
  /etc/systemd/system/knotops-agent.service.d

Copy the two configuration examples from /usr/share/knotops-agent/examples to a private staging directory. Replace all placeholders. Set one exact controller IP in 20-controller-network.conf. Set public_nameserver to the agent's canonical public FQDN with a trailing dot. Use distinct names for the primary and secondary agents. Register the same values in agent_machine_identities.public_nameserver before either agent connects. The controller rejects a different value in the authenticated hello. Install the reviewed files with these destinations and modes:

sudo install -o root -g knotops-agent -m 0440 agent.json /etc/knotops/agent.json
sudo install -o root -g knotops-agent -m 0440 agent-mappings.json /etc/knotops/agent-mappings.json
sudo install -o root -g root -m 0444 20-controller-network.conf \
  /etc/systemd/system/knotops-agent.service.d/20-controller-network.conf
sudo /usr/local/sbin/render-knotops-agent-exec-paths \
  | sudo tee /etc/systemd/system/knotops-agent.service.d/10-knotc-exec-paths.conf >/dev/null
sudo chmod 0444 /etc/systemd/system/knotops-agent.service.d/10-knotc-exec-paths.conf
sudo systemd-analyze verify knotops-agent.service

Provision agent credentials as protected files under /etc/knotops/secrets/agent. Never copy secret values into logs, Forgejo artifacts, screenshots, or the repository. Start the service after the protected configuration and credential files are complete:

sudo systemctl daemon-reload
sudo systemctl enable --now knotops-agent.service

Confirm the service identity, network restriction, controller connection, and metrics before adoption.

Existing zone adoption

KnotOps can adopt an unsigned zone that the registered primary and secondary already serve. Adoption does not change DNS or Knot configuration.

Prepare these items before you request adoption:

  1. Create the domain request and complete its TXT ownership proof.
  2. Register one distinct enabled primary and secondary agent.
  3. Set each agent's exact public nameserver name.
  4. Add the existing zone to both protected agent mappings at the next managed generation.
  5. Register the zone's active read-only TSIG reference for the worker.
  6. Confirm that both authorities permit the authenticated primary AXFR.

Open the verified zone and select "Adopt current DNS". The worker checks the exact parent delegation, both authenticated agents, UDP and TCP answers, matching SOA serials, and one complete primary transfer. Review every imported RRset and approve its digest. KnotOps repeats the complete read-only check. It activates the zone only when the second result matches the approved review.

Unsupported Knot settings stay external. Adoption sends no configuration reconcile operation. A mismatch, timeout, changed review, partial agent result, or unknown result cannot activate the zone. Read the current state before you retry an unknown request.

This release adopts only unsigned zones with no parent DS. Keep a signed zone read only until the production DNSSEC adoption gate verifies key restore and DS rollback.

Local verification

The full release check needs Docker and can take more than one hour:

bash scripts/ci/verify.sh

For a faster source check, use the pinned Go and Node versions from toolchain.lock.json:

go test ./...
cd web
npm ci --ignore-scripts --no-audit --no-fund
npm run typecheck
npm test

The kickoff document defines the product and trust boundaries. TASKS.md defines the ordered acceptance gates.