Skip to content

bootstrap: passwordless sudo for key-only platform users; groups once; inventory fixes - #19

Merged
tekn0ir merged 8 commits into
mainfrom
bootstrap-keyonly-nopasswd
Oct 16, 2026
Merged

tekn0ir merged 8 commits into
mainfrom
bootstrap-keyonly-nopasswd

Conversation

@tekn0ir

@tekn0ir tekn0ir commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What

  • Passwordless sudo for a key-only platform user. A device user without a password in the platform could not answer sudo's prompt, so Ansible become and tnctl could never act as root. The bootstrap now installs /etc/sudoers.d/90-teknoir-platform-user with #<uid> ALL=(ALL:ALL) NOPASSWD: ALL.
    • The file is mode 0440 and is validated with visudo -cf before it is moved into place.
    • The rule names the user by uid, so no user name can become a sudoers keyword. The file name has no ., which sudo's includedir would skip.
    • Drop-ins of earlier platform users are removed. With a password in the platform, the drop-in is removed and sudo asks for the password.
  • Login name validation. tnctl bootstrap now runs ValidateTemplateData before rendering. Username must be a lowercase login name (^[a-z_][a-z0-9_-]{0,31}$) and not root.
  • Group membership only once. Every run appended the user to users/sudo in /etc/group again; one staging device had it 12 times. The script now uses usermod -aG only for missing memberships, falling back to /etc/group without getent.
  • ansible-inventory:
    • Devices without login data or a transport are left out of hosts and groups, with one stderr line each.
    • New host var ansible_teknoir_login_user.
  • Docs: docs/commands/ansible.md marks the teknoir/ansible inventory plugin as deprecated and adds sections on become and on synchronize. synchronize needs use_ssh_args and verify_host; otherwise rsync skips the host key check, because the module puts StrictHostKeyChecking=no first.
  • Unattended runs (CREATE_USER=true) no longer warn that the user "has to be created", and the missing-extra-key message is correct now.

Review

An adversarial review found:

  • a sudoers keyword or a dotted name could slip through as the user name;
  • the drop-in file name could be one sudo skips;
  • drop-ins of renamed users were left stale;
  • there was no getent fallback;
  • devices without a transport vanished silently.

All are fixed in 041a614 and later. The stale-file cleanup uses find, because the scripts run with noglob.

Tests

  • go vet, go test ./... and pkg/templates tests pass, with goldens regenerated after merging main (fix(bootstrap): onboarding and re-runs with tnctl (keys, k3s token, pull secret) #17, fix(bootstrap): keep the legacy artifact-registry-secret credential #20).
  • Debian 13 container, under noglob:
    • a key-only user gets sudo, and the re-run is idempotent;
    • a stale 90-teknoir-teknoir is removed;
    • other users get no sudo;
    • a password removes the drop-in.
  • VM vm-rebootstrap-01 on staging, fresh bootstrap and re-runs:
    • k3s config, token and CA unchanged, and the group lines do not grow;
    • TOE on MQTT, and WireGuard came up zero-touch;
    • tnctl ansible -b gives root as teknoir without a password.
  • Staging devices rpi4-white-se, rtx2000-ada-128gb-se, orin-agx-32gb-se and rpi4-8gb-black-se were re-bootstrapped with teknoir/ansible playbooks/rebootstrap-device.yml (an earlier commit of this branch). tnctl ansible all -b -m command -a 'id -u' returned 0 on all of them.

🤖 Generated with Claude Code

tekn0ir and others added 5 commits October 16, 2026 18:38
…udoers drop-in)

A key-only platform user (no password in the platform) cannot answer sudo's
prompt, so Ansible become and other automation over SSH could never act as
root. setup_user now writes /etc/sudoers.d/90-teknoir-<user> with NOPASSWD
for exactly that user, mode 0440, checked with visudo -cf before it is moved
into place; with a password set the drop-in is removed again. Golden files
regenerated (UPDATE_GOLDEN=1 go test in pkg/templates).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…login_user host var

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… message

Every run appended the user to the users and sudo lines of /etc/group again
(rpi4-8gb-black-se had it 12 times). Use usermod -aG when the user is not yet
a member, with the old sed as a fallback. The message for a missing extra key
no longer claims password-only login: the device key authorizes logins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With CREATE_USER=true (or an OS build) the user is created without a prompt,
so the 'has to be created' warning was noise in every unattended run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tekn0ir and others added 3 commits October 16, 2026 19:20
…in name; inventory reports left-out devices

Review findings on the passwordless-sudo change:
- The rule is '#<uid> ALL=(ALL:ALL) NOPASSWD: ALL' in
  /etc/sudoers.d/90-teknoir-platform-user. A user name can no longer become a
  sudoers keyword (a platform user 'ALL' would have given every account root),
  and the file name has no '.', which sudo's includedir skips. Drop-ins of
  earlier platform users (90-teknoir-<name>) are removed.
- tnctl bootstrap now runs ValidateTemplateData before rendering, and the
  Username must be a lowercase login name (not root).
- Group membership falls back to /etc/group when getent is missing.
- Write and validation failures leave no drop-in and only warn.
- ansible-inventory prints one stderr line per device it leaves out.
- docs/commands/ansible.md: the plugin is deprecated; become; synchronize needs
  use_ssh_args and verify_host (else rsync skips the host key check).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ith noglob)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tekn0ir
tekn0ir merged commit 8c04051 into main Oct 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant