- Go 68.6%
- PLpgSQL 14%
- Shell 9.9%
- TypeScript 3.6%
- JavaScript 3.3%
- Other 0.5%
| .forgejo/workflows | ||
| cmd | ||
| deploy | ||
| docs | ||
| internal | ||
| migrations | ||
| proofs | ||
| proto/agent/v1 | ||
| scripts | ||
| tests | ||
| web | ||
| .dockerignore | ||
| .gitignore | ||
| AGENTS.md | ||
| buf.gen.yaml | ||
| buf.yaml | ||
| go.mod | ||
| go.sum | ||
| KnotOps-KICKOFF-PROMPT.md | ||
| knotops-logo.svg | ||
| README.md | ||
| TASKS.md | ||
| toolchain.lock.json | ||
KnotOps
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
apiserves the HTTP API and the compiled web frontend.workerperforms durable DNS work and verifies authoritative results.controlleraccepts the typed, authenticated agent protocol.migratorapplies the database schema as a one-shot process.agentruns 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 imagesbuilds, checks, and publishes the API, worker, controller, and migrator images.Build agent releasebuilds 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 publisherREGISTRY_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:
- Complete the read-only P11.1 inventory and compare it with the fixed topology.
- Create the database roles, protected secret mounts, and runtime configuration files.
- Resolve every candidate image to the digest recorded by Forgejo.
- Run the migrator once with its own database role.
- Start the API, worker, and controller with writes disabled.
- Verify health, identity, logs, metrics, database access, and authoritative DNS over UDP and TCP.
- 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:
- Create the domain request and complete its TXT ownership proof.
- Register one distinct enabled primary and secondary agent.
- Set each agent's exact public nameserver name.
- Add the existing zone to both protected agent mappings at the next managed generation.
- Register the zone's active read-only TSIG reference for the worker.
- 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.