milestone 20: declarative configuration for iac
Gates / test-aarch64 (push) Successful in 6m45s
Gates / frontend (push) Successful in 51s
Gates / test (push) Successful in 1m37s
Gates / container (push) Failing after 7m31s
Gates / package (push) Failing after 15m14s
CI / gates (push) Failing after 24m27s
Gates / test-aarch64 (push) Successful in 6m45s
Gates / frontend (push) Successful in 51s
Gates / test (push) Successful in 1m37s
Gates / container (push) Failing after 7m31s
Gates / package (push) Failing after 15m14s
CI / gates (push) Failing after 24m27s
This commit is contained in:
@@ -4,7 +4,7 @@ Every section, field and collection nxdns accepts, with its type, default,
|
||||
unit, validation rule and the subsystem that consumes it.
|
||||
|
||||
Source of truth: `src/config/model.zig` (the model and the defaults),
|
||||
`src/config/validate.zig` (the rules), `src/config/{bootstrap,import,export}.zig`
|
||||
`src/config/validate.zig` (the rules), `src/config/{loader,reconcile,import,export}.zig`
|
||||
(the lifecycle).
|
||||
|
||||
For how the file, the database and `export`/`import` relate to each other, see
|
||||
@@ -17,7 +17,7 @@ reference](cli.md).
|
||||
The file is ZON: a top-level anonymous struct whose fields are the sections and
|
||||
collections below. Enum values are ZON enum literals (`.level = .err`,
|
||||
`.response = .nxdomain`). Strings are double-quoted. The file may be at most
|
||||
4 MiB (`max_config_bytes` in `src/config/import.zig`); beyond that the error is
|
||||
4 MiB (`max_config_bytes` in `src/config/loader.zig`); beyond that the error is
|
||||
`ConfigTooLarge`. A syntax error is reported with its line and column.
|
||||
|
||||
Absent fields keep their defaults, both in the file and in the database. A
|
||||
@@ -38,7 +38,7 @@ Storage paths are process arguments, not configuration:
|
||||
| Argument | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `--data-dir DIR` | `/var/lib/nxdns` | Holds `config.db` and `querylog.db`; see [files and directories](files-and-directories.md). |
|
||||
| `--config FILE` | `/etc/nxdns/config.zon` | Names the seed file. |
|
||||
| `--config FILE` | — | On `run`, makes FILE the sole source of configuration; on `check`, grades FILE instead of the database. No default: without it the database is the configuration. |
|
||||
| `--web-dev DIR` | — | `run` only; serves the web interface from a directory instead of the embedded assets. |
|
||||
|
||||
## Scalar sections
|
||||
@@ -115,8 +115,8 @@ The web interface and REST API.
|
||||
| `web.enabled` | bool | true | — | — | gates the whole web stack: server, sessions, SSE hub, API limiter (`src/app.zig`) |
|
||||
| `web.bind` | string | `"0.0.0.0"` | IP address | must parse as an IP address of either family | web listener bind (`src/web/server.zig`) |
|
||||
| `web.port` | u16 | 8080 | port | 1–65535 (0 is refused) | web listener port |
|
||||
| `web.password` | string | `""` | — | must not be set together with `web.password_hash` | operator input only; hashed at import and discarded. Never a settings row — see [Password and hash](#password-and-hash) |
|
||||
| `web.password_hash` | string | `""` | — | — | argon2id PHC string verified at login (`src/web/auth.zig`); `""` disables authentication |
|
||||
| `web.password` | optional string | absent | — | must not be set together with `web.password_hash`; the empty string is refused | operator input only; hashed and discarded. Never a settings row — see [Password and hash](#password-and-hash) |
|
||||
| `web.password_hash` | optional string | absent | — | — | argon2id PHC string verified at login (`src/web/auth.zig`); `""` disables authentication, absent keeps the stored hash |
|
||||
| `web.session_ttl_hours` | u16 | 24 | hours | at least 1 | session expiry and cookie `Max-Age` (`src/web/auth.zig`) |
|
||||
| `web.api_rate_limit_per_min` | u32 | 300 | requests per minute | at least 1 | API token-bucket limiter (`src/web/api_limiter.zig`) |
|
||||
| `web.api_localhost_exempt` | bool | true | — | — | loopback requests skip the API limiter |
|
||||
@@ -341,27 +341,59 @@ deadline.
|
||||
|
||||
## Password and hash
|
||||
|
||||
Exactly one of `web.password` and `web.password_hash` may be set; setting both
|
||||
is refused (`PasswordAndHashBothSet` — ambiguity in a security setting).
|
||||
Both fields are optional, and the difference between *absent* and *empty* is the
|
||||
whole design. Absent means "keep whatever is stored". Empty means "there is no
|
||||
password".
|
||||
|
||||
- `web.password` is operator input only. At import time it is hashed with
|
||||
argon2id (OWASP parameters: t=2, m=19 MiB, p=1, PHC encoding) into
|
||||
`web.password_hash` and discarded. There is no `web.password` settings row,
|
||||
and `nxdns export` always writes `.password = ""`.
|
||||
| The file says | What happens to the stored hash |
|
||||
| --- | --- |
|
||||
| Neither field | Untouched. Authentication stays exactly as it was. |
|
||||
| `.password = "some-password"` | Verified against the stored hash; kept when it matches, replaced with a fresh hash when it does not. |
|
||||
| `.password = ""` | Refused, with a diagnostic naming the remedy. |
|
||||
| `.password_hash = "$argon2id$…"` | Written verbatim. |
|
||||
| `.password_hash = ""` | Cleared, which disables authentication. |
|
||||
| Both fields | Refused (`PasswordAndHashBothSet` — ambiguity in a security setting). |
|
||||
|
||||
Silence has to mean "keep", because the alternative is a trap. An operator who
|
||||
exports a configuration and trims the long PHC string out of it before
|
||||
committing the file to git means "leave the password alone", not "open the admin
|
||||
interface to the LAN". So disabling authentication takes the explicit empty
|
||||
string, and the empty *plaintext* — which would otherwise hash into a real hash
|
||||
that no login can ever satisfy — is refused outright:
|
||||
|
||||
```
|
||||
FAIL web.password: password is set to the empty string; omit the field to keep the stored password, or set password_hash = "" to disable authentication
|
||||
```
|
||||
|
||||
- `web.password` is operator input only. It is hashed with argon2id (OWASP
|
||||
parameters: t=2, m=19 MiB, p=1, PHC encoding) into `web.password_hash` and
|
||||
discarded. There is no `web.password` settings row, and `nxdns export` always
|
||||
writes `.password = null`.
|
||||
- `web.password_hash` is the stored argon2id PHC string. Supplying it directly,
|
||||
for example from a previous export, is how a backup restores authentication
|
||||
without knowing the password.
|
||||
- Both empty disables web authentication entirely.
|
||||
|
||||
Because the export carries the hash and re-importing an exported file takes the
|
||||
"password is empty" branch, the export/import round trip preserves the hash
|
||||
byte for byte. See [set up admin
|
||||
A plaintext password that has not changed is verified rather than re-hashed, so
|
||||
applying the same file twice leaves the same bytes in the database. That is what
|
||||
keeps the export/import round trip byte-stable with a password in the file. It
|
||||
costs a full argon2id computation either way — the verification is not a
|
||||
shortcut, and caching the plaintext to skip it would be a security bug.
|
||||
|
||||
Any change to whether a password is set is announced at startup, never left as a
|
||||
count:
|
||||
|
||||
```
|
||||
web authentication is now enabled
|
||||
web authentication is now disabled
|
||||
```
|
||||
|
||||
See [set up admin
|
||||
authentication](../how-to/set-up-admin-authentication.md).
|
||||
|
||||
## Validation errors
|
||||
|
||||
`nxdns check`, `nxdns import` and the `nxdns run` that seeds a database from the
|
||||
file all print one `FAIL path: message` line per problem, and report every
|
||||
`nxdns check`, `nxdns import` and a `nxdns run --config` that reads the file
|
||||
all print one `FAIL path: message` line per problem, and report every
|
||||
problem rather than the first. A finding that is legal but almost certainly
|
||||
unintended is prefixed `WARN` instead: it does not change the exit code, and it
|
||||
is printed by all three even when nothing failed, so an accepted configuration
|
||||
@@ -461,7 +493,7 @@ upstream. Everything else keeps its default.
|
||||
|
||||
.cache = .{ .size = 10000, .negative_ttl_max = 3600 },
|
||||
|
||||
// Web UI on 8080. The password is hashed at import and never stored;
|
||||
// Web UI on 8080. The password is hashed and never stored as plaintext;
|
||||
// leave .password_hash out when setting .password (they are exclusive).
|
||||
.web = .{
|
||||
.enabled = true,
|
||||
|
||||
Reference in New Issue
Block a user