Skip to content

Manage dropbear via a systemd path unit watching authorized_keys - #4896

Closed
agners wants to merge 1 commit into
devfrom
dropbear-path-unit
Closed

Manage dropbear via a systemd path unit watching authorized_keys#4896
agners wants to merge 1 commit into
devfrom
dropbear-path-unit

Conversation

@agners

@agners agners commented Jul 13, 2026

Copy link
Copy Markdown
Member

Proposed change

Debug SSH access on port 22222 currently has two lifecycle mechanisms: dropbear.service is enabled and gated by ConditionFileNotEmpty=/root/.ssh/authorized_keys at boot, and haos-config explicitly starts/stops the service when importing a CONFIG partition. Keys written through any other path — most notably os-agent's AddSSHAuthKey D-Bus method (see home-assistant/os-agent#273), which the Supervisor is growing an API on top of — do not start dropbear until the next reboot.

This PR adds dropbear.path, watching /root/.ssh/authorized_keys, which triggers a oneshot dropbear-lifecycle.service that starts dropbear when the file is non-empty and stops it when it is absent. The file becomes the single source of truth for whether debug SSH runs, regardless of who wrote it (haos-config import, os-agent, or a manual write on the console), and haos-config drops its explicit systemctl start/stop dropbear calls.

Resulting behavior:

Event Result
Boot, keys present dropbear.service starts via its own enablement + condition (unchanged)
Boot, no keys Service skipped by ConditionFileNotEmpty (unchanged)
Key written at runtime (os-agent, haos-config, manual) Path unit triggers, dropbear starts
File removed at runtime Path unit triggers, dropbear stops

Design notes

  • PathChanged= only, deliberately no PathExists=: an exists-trigger pointed at a oneshot that doesn't consume the file re-triggers on every deactivation until the start rate limit is hit and the path unit fails. Boot-time presence is already covered by the enabled dropbear.service and its ConditionFileNotEmpty.
  • The lifecycle service is idempotent (converges to file state rather than toggling), since the watch can fire for any change in the directory — including temporary files from atomic writes. os-agent's new temp-file-plus-rename write lands as IN_MOVED_TO, which PathChanged= covers.
  • RequiresMountsFor=/root/.ssh orders the watch after root-.ssh.mount: inotify events don't cross mount points, so a watch armed before the bind mount would sit on the shadowed rootfs inode and never fire. It also restarts the path unit if the mount is ever restarted, and degenerates to a no-op if the bind mount goes away in a future layout.
  • Before=haos-config.service arms the watch before the config import can write the file, closing the missed-event race on first boot (PathChanged= is edge-triggered and does not check the initial state).
  • Unit enablement comes from the existing systemctl preset-all in post-build.sh (implicit enable * policy); no preset change needed.
  • systemd path units survive deletion/recreation of watched path components (watches are placed on every ancestor and re-armed on events), so ClearSSHAuthKeys / MkdirAll cycles from os-agent are handled.

Type of change

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (which adds functionality to an existing integration)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

🤖 Generated with Claude Code

home-assistant[bot]

This comment was marked as outdated.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e2c835f9-b2fa-4658-8a96-b7e69869dde0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dropbear-path-unit

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@agners
agners force-pushed the dropbear-path-unit branch from 1a173c6 to 79a048a Compare July 13, 2026 20:19
home-assistant[bot]

This comment was marked as outdated.

@agners agners closed this Jul 13, 2026
@agners agners reopened this Jul 13, 2026
home-assistant[bot]

This comment was marked as outdated.

Debug SSH access on port 22222 currently has two lifecycle mechanisms:
dropbear.service is enabled and gated by
ConditionFileNotEmpty=/root/.ssh/authorized_keys at boot, and haos-config
explicitly starts/stops the service when importing a CONFIG partition.
Keys written through any other path - most notably os-agent's
AddSSHAuthKey D-Bus method, which the Supervisor is growing an API on
top of - do not start dropbear until the next reboot.

Add dropbear.path, watching /root/.ssh/authorized_keys, triggering a
oneshot dropbear-lifecycle.service that starts dropbear when the file is
non-empty and stops it when it is absent. This makes the file the single
source of truth for whether debug SSH runs, regardless of the writer,
and lets haos-config drop its explicit systemctl calls.

Design notes:

- PathChanged= only, no PathExists=: an exists-trigger pointed at a
  oneshot that does not consume the file re-triggers on every
  deactivation until the start rate limit is hit. Boot-time presence is
  already handled by the enabled dropbear.service and its
  ConditionFileNotEmpty.
- The lifecycle service is idempotent since the watch may fire for any
  change in the directory, including temporary files from atomic
  writes (rename into place is IN_MOVED_TO, which PathChanged= covers).
- RequiresMountsFor=/root/.ssh orders the watch after root-.ssh.mount:
  inotify events do not cross mount points, so a watch armed on the
  shadowed rootfs inode would never fire. If the bind mount ever goes
  away, the directive degenerates to a no-op.
- Before=haos-config.service arms the watch before the config import
  can write the file, closing the missed-event race on first boot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@agners
agners force-pushed the dropbear-path-unit branch from 79a048a to 36d23c7 Compare July 13, 2026 20:21
@agners
agners dismissed home-assistant[bot]’s stale review July 13, 2026 20:22

The cla error is addressed with the latest commit

@agners

agners commented Jul 14, 2026

Copy link
Copy Markdown
Member Author

This feels a bit backwards: Dropbear gets started based on a file instead of explicit start call. This also means that in case the start fails the caller would not directly notice. And finally, it also opens up to a small race condition (e.g. add key then connect through SSH isn't guarantee to work since it may take short moment for Dropbear to start.

We need to start Dropbear explicitly in home-assistant/supervisor#7039 anyways. This more explicit lifecycle management of system services seems the better idea.

Hence not following through with this file based approach.

@agners agners closed this Jul 14, 2026
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant