Skip to content

feat: CoreOS-compatible login hardening and core operator key provisioning - #80

Open
castrojo wants to merge 11 commits into
mainfrom
feat/coreos-compatible-hardened-login
Open

feat: CoreOS-compatible login hardening and core operator key provisioning#80
castrojo wants to merge 11 commits into
mainfrom
feat/coreos-compatible-hardened-login

Conversation

@castrojo

@castrojo castrojo commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements CoreOS-compatible login hardening and key-only access for the core operator account, locking the shared root account and wiring persistent SSH keys and systemd presets.

Changes

  • SSH access enabled by default (key-only, fail-closed): sshd is enabled in the image for the core operator account. To prevent unauthorized or headless exposure, sshd.service is strictly gated by bluefin-core-access.service (Requires= + After=), which executes test -s /var/home/core/.ssh/authorized_keys. sshd will fail-closed and refuse to start unless an authorized SSH key has been provisioned via tmpfiles.extra. Root login and password authentication are completely disabled (PermitRootLogin no, PasswordAuthentication no).
  • Add core operator user with passwordless wheel sudo privileges.
  • Configure persistent host keys under /var/lib/ssh generated once via bluefin-ssh-host-keys.service.
  • Ensure root account is locked by default via systemd-sysusers.
  • Package systemd presets in OS DDI (os-systemd-presets.bst) to ensure networkd, persistent /var mount, and k0s first-boot services run reliably.
  • Add systemd drop-in 10-bluefin-access.conf preserving /usr/sbin/sshd -t config validation alongside persistent host key checks.
  • Update documentation and test contracts in tests/unit/test_core_login_contract.py and test_vm_dashboard_contract.py.
  • Update install-vm health probe in Justfile to test HTTPS.

Verification

  • pytest tests/unit (210 passed, 1 xfailed in 0.52s)
  • bats tests/unit (41 passed, 0 failed)
  • python3 .github/scripts/docs-checks.py (Passed)

castrojo and others added 11 commits September 9, 2026 20:06
Assisted-by: GPT-5.6 Terra via GitHub Copilot

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GPT-5.6 Terra via GitHub Copilot

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GPT-5.6 Terra via GitHub Copilot

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove build-time sed mutation of /layer/etc/shadow from bluefin-server-ddi.bst and verify systemd-sysusers root user declaration in test_core_login_contract.py.

Assisted-by: GPT-5.6 Terra via GitHub Copilot

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GPT-5.6 Terra via GitHub Copilot

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GPT-5.6 Terra via GitHub Copilot

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reflect implemented SSH public-key provisioning via tmpfiles.extra
and retain static network configuration as the remaining credential gap.

Assisted-by: Claude Sonnet 5 via GitHub Copilot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Deleting os-sshd-preset.bst orphaned the whole
files/os/systemd/system-preset directory, dropping the networkd, k0s
first-boot, and var-mount presets from the OS DDI. Reintroduce packaging
through a clearly named os-systemd-presets.bst import element and guard
it with an anti-orphan packaging assertion over files/os.

BuildStream strips setuid bits from artifacts, so the key-only core
operator could not elevate through wheel. Restore mode 4755 on
/usr/bin/sudo in the DDI and assert the contract at source level.

Scope the roadmap's open credential item to static network
configuration; core SSH keys already arrive via tmpfiles.extra.

Assisted-by: GPT-5.6 Terra via GitHub Copilot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GPT-5.6 Terra via GitHub Copilot

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GPT-5.6 Terra via GitHub Copilot

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
In commit c752c48, the kiosk proxy was updated to generate self-signed TLS
certificates on the fly and serve over HTTPS. Update the install-vm recipe
to probe https://127.0.0.1:8080/ (with --insecure) with fallback to HTTP,
and output HTTPS access URLs.

Assisted-by: Gemini 3.8 Flash via GitHub Copilot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@castrojo
castrojo force-pushed the feat/coreos-compatible-hardened-login branch from e639d51 to 863a6ec Compare September 10, 2026 00:06

@hanthor hanthor left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly good hardening, but it turns SSH on by default without saying so

The core of this — key-only core operator, PermitRootLogin no, PasswordAuthentication no, persistent host keys in /var/lib/ssh, NOPASSWD wheel — is a coherent CoreOS-shaped design and the new test_core_login_contract.py is a genuinely good contract suite. test_no_os_payload_file_is_orphaned_from_the_os_stack in particular is the kind of guard this repo benefits from.

Three things need addressing before merge.

1. SSH flips from disabled-by-default to enabled-by-default, undisclosed

The PR deletes the preset that kept sshd off:

