Configuration
The instance is configured via environment variables. Some settings can also be managed in the admin panel — where the rule is: a set environment variable always wins, and the interface shows its value read-only (intended for cluster deployments where configuration comes from the outside).
Basic settings
Section titled “Basic settings”| Variable | Default | Meaning |
|---|---|---|
DNSE_LISTEN | 127.0.0.1:8422 | Address and port |
DNSE_ENCRYPTION_KEY | — | Required. 32 bytes as hex or Base64 |
DNSE_ADMIN_USER | admin | Username of the bootstrap account |
DNSE_ADMIN_PASSWORD | — | Password on the very first start (no effect afterwards) |
DNSE_SERVE_UI | on | off serves only the API |
DNSE_SECURE_COOKIES | off | Secure for the short-lived SSO handshake cookies (EE) — the login itself no longer uses cookies |
DNSE_TRUSTED_PROXIES | — | Proxy IPs/CIDRs whose X-Forwarded-For is trusted |
DNSE_TLS_CERT | — | Certificate chain (PEM) — the service then speaks HTTPS itself |
DNSE_TLS_KEY | — | Matching private key (PEM); only together with DNSE_TLS_CERT |
DNSE_DEMO | off | on registers the demo provider and seeds sample data |
DNSE_UPDATE_FEED | index on repo.techeve.de | URL of the APT package index for the update hint in the admin area; off disables the check |
Database
Section titled “Database”| Variable | Default | Meaning |
|---|---|---|
DNSE_BACKUP_DIR | — | Directory of the nightly organisation backup (empty = off) |
DNSE_DB | ./dnseditor.db | Path of the SQLite file |
DNSE_DB_DSN | — | PostgreSQL DSN; replaces DNSE_DB |
DNSE_DB_YB | off | on = DNSE_DB_DSN points to a YugabyteDB cluster (YSQL) |
Details in Database and Keys.
Sessions in a cluster
Section titled “Sessions in a cluster”| Variable | Default | Meaning |
|---|---|---|
DNSE_VALKEY_ADDR | — | host:port of a shared Valkey; several addresses (comma) = cluster |
DNSE_VALKEY_PASSWORD | — | AUTH password, if set |
DNSE_SESSION_TTL | 12h | Validity of a sign-in, counted from its last use |
DNSE_SESSION_TOUCH_INTERVAL | — | Minimum gap between two refreshes; empty = every request refreshes |
Without DNSE_VALKEY_ADDR, sessions live in the process memory — exactly
right for a single instance. As soon as several nodes run behind a load
balancer, a shared Valkey belongs in the setup: a login on node 1 is then
valid on all nodes, and a logout takes effect everywhere.
DNSE_SESSION_TOUCH_INTERVAL throttles how often validity is written
forward. Without it, every request writes to the session store —
irrelevant for a single instance, needless constant load on a shared Valkey
for a value nobody needs to the minute. 5m is a workable setting; sign-ins
do not expire any earlier, they are just extended less often.
Certificates (ACME)
Section titled “Certificates (ACME)”| Variable | Default | Meaning |
|---|---|---|
DNSE_ACME_EMAIL | — | Account email; issuance is only active once set |
DNSE_ACME_DIRECTORY | Let’s Encrypt (production) | Alternative ACME directory |
DNSE_ACME_PROPAGATION | — | Fixed wait time instead of checking the authoritative NS, e.g. 90s |
DNSE_ACME_PROPAGATION is needed in split-DNS environments where lego’s
pre-check fails against the internal resolver.
Core settings (admin panel or environment)
Section titled “Core settings (admin panel or environment)”These values live under Administration → Core Settings and can alternatively be provided via environment variables:
| Setting | Variable | Purpose |
|---|---|---|
| Public address | DNSE_INSTANCE_URL | Base for links in outgoing mail |
| DynDNS hostname | DNSE_DYNDNS_HOSTNAME | Update address for routers |
| DynDNS hostname (IPv4 only) | DNSE_DYNDNS_HOSTNAME4 | forces A resolution |
| DynDNS hostname (IPv6 only) | DNSE_DYNDNS_HOSTNAME6 | forces AAAA resolution |
| SMTP server | DNSE_SMTP_HOST | Outgoing mail |
| SMTP port | DNSE_SMTP_PORT | Outgoing mail |
| Sender | DNSE_SMTP_FROM | Sender address |
| SMTP user | DNSE_SMTP_USERNAME | Authentication |
| SMTP password | DNSE_SMTP_PASSWORD | stored encrypted, never returned |
| ALTCHA service | DNSE_ALTCHA_URL | proof-of-work in front of the public forms; empty = off |
| ALTCHA token | DNSE_ALTCHA_TOKEN | stored encrypted, never returned |
Without a configured mail relay, invitations, password resets, and expiry reminders do not work — in that case the interface shows generated passwords directly so they can be handed over manually.
ALTCHA instead of a captcha
Section titled “ALTCHA instead of a captcha”With DNSE_ALTCHA_URL and DNSE_ALTCHA_TOKEN set, the three public
forms — sign-in, registration and “forgot password” — get a small
proof-of-work puzzle in front of them (no cookies, no picture guessing). The
server checks every submission with the service; without a solved puzzle the
form is not processed at all.
Three properties worth knowing:
- Fail closed. If the service does not answer, the submission counts as invalid. Without it the visitor would not have received a puzzle either, and a silent pass would disable the protection during any outage.
- Every solution is valid exactly once. The service remembers submitted solutions; the interface computes a fresh one after each attempt.
- The second sign-in step (two-factor code, organisation choice) requires a solution too — otherwise sending a code along would be enough to try passwords past the gate.
The site’s address must be listed as an origin on the service
(ALTCHA_ORIGINS), otherwise it refuses to hand out puzzles. With nothing
configured, everything stays as before.
Example: systemd unit
Section titled “Example: systemd unit”[Unit]Description=DNS-EditorAfter=network-online.target
[Service]User=dnseditorEnvironment=DNSE_LISTEN=127.0.0.1:8422Environment=DNSE_DB_DSN=postgres://dnseditor:…@localhost:5432/dnseditorEnvironment=DNSE_SECURE_COOKIES=onEnvironment=DNSE_INSTANCE_URL=https://dns.example.comEnvironmentFile=/etc/dnseditor/secrets.envExecStart=/usr/local/bin/dnseditorRestart=on-failure
[Install]WantedBy=multi-user.targetThe encryption key and the SMTP password belong in the EnvironmentFile with
restrictive permissions, not in the unit itself.
Example: nginx as reverse proxy
Section titled “Example: nginx as reverse proxy”server { listen 443 ssl; server_name dns.example.com;
ssl_certificate /etc/ssl/dns.example.com.pem; ssl_certificate_key /etc/ssl/dns.example.com.key;
location / { proxy_pass http://127.0.0.1:8422; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; }}Matching settings on the instance:
DNSE_SECURE_COOKIES=onDNSE_TRUSTED_PROXIES=127.0.0.1DNSE_INSTANCE_URL=https://dns.example.comDNSE_TRUSTED_PROXIES names the address from which the proxy forwards
requests — only its X-Forwarded-For is believed. Without this setting,
DynDNS would record the proxy address instead of the router IP.
One consequence worth knowing: once a source counts as a proxy, only
the header is used. A router that talks to the port directly without
X-Forwarded-For then gets 911 instead of an update. Keep the list
narrow; do not set it to 0.0.0.0/0.
When the proxy speaks HTTPS to its upstream
Section titled “When the proxy speaks HTTPS to its upstream”The setup above is the normal one: TLS ends at the proxy, plain HTTP
behind it. Some proxies are configured for proxy_pass https://… across
the board, often together with proxy_ssl_verify off. The service then
has to hold a certificate itself — otherwise it answers the TLS handshake
with an HTTP 400 and the proxy reports 502:
DNSE_TLS_CERT=/etc/dnseditor/tls/cert.pemDNSE_TLS_KEY=/etc/dnseditor/tls/key.pemBoth belong together — with only one set the service refuses to start (silently speaking plain HTTP where TLS was meant would be the more dangerous answer). If the proxy does not verify the certificate, a self-signed one is enough:
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -keyout key.pem -out cert.pem -subj "/CN=dns.example.com"The key belongs to the service group and stays at 0640.
The interface’s security header
Section titled “The interface’s security header”The interface sends a Content Security Policy that restricts scripts to
its own origin. This is not decoration: now that the login lives as a token
in browser storage (instead of an HttpOnly cookie), injected JavaScript
could read it. The policy keeps foreign script from executing at all.
Practical consequence for operations: a proxy that injects its own scripts — analytics, consent banners, error trackers — will be blocked by the browser. Anyone who needs that has to replace the header in the proxy deliberately, and then knows which protection they are giving up.
Styles may be inline (the components set them at runtime), scripts explicitly may not — that is where the danger sits.