- Go 64.1%
- Shell 25.3%
- Nix 8.6%
- Makefile 2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
build / check (push) Successful in 1m7s
The tag build for v0.1.0 failed and nobody noticed. The publish step reads REGISTRY_USER and REGISTRY_TOKEN, this repository has neither -- secrets are per-repo in this forge, and the workflow was modelled on homelab's and torch's, which do have them -- so curl was handed an empty user, answered 401, and the run died naming nothing. The tag exists, the generic registry holds nothing under oath-enroll, and homelab#100 records that a citation of v0.1.0 now resolves to an artefact. It does not. Checking the variables up front turns that into a first-line failure that names the missing secret and the scope it needs. It does not fix the tag: the secrets still have to be set, and a release has to be re-cut afterwards, because CI checks out the tag ref and can only publish what that ref can build. Verified by reading the run and the registry -- run 156 (ref v0.1.0) failed where run 155 (main) passed, which is the publish step and nothing else, and both the releases list and the generic package list are empty. The upload path itself is unproven either way: an anonymous probe of the registry endpoint is refused with the same 401, so the URL is still only as good as the two working examples it was copied from. |
||
| .forgejo/workflows | ||
| oath-enroll | ||
| sshd | ||
| .gitignore | ||
| flake.lock | ||
| flake.nix | ||
| install-oath-sudo.sh | ||
| package.nix | ||
| README.md | ||
oath-sudo
Sudo authenticated by TOTP instead of a password, on kanidm-backed hosts.
The OS image knows nothing about individual people. kanidm provides identity — NSS, ssh keys, group membership — and this provides the sudo factor. Users provision themselves on first login.
ssh key (kanidm) -> login
TOTP (local seed) -> sudo
admin + password -> break-glass, when both of the above are broken
The seed is deliberately local to the host, never stored in kanidm. That is what keeps the two factors independent: sudo survives a kanidm outage, and a kanidm compromise does not hand over root.
Documentation
Lives in the homelab wiki at wiki/sudo/ — auth model, decisions,
runbook, traps, verification and the fleet rollout plan. Start at
wiki/sudo/README.md.
This repository holds only the code.
Layout
install-oath-sudo.sh host prep, run once per node as root. Enrols nobody.
oath-enroll/ static Go binary: generates one user's seed, prints it once
sshd/ drop-in restricting password auth to the break-glass account
install-oath-sudo.sh is pure declarative host state, which is why it maps cleanly onto
Ignition. oath-enroll stays imperative because it must generate a secret and show it
exactly once.
pam_oath.so is the only package a host needs. The binary does its own TOTP and QR
rendering, so oathtool and qrencode are not required — though oathtool remains
handy for debugging, since it can generate a code from the hex seed in users.oath.
Quick start
make -C oath-enroll all
scp -r . <host>:oath-sudo/
ssh -t <host> 'sudo ./oath-sudo/install-oath-sudo.sh --admin-group <group>'
--admin-group may be repeated, or given a comma/space-separated list.
Membership in any listed group permits self-enrolment — they are
alternatives, so one fleet-wide group is the simple case and a node can still
be rolled with different auth without a redesign.
Configuration precedence
oath-enroll and the login notice both read /etc/oath-enroll.conf, then
every /etc/oath-enroll.conf.d/*.conf in lexical order. Later files replace
earlier keys rather than merging them, so a node declaring its own
ADMIN_GROUPS gets exactly those.
/etc/oath-enroll.conf ADMIN_GROUPS="fleet_admin" # image default
/etc/oath-enroll.conf.d/50-x.conf ADMIN_GROUPS="x_admin" # this node only
The drop-in directory exists for image-based hosts. bootc/ostree
three-way-merges /etc against the image on every upgrade, so having the
image and the per-node layer write the same path is the case you least want
to reason about at upgrade time. Separate files never contend.
ADMIN_GROUPS is canonical; the singular ADMIN_GROUP is still honoured as a
fallback, so a host installed before this change keeps working with its /etc
untouched.
⚠
--admin-groupvalidates that each group resolves (getent group), and that check cannot run at image build time — there is nokanidm-unixdin a build container. Baking these files therefore converts a loud install-time failure into a silent "nobody can enrol", discovered at first login on a real node. An image-based host wants a boot-time equivalent.
Then, as yourself, in your own terminal — the seed prints once:
sudo oath-enroll
That first run works because the sudoers rule is NOPASSWD, which skips the PAM auth
stack entirely. Bootstrap therefore succeeds even on a host where OTP is already
mandatory.
Full detail in the runbook.
Nix
The package lives here, next to the code it builds, so a change to go.mod and
a change to the derivation move in one commit.
nix build .#oath-enroll # or: nix run .#oath-enroll -- --user alex
Consumers take it as a flake input:
inputs.oath-sudo.url = "git+ssh://git@git.piqueras.me:443/alex/oath-sudo?ref=v0.1.0";
# then: oath-sudo.packages.${system}.oath-enroll
# or: nixpkgs.overlays = [ oath-sudo.overlays.default ];
⚠ The repository is private, so the flake input is git+ssh — fetched by
the nix CLI outside the build sandbox, where ssh keys apply. A build host needs
a key that can read it.
⚠ CGO_ENABLED=0 means id(1) must be on PATH. The Makefile explains why
the binary is static; the consequence is that os/user cannot see NSS, so
group membership is resolved by shelling out. package.nix wraps the binary to
guarantee coreutils rather than trusting the caller's environment — this tool
decides who may obtain a sudo token, and an empty group list because a helper
was missing is not a failure to leave to chance.