milestone 20: declarative configuration for iac
This commit is contained in:
@@ -11,7 +11,7 @@ data directory is `/var/lib/nxdns` and the web port is 8080.
|
||||
|
||||
## 1. Set the password
|
||||
|
||||
Put it in the seed configuration file, under `web`:
|
||||
Put it in the configuration file, under `web`:
|
||||
|
||||
```zon
|
||||
.{
|
||||
@@ -21,17 +21,60 @@ Put it in the seed configuration file, under `web`:
|
||||
}
|
||||
```
|
||||
|
||||
At import time the plaintext is hashed with argon2id into `web.password_hash`
|
||||
and discarded. It becomes no database row and appears in no log line. Setting
|
||||
both `password` and `password_hash` in one file is refused:
|
||||
The plaintext is hashed with argon2id into `web.password_hash` and discarded. It
|
||||
becomes no database row and appears in no log line. Setting both `password` and
|
||||
`password_hash` in one file is refused:
|
||||
|
||||
```
|
||||
web.password: password and password_hash are both set; ambiguity in a security setting is refused
|
||||
import failed: PasswordAndHashBothSet
|
||||
```
|
||||
|
||||
The seed file is read only while the database is empty. On a server that
|
||||
already has a database, use step 4 or step 5 instead.
|
||||
Applying that file — with `nxdns import`, or with a `nxdns run --config` start —
|
||||
announces the change:
|
||||
|
||||
```
|
||||
web authentication is now enabled
|
||||
```
|
||||
|
||||
### Absent, empty, and set are three different things
|
||||
|
||||
The two fields are optional, and the difference between leaving one out and
|
||||
setting it to `""` is the difference between keeping your password and removing
|
||||
it:
|
||||
|
||||
| The file says | Effect on the stored password |
|
||||
| --- | --- |
|
||||
| Neither field | Nothing. It stays exactly as it was. |
|
||||
| `.password = "…"` | Installs that password. Unchanged plaintext keeps the existing hash rather than re-hashing it. |
|
||||
| `.password = ""` | Refused. |
|
||||
| `.password_hash = "$argon2id$…"` | Installs that hash, for example from an export. |
|
||||
| `.password_hash = ""` | **Removes the password.** Authentication is off. |
|
||||
|
||||
Absence has to mean "keep", because the alternative is a foot-gun with a live
|
||||
round in it. An export carries the full PHC string, which is long and ugly, and
|
||||
sooner or later someone trims that line out of a file before committing it —
|
||||
meaning "leave the password alone". If absence meant "no password", that edit
|
||||
would open the admin interface to the whole LAN without a word.
|
||||
|
||||
So removing the password takes the explicit empty string:
|
||||
|
||||
```
|
||||
web authentication is now disabled
|
||||
```
|
||||
|
||||
And an empty plaintext is refused outright, because hashing the empty string
|
||||
would switch authentication *on* while making every login impossible — the login
|
||||
handler rejects empty passwords:
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
Which of steps 4 and 5 applies to your server depends on its authority. Under
|
||||
`nxdns run --config FILE` the file is the password: edit it and restart, and the
|
||||
API refuses the change with a 403. Under bare `nxdns run` the database holds it,
|
||||
and step 4 or step 5 is how it moves.
|
||||
|
||||
## 2. Log in
|
||||
|
||||
@@ -63,7 +106,7 @@ The session token comes back in a `Set-Cookie` header, not in the body. In the
|
||||
jar it looks like this (value redacted here):
|
||||
|
||||
```
|
||||
#HttpOnly_127.0.0.1 FALSE / FALSE 1785770178 nxdns_session <redacted>
|
||||
#HttpOnly_127.0.0.1 FALSE / FALSE 1786559938 nxdns_session <redacted>
|
||||
```
|
||||
|
||||
The cookie is named `nxdns_session` and carries `HttpOnly; SameSite=Lax;
|
||||
@@ -123,6 +166,9 @@ already is.
|
||||
|
||||
## 4. Change the password on a running server
|
||||
|
||||
This is a database-mode procedure. In file mode `PUT /api/settings` answers 403
|
||||
naming the file; edit `web.password` there and restart instead.
|
||||
|
||||
Send the new one to `PUT /api/settings` as `web.password`. The response is the
|
||||
full settings document; `password` is write-only and `password_hash` is neither
|
||||
readable nor directly writable, so neither value comes back.
|
||||
@@ -161,7 +207,7 @@ Log back in with the new password. That is the whole rotation.
|
||||
|
||||
If you have lost the password, the admin interface cannot help — go through the
|
||||
database instead. Export, edit, import. `nxdns export` always writes
|
||||
`.password = ""` and carries the hash, so an exported file re-imports without
|
||||
`.password = null` and carries the hash, so an exported file re-imports without
|
||||
anyone knowing the password. To install a new one, put it in `.password` and
|
||||
clear `.password_hash`:
|
||||
|
||||
@@ -169,11 +215,22 @@ clear `.password_hash`:
|
||||
nxdns export --data-dir /tmp/nxdns-lab/data --out /tmp/nxdns-lab/rekeyed.zon
|
||||
```
|
||||
|
||||
Edit the `web` section of `/tmp/nxdns-lab/rekeyed.zon` so it reads:
|
||||
Edit the `web` section of `/tmp/nxdns-lab/rekeyed.zon`: set `.password` to the
|
||||
new value and **delete the `.password_hash` line entirely**, so the `web` block
|
||||
carries one password field and not two:
|
||||
|
||||
```zon
|
||||
.password = "offline-password",
|
||||
.password_hash = "",
|
||||
```
|
||||
|
||||
Deleting the line is the part to get right. Setting `.password_hash = ""`
|
||||
alongside a plaintext password does not clear the way for it — an empty string
|
||||
is a present value meaning "no password", so the file then states two
|
||||
contradictory things and is refused:
|
||||
|
||||
```
|
||||
FAIL web.password: password and password_hash are both set; ambiguity in a security setting is refused
|
||||
import failed: PasswordAndHashBothSet
|
||||
```
|
||||
|
||||
Stop the server before importing. `import` rewrites the stored hash underneath a
|
||||
@@ -184,14 +241,17 @@ terminal stops it, and it goes back up with the same command:
|
||||
|
||||
```sh
|
||||
# Ctrl-C the `nxdns run` terminal, or `kill` its pid from another shell
|
||||
nxdns import /tmp/nxdns-lab/rekeyed.zon --force --data-dir /tmp/nxdns-lab/data
|
||||
nxdns run --data-dir /tmp/nxdns-lab/data --config /tmp/nxdns-lab/etc/config.zon
|
||||
nxdns import /tmp/nxdns-lab/rekeyed.zon --data-dir /tmp/nxdns-lab/data
|
||||
nxdns run --data-dir /tmp/nxdns-lab/data
|
||||
```
|
||||
|
||||
```
|
||||
imported /tmp/nxdns-lab/rekeyed.zon
|
||||
```
|
||||
|
||||
No flag is needed: replacing a password edits a settings value and deletes no
|
||||
rows.
|
||||
|
||||
On a real install the stop and start are `systemctl stop nxdns` and
|
||||
`systemctl start nxdns` around the same `import` — **not verified on this
|
||||
host**, which has no installed nxdns systemd unit (`systemctl status nxdns`
|
||||
@@ -202,7 +262,7 @@ Once it is back up the old password is refused and the new one works:
|
||||
|
||||
```sh
|
||||
curl -sS -X POST http://127.0.0.1:8451/api/auth/login \
|
||||
-H 'content-type: application/json' -d '{"password":"a-new-password"}' \
|
||||
-H 'content-type: application/json' -d '{"password":"lab-password"}' \
|
||||
-w ' (old password, http %{http_code})\n'
|
||||
curl -sS -c /tmp/nxdns-lab/c5.txt -X POST http://127.0.0.1:8451/api/auth/login \
|
||||
-H 'content-type: application/json' -d '{"password":"offline-password"}' \
|
||||
@@ -217,19 +277,19 @@ curl -sS -b /tmp/nxdns-lab/c5.txt -o /dev/null -w 'stats: %{http_code}\n' \
|
||||
stats: 200
|
||||
```
|
||||
|
||||
The next export shows the new hash and an empty `password` again:
|
||||
The next export shows the new hash and a null `password` again:
|
||||
|
||||
```sh
|
||||
nxdns export --data-dir /tmp/nxdns-lab/data | grep password
|
||||
```
|
||||
|
||||
```
|
||||
.password = "",
|
||||
.password_hash = "$argon2id$v=19$m=19456,t=2,p=1$kvlRj1tdGul3MlfbvzLncLKWirNpJRJ3howFA9/ysgg$7elW7PPQ3WXHwI4YOmOpZ/1KNEQo7ZDLRJhnYOPMjqw",
|
||||
.password = null,
|
||||
.password_hash = "$argon2id$v=19$m=19456,t=2,p=1$xqzK66LgiWGvyCmCl6ZRa3GHH0nS5qZnRgVfWmeGadc$1mafhflKFIg3vcHjJaDMAXGQiOjtym2UADZsPW1xkfw",
|
||||
```
|
||||
|
||||
`--force` is required because the database already holds configuration. See
|
||||
[back up and restore](back-up-and-restore.md).
|
||||
See [back up and restore](back-up-and-restore.md) for when `import` does need
|
||||
`--allow-delete`.
|
||||
|
||||
## What happens with no password set
|
||||
|
||||
@@ -279,6 +339,11 @@ interface at the very least, and preferably set a password.
|
||||
[configuration reference](../reference/configuration.md); the routes are in
|
||||
the [API reference](../reference/api.md).
|
||||
|
||||
Every command on this page was executed on this host as written, except the
|
||||
`systemctl` stop and start named in step 5 and marked **not verified on this
|
||||
host** there.
|
||||
Every command on this page was executed on this host as written, against the
|
||||
lab described at the top, except the `systemctl` stop and start named in step 5
|
||||
and marked **not verified on this host** there. That includes the whole of
|
||||
steps 2 to 5, re-run for this revision: the login, logout and rate-limit
|
||||
transcripts reproduced exactly as printed, the both-set refusal in step 5 was
|
||||
reproduced by leaving `.password_hash = ""` in the file, and the rekey then
|
||||
succeeded once that line was deleted. The cookie jar's expiry timestamp is the
|
||||
one that run produced and will differ on yours.
|
||||
|
||||
Reference in New Issue
Block a user