Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ jobs:
# the overlay tests hand keel's key and file to (tests/wgtools.py);
# in CI they fail rather than skip without it.
apt-packages: bats shellcheck gnupg gpgv wireguard-tools
# keel mesh end to end (decision 0048) in a Debian trixie system
# container, the release keel ships on: three network namespaces,
# the real wg-quick and TLS, as root in the container
# (tests/test_mesh_netns.py). The check is "mesh / trixie".
mesh:
uses: keel-linux/.github/.github/workflows/lxc-trixie.yml@main
with:
timeout: 20
run: |
apt-get update -qq
apt-get install -y -qq --no-install-recommends python3-pytest \
python3-yaml wireguard-tools iproute2 iputils-ping openssl
PYTHONPATH=. python3 -m pytest -q tests/test_mesh_netns.py \
tests/test_mesh_channel.py
# A change that ships must come with a changelog entry, or it cannot be
# installed and never reaches an appliance. The rule and its tests are
# bin/require-changelog of the .github repository. The check is
Expand Down
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@ in [docs/apply.md](docs/apply.md).
| `keel network revert` | Put back the interfaces file a pending network change replaced; what the revert timer runs. `--boot` restores the file only, for the boot unit |
| `keel network wireguard key` | Print this node's WireGuard public key, making the key pair first when there is none (root, live system only); the private key is never printed (decision 0020, docs/spec.md) |
| `keel network wireguard suggest-address` | Print a random unique local IPv6 address with its /64, for the first node of an overlay |
| `keel mesh invite` | Print the `keel mesh join keel1:<token>` line a new node runs to join this node's WireGuard mesh, valid one hour and once, and reserve the new node's overlay address (decision 0048, [docs/mesh.md](docs/mesh.md)) |
| `keel mesh join` | With `--dry-run`, check a `keel1:` token and print the address and the peer it makes in this node's spec, applying nothing; the join itself comes later ([docs/mesh.md](docs/mesh.md)) |
| `keel mesh create` | On the first node: make the WireGuard mesh, a random unique local /64 with this node at `::1`, converged and confirmed by itself after the route check (decision 0048, [docs/mesh.md](docs/mesh.md)) |
| `keel mesh invite` | Print the `keel mesh join - <<< keel1:<token>` line a new node runs to join this node's WireGuard mesh, valid one hour and once; reserve the new node's overlay address and start the invite's root helper, `keel-mesh-invite@<id>`, and its unprivileged listener, `keel-mesh-listen@<id>` ([docs/mesh.md](docs/mesh.md)) |
| `keel mesh join` | Join the mesh with that token: the request to the inviter's port, pinned and authenticated, this node's spec written and applied, and both windows confirmed over the overlay; prints `keel mesh accept keel1a:...` for the inviter when its port cannot be reached. With `--dry-run`, print the change and apply nothing ([docs/mesh.md](docs/mesh.md)) |
| `keel mesh accept` | On the inviter, the line a join printed when it could not reach the port ([docs/mesh.md](docs/mesh.md)) |
| `keel mesh status` | This node's peers, their last handshakes, and the pending invites; never a secret ([docs/mesh.md](docs/mesh.md)) |
| `keel notify` | What monit runs when a check of the `monitor` section fails, lasts or recovers: sends one message, saying what happened and what to do, to every channel the section declares (email, Telegram, ntfy, a webhook). It reads the channels from `/etc/keel/monitor.json`, which `apply --system` writes as root, never from the spec, reads the tokens from their secret files and prints no URL (decision 0021, docs/apply.md) |
| `keel inspect` | Write a spec from the running machine, or from an offline root, and report every field with its source or why it was not inferred; secrets are never read (brief sections 5.2 and 7) |
| `keel diff` | Report drift between the spec and the running machine, or an offline root, field by field, through the same collector `inspect` uses; secrets are never compared and their files need not exist, nothing is written (brief section 5.2) |
Expand Down Expand Up @@ -140,16 +143,16 @@ Defined in one place, `keel/exits.py`, and reproduced here.
| 12 | `ASSEMBLE_FAILED` | The rootfs is not empty or cannot be created, or `tar` or `zstd` failed while extracting or packing |
| 13 | `INSPECT_INCOMPLETE` | `inspect` wrote the spec, but a required field (hostname, fqdn, interfaces, alerts, updates) could not be inferred; or `diff` found no drift, but a declared field could not be observed. The report says which and why |
| 14 | `DRIFT_FOUND` | `diff` found at least one declared field whose observed value differs. Drift wins over unobserved fields, so a report with both exits 14 |
| 15 | `APPLY_NEEDS_ROOT` | The system phase (`--system`, `--system-only`) on the live system was run by a user other than root; nothing was written, not even the conf. Also `keel network wireguard key` making a key, and `keel mesh invite`, on the live system |
| 15 | `APPLY_NEEDS_ROOT` | The system phase (`--system`, `--system-only`) on the live system was run by a user other than root; nothing was written, not even the conf. Also `keel network wireguard key` making a key, and `keel mesh create`, `invite`, `join`, `accept` and `serve`, on the live system |
| 16 | `APPLY_FAILED` | The system phase could not make at least one change; the output names it. The conf was written, where the run writes one, and every other change was made, so the run can be repeated |
| 17 | `CHANNEL_INVALID` | A channel pointer, or the record of the one this instance follows, does not parse or fails validation. Also a pointer signed in the future, one claiming more than 30 days of life, and a revision's archived pointer naming another revision |
| 18 | `CHANNEL_UNVERIFIED` | A channel pointer is not signed by a key that may move a channel, or no keyring was given to check it against. A key that is revoked or expired is refused here: gpgv exits 0 for both and still prints `VALIDSIG`, so `GOODSIG` is what is required |
| 19 | `CHANNEL_EXPIRED` | A channel pointer is past its expiry. A mirror that is stale, broken or hostile holds an appliance on an old release by not updating, so this is an error and never a warning |
| 20 | `CHANNEL_ROLLBACK` | A channel pointer names an earlier revision than the one this instance is on; `--allow-rollback`, or naming the release and revision, is how going back is asked for |
| 21 | `NETWORK_NOT_CONFIRMED` | `keel network confirm` refused: no change is waiting, or it was not run from a new session over the new configuration, or a console (docs/apply.md) |
| 21 | `NETWORK_NOT_CONFIRMED` | `keel network confirm` refused: no change is waiting, or it was not run from a new session over the new configuration, or a console (docs/apply.md). Also `keel mesh create`, `join` and `accept` whose overlay change was not confirmed: it reverts by itself when its window ends |
| 22 | `NOTIFY_FAILED` | `keel notify` reached no channel: `/etc/keel/monitor.json` is missing or refused, declares none, or every one failed; the message then went to syslog (user.crit) and root's mailbox. One channel that takes the message is success; the others' failures are printed |
| 23 | `MESH_TOKEN_INVALID` | `keel mesh join` could not use the token: not a `keel1:` token, mistyped or truncated (its checksum), of a later format, inconsistent, or expired. The message never holds the token or its secret |
| 24 | `MESH_REFUSED` | `keel mesh` refused: no overlay to invite into, no key yet, no endpoint, no free address in the prefix; or this node is in another mesh, at another address of it, or the change would make a spec validation refuses |
| 23 | `MESH_TOKEN_INVALID` | `keel mesh join` could not use the token: not a `keel1:` token, mistyped or truncated (its checksum), of a later format, inconsistent, or expired; or `keel mesh accept` could not read its `keel1a:` line. The message never holds the token or its secret |
| 24 | `MESH_REFUSED` | `keel mesh` refused: no overlay to invite into, no key yet, no endpoint, no free address in the prefix, another invite pending on the port; this node is in another mesh, at another address of it, or the change would make a spec validation refuses; a network change already waits in its window; the inviter refused the join (used, expired, bad HMAC), or the certificate is not the pinned one; no route to the inviter, or neither node can reach the other |

