Say which machine each setting is about, and stop defaulting to root
Two reports from setting this up, and the second is a real defect rather than a documentation one. DEJIMA_GUARD_HOST READ AS THE HOST THE GUARD RUNS ON. It meant the one it watches - the wrong end of the only connection this program makes. A name that needs a comment to be read correctly is the wrong name, so it is devbox_host and devbox_user now, under a heading that says everything in that group describes the machine at the other end. AND devbox_user DEFAULTED TO ROOT, on a machine whose own runbook builds it with no root password and expects sudo from an ordinary account. The default was wrong for every devbox built the documented way. There is no default now: falling back to nothing would have ssh use the local username, which in a container is an account that does not exist over there, so it says which is missing instead. Every setting is documented twice over: a table in the README saying what goes in it, and etc/config.example with the reasoning attached. The compose example carries a comment per variable rather than a bare list, since that is the file somebody actually edits. Three tests worth having beyond the rename: that no setting is named so it could mean this machine, that a missing devbox_user is refused rather than guessed, and that the two SSH commands still match the strings dejima-devbox-hardener's dejima-guard-shell whitelists - one contract across two repositories, and the failure if it drifts is a collection that fails and gets reported as the devbox being unreachable. `bats test/` 26 passing, 3 new.
This commit is contained in:
@@ -120,6 +120,35 @@ change list uses `--unchanged-line-format`, which is GNU diffutils and not
|
||||
busybox. Alpine works once diffutils and coreutils are added back, at which
|
||||
point the smaller base has been patched towards this one a package at a time.
|
||||
|
||||
### What goes in each setting
|
||||
|
||||
Every one can come from the config file or from the environment as
|
||||
`DEJIMA_GUARD_` plus the key in upper case. The environment wins, which is
|
||||
what makes a compose file workable — but a token belongs in the file, since
|
||||
`docker inspect` shows the environment and so does anything reading `/proc`.
|
||||
|
||||
**The machine being watched.** Everything here describes the *devbox*, at the
|
||||
other end of the connection, not the machine the guard runs on.
|
||||
|
||||
| Setting | What goes in it |
|
||||
| --- | --- |
|
||||
| `devbox_host` | The devbox's hostname or address, as this machine reaches it |
|
||||
| `devbox_user` | The account **on the devbox** to log in as. Not root, and no default — the devbox runbook builds a machine with no root password, so the guard connects as an ordinary account and elevates for two commands |
|
||||
| `ssh_key` | The private key **on this machine**. Its own, not a general-purpose administrative one |
|
||||
| `ssh_known_hosts` | Where the devbox's host key is remembered. Must persist — in a container, on the state volume |
|
||||
| `ssh_timeout` | Seconds to wait. The line between a slow devbox and a gone one |
|
||||
|
||||
**Where alerts go.**
|
||||
|
||||
| Setting | What goes in it |
|
||||
| --- | --- |
|
||||
| `ntfy_url` | The topic to publish to. **Its own**, not the one dejima pushes to — that URL lives on the devbox, so root there could drown it or forge an all-clear |
|
||||
| `ntfy_token` | Write access to that topic and nothing else. The reason the config file is `0640` |
|
||||
| `heartbeat_url` | GET after a cycle that completed clean. An Uptime Kuma push monitor |
|
||||
| `alert_command` | An alternative to ntfy: gets the title and body on stdin |
|
||||
|
||||
`etc/config.example` carries the same reference with the reasoning attached.
|
||||
|
||||
### The happy path is ntfy and Uptime Kuma
|
||||
|
||||
Both self-hosted, and if you run dejima's remote access you already have the
|
||||
|
||||
+16
-4
@@ -77,10 +77,14 @@ check_url() { # check_url NAME VALUE
|
||||
|
||||
# --- reaching the devbox ----------------------------------------------------
|
||||
|
||||
# The two commands sent are matched EXACTLY by dejima-guard-shell, the forced
|
||||
# command on the devbox's end of this key. Change either string and the other
|
||||
# side refuses it - which arrives here as a failed collection, reported as the
|
||||
# devbox being unreachable. Both repositories, one contract.
|
||||
ssh_devbox() { # ssh_devbox COMMAND...
|
||||
local host user key known opts
|
||||
host=$(cfg host) ; [ -n "$host" ] || die "no host in $CONF"
|
||||
user=$(cfg user root)
|
||||
host=$(cfg devbox_host) ; [ -n "$host" ] || die "no devbox_host in $CONF"
|
||||
user=$(cfg devbox_user) ; [ -n "$user" ] || die "no devbox_user in $CONF"
|
||||
key=$(cfg ssh_key)
|
||||
known=$(cfg ssh_known_hosts)
|
||||
opts="-o BatchMode=yes -o ConnectTimeout=$(cfg ssh_timeout 10)"
|
||||
@@ -177,8 +181,16 @@ summarise() { # summarise DIFF_FILE
|
||||
# result and the cycle carried on as though the box had answered with nothing.
|
||||
require_config() {
|
||||
local h
|
||||
h=$(cfg host)
|
||||
[ -n "$h" ] || die "no host in $CONF - nothing to watch"
|
||||
h=$(cfg devbox_host)
|
||||
[ -n "$h" ] || die "no devbox_host in $CONF - nothing to watch"
|
||||
# No default for the account, deliberately.
|
||||
#
|
||||
# It used to default to root, on a machine whose own runbook builds it with
|
||||
# no root password and expects sudo from an ordinary account - so the
|
||||
# default was wrong for every devbox built the documented way. Defaulting
|
||||
# to nothing means ssh would use the local username instead, which in a
|
||||
# container is an account that does not exist over there. Say which.
|
||||
[ -n "$(cfg devbox_user)" ] || die "no devbox_user in $CONF - which account does the guard connect as?"
|
||||
# The placeholder the shipped example carries, refused so that example can
|
||||
# never become a live config by being copied and forgotten. dejima's own
|
||||
# nginx example takes the same approach for the same reason.
|
||||
|
||||
@@ -19,11 +19,47 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
environment:
|
||||
DEJIMA_GUARD_HOST: CHANGE-ME.example.invalid
|
||||
DEJIMA_GUARD_USER: root
|
||||
# --- the machine being WATCHED, not the one this runs on ------------
|
||||
#
|
||||
# The devbox's hostname or address, as this container reaches it.
|
||||
# Replace the placeholder: the guard refuses to start on CHANGE-ME.
|
||||
DEJIMA_GUARD_DEVBOX_HOST: CHANGE-ME.example.invalid
|
||||
|
||||
# The account ON THE DEVBOX to log in as. Not root, and there is no
|
||||
# default: the devbox runbook builds a machine with no root password
|
||||
# and expects sudo from an ordinary account. The guard connects as that
|
||||
# account and elevates for the two commands it runs.
|
||||
DEJIMA_GUARD_DEVBOX_USER: your-devbox-account
|
||||
|
||||
# The private key, inside this container, to connect with. Mounted as a
|
||||
# secret below rather than baked into the image.
|
||||
DEJIMA_GUARD_SSH_KEY: /run/secrets/guard_key
|
||||
|
||||
# Where the devbox's host key is remembered. On the state volume,
|
||||
# because a container filesystem is new every run - and without it,
|
||||
# every collection fails host-key verification and reports the devbox
|
||||
# unreachable. Already set in the image; here so it is visible.
|
||||
DEJIMA_GUARD_SSH_KNOWN_HOSTS: /var/lib/dejima-guard/known_hosts
|
||||
|
||||
# --- where alerts go -----------------------------------------------
|
||||
#
|
||||
# Its OWN ntfy topic, not the one dejima pushes to. That URL lives on
|
||||
# the devbox, so root there can publish to it: a compromised box could
|
||||
# drown the topic or forge an all-clear. The token for this one goes in
|
||||
# the mounted config, not here.
|
||||
DEJIMA_GUARD_NTFY_URL: https://ntfy.example.invalid/devbox-alerts
|
||||
|
||||
# --- the dead-man's switch ------------------------------------------
|
||||
#
|
||||
# GET after a cycle that completed clean - an Uptime Kuma push monitor
|
||||
# is the documented shape. Without it, a guard that dies looks exactly
|
||||
# like a guard with nothing to report.
|
||||
DEJIMA_GUARD_HEARTBEAT_URL: https://kuma.example.invalid/api/push/abcdef
|
||||
|
||||
# --- this container --------------------------------------------------
|
||||
#
|
||||
# Seconds between collections. Sets the dead-man's switch granularity
|
||||
# and how long an intruder has to notice the monitoring and scrub.
|
||||
DEJIMA_GUARD_INTERVAL: "900"
|
||||
# Anything secret goes in the config file rather than here. Everything
|
||||
# above is visible in `docker inspect` and to anything that can read
|
||||
|
||||
+71
-25
@@ -2,27 +2,64 @@
|
||||
#
|
||||
# Install at /etc/dejima-guard/config, mode 0640 root. It holds an ntfy token.
|
||||
#
|
||||
# Every setting here can also come from the environment, as DEJIMA_GUARD_ plus
|
||||
# the key in upper case: devbox_host becomes DEJIMA_GUARD_DEVBOX_HOST. The
|
||||
# environment wins. That is for containers, where a compose file is where a
|
||||
# deployment already lives - but a token belongs here rather than there, since
|
||||
# `docker inspect` shows the environment and anything reading /proc does too.
|
||||
#
|
||||
# Parsed, never sourced: key = value, and nothing here is executed. This file
|
||||
# names a host to SSH to and URLs to post to, and sourcing it would make all of
|
||||
# those arbitrary code from a file with no reason to be executable.
|
||||
# names a host to connect to and URLs to post to, and sourcing it would make
|
||||
# every one of those arbitrary code from a file with no reason to be
|
||||
# executable.
|
||||
|
||||
# --- the devbox this watches ------------------------------------------------
|
||||
host = devbox.example.invalid
|
||||
user = root
|
||||
# ===========================================================================
|
||||
# THE MACHINE BEING WATCHED
|
||||
# ===========================================================================
|
||||
#
|
||||
# Not the machine the guard runs on. Everything in this section describes the
|
||||
# devbox at the other end of the connection.
|
||||
|
||||
# The devbox's hostname or address, as this machine reaches it.
|
||||
devbox_host = devbox.example.invalid
|
||||
|
||||
# The account on the DEVBOX that the guard logs in as.
|
||||
#
|
||||
# NOT root, and there is no default. The devbox runbook builds a machine with
|
||||
# no root password and expects sudo from an ordinary account, so the guard
|
||||
# connects as that account and elevates for the two commands it runs - see
|
||||
# dejima-devbox-hardener, docs/building-a-devbox.md section 7, which has the
|
||||
# authorized_keys line and the sudoers rule together.
|
||||
devbox_user = brad
|
||||
|
||||
# The private key, on THIS machine, that the guard connects with.
|
||||
#
|
||||
# Its own key, not a general-purpose administrative one, and restricted at the
|
||||
# devbox end. In that account's authorized_keys:
|
||||
#
|
||||
# command="/usr/local/sbin/dejima-devbox-audit ${SSH_ORIGINAL_COMMAND##* }",\
|
||||
# no-port-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAA...
|
||||
#
|
||||
# What this program needs is two read-only subcommands. A key that can do only
|
||||
# those is a key worth less to whoever takes this machine - and this machine is
|
||||
# the one holding the evidence about the other one.
|
||||
# devbox end by a forced command. What this program needs is two read-only
|
||||
# subcommands; a key that can do only those is worth little to whoever
|
||||
# eventually takes this machine - and this machine holds the evidence about
|
||||
# the other one, which is a reason somebody might want it.
|
||||
ssh_key = /var/lib/dejima-guard/id_ed25519
|
||||
|
||||
# Where the devbox's host key is remembered.
|
||||
#
|
||||
# Host key checking is left at ssh's default on purpose: this is the one
|
||||
# connection whose whole point is noticing that something about that machine
|
||||
# changed. So this file has to persist, which in a container means the state
|
||||
# volume rather than a home directory that is new every run - without it,
|
||||
# BatchMode turns the first connection into a failure and every collection
|
||||
# reports the devbox unreachable.
|
||||
ssh_known_hosts = /var/lib/dejima-guard/known_hosts
|
||||
|
||||
# Seconds to wait for the connection. A devbox that is slow is not a devbox
|
||||
# that is gone, and this is the line between those two readings.
|
||||
ssh_timeout = 10
|
||||
|
||||
# --- where alerts go --------------------------------------------------------
|
||||
# ===========================================================================
|
||||
# WHERE ALERTS GO
|
||||
# ===========================================================================
|
||||
|
||||
# The ntfy topic to publish alerts to.
|
||||
#
|
||||
# A DIFFERENT TOPIC FROM THE ONE dejima PUSHES TO, and this is the part that
|
||||
# matters rather than a tidiness preference.
|
||||
@@ -31,31 +68,40 @@ ssh_timeout = 10
|
||||
# reads it. So a compromised devbox can publish to that topic - drowning it
|
||||
# until you mute it, or forging an all-clear that never came from here.
|
||||
#
|
||||
# Give this its own topic, and give the devbox no credential for it. On a
|
||||
# Give this its own topic and give the devbox no credential for it. On a
|
||||
# self-hosted ntfy with access control that is a token per topic. Deny the
|
||||
# devbox READ on this one too: an intruder who can read the alerts about
|
||||
# themselves learns exactly what the monitoring noticed and what it missed,
|
||||
# which is worse than being able to write.
|
||||
ntfy_url = https://ntfy.example.invalid/devbox-alerts
|
||||
|
||||
# A token with write access to that topic and nothing else. Optional on a
|
||||
# server without access control, and the reason this file is 0640.
|
||||
# ntfy_token = tk_...
|
||||
|
||||
# --- the dead-man's switch --------------------------------------------------
|
||||
# ===========================================================================
|
||||
# THE DEAD-MAN'S SWITCH
|
||||
# ===========================================================================
|
||||
|
||||
# A URL to GET after a cycle that completed and found nothing wrong.
|
||||
#
|
||||
# An Uptime Kuma push monitor. Kuma expects a ping on a cadence and alerts when
|
||||
# one does not arrive, which is exactly the shape wanted: this program pings
|
||||
# only on a cycle that completed and found nothing wrong, so silence from it is
|
||||
# itself the alarm.
|
||||
# An Uptime Kuma push monitor is the documented shape: it expects a ping on a
|
||||
# cadence and alerts when one does not arrive. That is exactly what is wanted
|
||||
# here, because this program is pinged only on a clean cycle - so silence from
|
||||
# it is itself the alarm.
|
||||
#
|
||||
# Without this, a watcher that dies is indistinguishable from a watcher with
|
||||
# Without this, a guard that dies is indistinguishable from a guard with
|
||||
# nothing to report. That is the one failure this design cannot catch about
|
||||
# itself, and it is why the line is here rather than optional-by-omission.
|
||||
# itself, which is why the line is here rather than left to be discovered.
|
||||
#
|
||||
# Anything speaking the same "GET this URL to say you are alive" shape works.
|
||||
heartbeat_url = https://kuma.example.invalid/api/push/abcdef
|
||||
|
||||
# --- if you use something else ----------------------------------------------
|
||||
# ===========================================================================
|
||||
# IF YOU ALERT THROUGH SOMETHING ELSE
|
||||
# ===========================================================================
|
||||
#
|
||||
# alert_command receives the title on the first line and the body on the second,
|
||||
# on stdin. Set it and ntfy_url is ignored.
|
||||
# Receives the title on the first line and the body on the second, on stdin.
|
||||
# Set it and ntfy_url is ignored.
|
||||
#
|
||||
# alert_command = /usr/local/bin/my-alerter
|
||||
|
||||
+40
-7
@@ -24,7 +24,8 @@ setup() {
|
||||
export DEJIMA_GUARD_STATE="$BATS_TEST_TMPDIR/lib"
|
||||
export DEJIMA_GUARD_CONF="$BATS_TEST_TMPDIR/config"
|
||||
cat > "$DEJIMA_GUARD_CONF" <<EOF
|
||||
host = devbox.example.invalid
|
||||
devbox_host = devbox.example.invalid
|
||||
devbox_user = tester
|
||||
ntfy_url = https://ntfy.example.invalid/alerts
|
||||
heartbeat_url = https://kuma.example.invalid/api/push/abc
|
||||
EOF
|
||||
@@ -201,8 +202,8 @@ FILE bbb /etc/evil-http-backdoor" 0
|
||||
|
||||
@test "the environment overrides the config file" {
|
||||
stub_ssh "FILE aaa /etc/one" 0
|
||||
printf 'host = wrong.example.invalid\n' > "$DEJIMA_GUARD_CONF"
|
||||
DEJIMA_GUARD_HOST=devbox.example.invalid run_watch check
|
||||
printf 'devbox_host = wrong.example.invalid\ndevbox_user = tester\n' > "$DEJIMA_GUARD_CONF"
|
||||
DEJIMA_GUARD_DEVBOX_HOST=devbox.example.invalid run_watch check
|
||||
[ "$status" -eq 0 ]
|
||||
[ -f "$DEJIMA_GUARD_STATE/baseline" ]
|
||||
}
|
||||
@@ -248,7 +249,8 @@ EOF
|
||||
# curl reads a leading -K/path as an option taking the request from a file.
|
||||
# dejima refuses these for the same reason and this follows it.
|
||||
stub_ssh "FILE aaa /etc/one" 0
|
||||
printf 'host = devbox.example.invalid\nntfy_url = ntfy.example.invalid/x\n' > "$DEJIMA_GUARD_CONF"
|
||||
printf 'devbox_host = devbox.example.invalid
|
||||
devbox_user = tester\nntfy_url = ntfy.example.invalid/x\n' > "$DEJIMA_GUARD_CONF"
|
||||
run_watch check
|
||||
[ -f "$DEJIMA_GUARD_STATE/baseline" ] || true
|
||||
stub_ssh "FILE zzz /etc/one" 0
|
||||
@@ -260,7 +262,7 @@ EOF
|
||||
# So the shipped compose example cannot become a live config by being
|
||||
# copied and forgotten. It would otherwise start, fail to resolve, and
|
||||
# report a devbox unreachable that was never configured.
|
||||
printf 'host = CHANGE-ME.example.invalid\n' > "$DEJIMA_GUARD_CONF"
|
||||
printf 'devbox_host = CHANGE-ME.example.invalid\ndevbox_user = tester\n' > "$DEJIMA_GUARD_CONF"
|
||||
stub_ssh "FILE aaa /etc/one" 0
|
||||
run_watch check
|
||||
[ "$status" -ne 0 ]
|
||||
@@ -270,7 +272,7 @@ EOF
|
||||
@test "the shipped compose example carries that placeholder" {
|
||||
# If it ever carries a plausible-looking host instead, the refusal above
|
||||
# stops protecting anything and this is the test that says so.
|
||||
grep -q 'DEJIMA_GUARD_HOST: CHANGE-ME' "$REPO/docker-compose.example.yml"
|
||||
grep -q 'DEJIMA_GUARD_DEVBOX_HOST: CHANGE-ME' "$REPO/docker-compose.example.yml"
|
||||
}
|
||||
|
||||
@test "there is no committed docker-compose.yml to start by accident" {
|
||||
@@ -278,10 +280,41 @@ EOF
|
||||
[ ! -e "$REPO/docker-compose.yml" ]
|
||||
}
|
||||
|
||||
@test "no devbox_user is refused rather than defaulted to root" {
|
||||
# It used to default to root, on a machine whose own runbook builds it
|
||||
# with no root password and expects sudo from an ordinary account - so the
|
||||
# default was wrong for every devbox built the documented way. Defaulting
|
||||
# to nothing is not better: ssh would use the local username, which in a
|
||||
# container is an account that does not exist over there.
|
||||
printf 'devbox_host = devbox.example.invalid\n' > "$DEJIMA_GUARD_CONF"
|
||||
stub_ssh "FILE aaa /etc/one" 0
|
||||
run_watch check
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *devbox_user* ]]
|
||||
}
|
||||
|
||||
@test "nothing in the config is named so that it could mean this machine" {
|
||||
# DEJIMA_GUARD_HOST read as the host the guard runs on rather than the one
|
||||
# it watches, which is the wrong end of the only connection this makes. A
|
||||
# name that needs a comment to be read correctly is the wrong name.
|
||||
! grep -qE '^\s*(host|user)\s*=' "$REPO/etc/config.example" \
|
||||
|| { echo "config.example still has a bare host/user key"; false; }
|
||||
! grep -qE 'DEJIMA_GUARD_(HOST|USER):' "$REPO/docker-compose.example.yml" \
|
||||
|| { echo "the compose example still has an ambiguous variable"; false; }
|
||||
}
|
||||
|
||||
@test "the two commands sent match what the devbox forced command permits" {
|
||||
# dejima-devbox-hardener's dejima-guard-shell whitelists these two strings
|
||||
# exactly. Change one here and the other side refuses it, which arrives
|
||||
# back as a failed collection reported as the devbox being unreachable.
|
||||
grep -q 'ssh_devbox dejima-devbox-audit drift --state' "$REPO/bin/dejima-guard"
|
||||
grep -q 'ssh_devbox dejima-devbox-audit reach' "$REPO/bin/dejima-guard"
|
||||
}
|
||||
|
||||
@test "no host configured is refused, not silently skipped" {
|
||||
printf 'ntfy_url = https://ntfy.example.invalid/x\n' > "$DEJIMA_GUARD_CONF"
|
||||
stub_ssh "FILE aaa /etc/one" 0
|
||||
run_watch check
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"no host"* ]]
|
||||
[[ "$output" == *"no devbox_host"* ]]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user