$ git diff main...pr80 -- files/os/systemd/system-preset/zz-enable-sshd.preset
-# SSH is present in the image for on-demand diagnostics, but off by default.
-disable sshd.service
-disable sshd.socket

os-sshd-preset.bst is renamed to os-systemd-presets.bst with the same source path and target, so the element survives — but the only file in it that mentioned sshd is gone, leaving zz-enable-networkd, zz-enable-k0s-first-boot, zz-enable-var-mount. With no explicit disable, sshd falls through to the default preset policy and comes up enabled.

This is not accidental — your own test asserts it:

assert "multi-user.target.wants/sshd.service" in ddi
assert not SSHD_PRESET.exists()

So the behaviour is deliberate and tested. The problem is that the PR body does not mention it. The closest it gets is:

Package systemd presets in OS DDI to ensure networkd and k0s first-boot services run reliably.

A remote-access daemon changing from off to on is the single most consequential line in this diff, and it is described as a networkd packaging change. Please state it explicitly in the body, and ideally record the rationale in docs/ next to the deleted file's comment, which said the opposite.

For the record, the fail-closed gate you built is real and I'm not disputing the design — bluefin-core-access.service runs test -s /var/home/core/.ssh/authorized_keys and the sshd drop-in has Requires=+After= on it, so sshd genuinely refuses to start without a provisioned key. That mitigation is what makes this defensible. It just needs to be stated.

2. The drop-in clears sshd's config validation

[Service]
ExecStartPre=
ExecStartPre=/usr/bin/test -s /var/lib/ssh/ssh_host_ed25519_key
ExecStartPre=/usr/bin/test -s /var/lib/ssh/ssh_host_rsa_key

The bare ExecStartPre= resets the list, which drops whatever the FSDK unit had — on most openssh-systemd builds that includes sshd -t, the config syntax check. Losing it means a malformed drop-in in sshd_config.d/ now fails at daemon start with a less useful error instead of being caught by the pre-check. Since the reason for the reset is presumably to drop upstream's host-key generation (now owned by bluefin-ssh-host-keys.service), consider re-adding /usr/sbin/sshd -t as a fourth ExecStartPre= rather than dropping it wholesale.

3. The verification numbers in the body don't match reality

pytest tests/unit (166 passed, 1 xfailed)

That is below what main alone produces. Measured just now:

main @ 7f376fa:            201 passed, 1 xfailed in 0.59s
main + pr80 (local merge): 210 passed, 1 xfailed in 0.83s

166 corresponds to neither. The suite does pass on this branch — that part is fine — but the number is stale enough to be misleading about what was actually run. Please re-run and update, since the same body is the only record of what was verified.

Also worth a look

  • --insecure in the health probe. curl --silent --insecure ... https://127.0.0.1:8080/ in install-vm accepts any certificate. Acceptable for a loopback dev probe, but note it presumes the kiosk already serves HTTPS — which is what #91 is implementing. If #91 doesn't land first, the https:// branch just fails and falls through to the http:// fallback, so the probe still works; the printed "Access URL" lines and xdg-open are unconditionally https:// though, and will be wrong until then.
  • Blast radius. files/os/sysusers.d/, files/os/sudoers.d/ and files/os/systemd/system-preset/ are the kind of system_files/-style payload that sibling Bluefin images tend to share. A %wheel ALL=(ALL) NOPASSWD: ALL policy and an enabled-by-default sshd are both worth checking against the other images before this pattern spreads.

Status

mergeable_state from the API is dirty, but that appears stale — merging this head into the recorded base succeeds with no conflicts:

$ git checkout -B testmerge 7f376fa && git merge --no-gpg-sign --no-edit pr80
CLEAN      # git diff --name-only --diff-filter=U -> (empty)

CI is green on 863a6ec (build, unit, docs all success). The local gates on the merged tree are green too: 210 passed, 1 xfailed, 41/41 bats, all three .github/scripts checkers exit 0, against a fully-green main baseline — so nothing here is pre-existing breakage.

Blocking on item 1 primarily; items 2 and 3 are quick.


Generated by Claude Code

@castrojo

Copy link
Copy Markdown
Collaborator Author

Addressed review feedback:

  1. Updated the PR summary to explicitly document that sshd is enabled by default with key-only access, detailing the fail-closed gate via bluefin-core-access.service requiring tmpfiles.extra key provisioning before daemon start.
  2. Restored /usr/sbin/sshd -t as an ExecStartPre= check in 10-bluefin-access.conf to ensure sshd configuration syntax validation is preserved.
  3. Updated verification counts to reflect the current test suite: 210 passed, 1 xfailed for pytest and 41 passed for bats.

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.

2 participants