Two rules that callers depend on:

Expand Down
30 changes: 30 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
keel (0.17.0) unstable; urgency=medium

* The second part of handbook decision 0048, joining the mesh with one
command. keel mesh create makes the mesh on its first node, a random
unique local /64 with the node at ::1, and confirms it itself once
the route check finds the gateways and the operator's SSH client
still on the uplink (0048 amending 0018). keel mesh invite starts
a root helper as the transient unit keel-mesh-invite@<id> and, from
it, the unprivileged listener keel-mesh-listen@<id> (DynamicUser, no
capability, sandboxed), HTTPS with the invite's own certificate on
TCP 51820 by default (--port), its TLS key handed over as a memfd on
a unix socket in its own runtime directory, the peer checked against
the unit's MainPID; the HMAC key never leaves the root side, and a
join's confirmation rests on a WireGuard handshake from the new
peer's key. The port is opened in keel's
firewall only where keel-firewall is enabled, through a new named
set of the derived ruleset. The printed line feeds the token on
standard input (keel mesh join - <<< keel1:...). keel mesh join
joins: the request pinned to the token's fingerprint and signed with
an HMAC of the invite's key, checked in constant time on the root
side, once, without replays; each node writes the other
into its own spec and applies it under the window, and the join's
own session over the overlay confirms both windows. Tested end to
end over 250 ms links with 2% loss. When the port cannot be reached, join
applies its side and prints keel mesh accept keel1a:<answer> for the
inviter. keel mesh status shows the peers, their handshakes and the
pending invites, never a secret. docs/mesh.md.

-- Marcos Mendez <mendez.foto@gmail.com> Sat, 03 Oct 2026 23:00:00 +0000

keel (0.16.0) unstable; urgency=medium

* keel mesh invite and keel mesh join --dry-run, the first part of
Expand Down
15 changes: 14 additions & 1 deletion docs/apply.md
Original file line number Diff line number Diff line change
Expand Up @@ -1118,6 +1118,15 @@ drop, and these rules:
- every `mesh` port of an enabled overlay, on the WireGuard interface
only, and none (the step says which) while the spec declares no
overlay;
- with an overlay, the TCP ports of the pending mesh invites: a named
set, `mesh_invites`, of ports with a timeout, and one rule that
accepts TCP to a port in it. `keel mesh invite` adds its port for the
time left to its expiry and the listener's end removes it, so the
port is open exactly while an invite is pending ([docs/mesh.md](mesh.md),
decision 0048). Adding an element is not a ruleset change, so it
neither waits on nor disturbs a change in its window. The set is
empty while no invite is pending, and a reload of the table empties
it;
- on each of the machine's guest bridges, DNS on 53 and DHCP on 67 and
547: what lxc-net's or libvirt's dnsmasq serves the guests there,
which a drop policy would otherwise cut, so every container lost its
Expand Down Expand Up @@ -1373,6 +1382,8 @@ used survived, so:
| SSH, over the overlay | the session started after the change, came from another machine, and arrived at an address the overlay declares (`address`, `ipv4_address`). confirm says the overlay was tested |
| SSH, over the uplink | the session started after the change, came from another machine, and arrived at an address another interface of this machine holds now. confirm says the overlay itself was not tested, and at which address a peer would test it |
| A console, a process attached from a container's host | always, as for the uplink |
| The mesh session of a join (decision 0048), for the overlay change that join made and no other | on the inviter, a WireGuard handshake from the new peer's key since the join (`wg show <if> latest-handshakes`) and the join's confirmation request signed with the invite's HMAC key, both checked by the root side; on the new node, the inviter's signed answer over the tunnel ([docs/mesh.md](mesh.md)) |
| `keel mesh create`, for the change that created a mesh with no peer and no other | always, after the route check: an overlay with no peer routes its own private prefix only, and no peer exists to confirm it (decision 0048, second round, point 1) |
| Anything else, a session older than the change, a session from this machine to itself, or one arriving at an address the overlay does not declare | refused (exit 21) |

Requiring the overlay alone would make pairing impossible: the first
Expand All @@ -1385,7 +1396,9 @@ first, with `ip route get`: to each gateway `network.interfaces`
declares and each gateway of a default route in place now (`ip -6` and
`ip -4 route show default`: what DHCP, SLAAC or a container's host
configured, and what `--skip-uplink` left), IPv6 first, and, for an SSH
session that came over the uplink, back to its client. When one of them
session that came over the uplink, back to its client. `keel mesh
create` and `keel mesh join` also ask the route back to the client of
the SSH session they run in. When one of them
leaves through the overlay's interface, the change routes the uplink's
traffic into the overlay, and confirm refuses (exit 21) and leaves the
change to revert when its window ends, or at once with `keel network
Expand Down
Loading
Loading