Skip to content

Add API to manage SSH authorized keys on Home Assistant OS - #7039

Draft
agners wants to merge 6 commits into
mainfrom
os-ssh-authorized-keys
Draft

Add API to manage SSH authorized keys on Home Assistant OS#7039
agners wants to merge 6 commits into
mainfrom
os-ssh-authorized-keys

Conversation

@agners

@agners agners commented Jul 13, 2026

Copy link
Copy Markdown
Member

Proposed change

Add an API to manage root's SSH authorized keys on Home Assistant OS through the Supervisor, wrapping the AddSSHAuthKey and ClearSSHAuthKeys D-Bus methods of the OS Agent io.hass.os System object, which the Supervisor never used so far. This provides the backend for managing SSH access to the HAOS debug console (port 22222). The endpoints map 1:1 onto the OS Agent operations, so no request can partially succeed:

  • GET /os/ssh/authorized_keys returns the configured keys ({"keys": ["ssh-ed25519 AAAA…", …]}).
  • POST /os/ssh/authorized_keys with {"key": "ssh-ed25519 AAAA…"} appends a single key.
  • DELETE /os/ssh/authorized_keys removes all keys.

The GET requires OS Agent 1.11.0 (which added the ListSSHAuthKeys D-Bus method, home-assistant/os-agent#281) and returns 404 on older releases, like the Raspberry Pi firmware endpoints do — this provides the auditability of what keys grant access to the box, including keys imported via USB or written by add-ons. Replacing the configured set means clear and re-add; an idempotent PUT to replace the whole set can be added later.

Proper key validation is left to OS Agent, which validates submitted keys since 1.10.0 (home-assistant/os-agent#273); a key it rejects surfaces as an error response including its validation message. The endpoints work with older OS Agent releases too, accepting that validation is lossy there: the Supervisor runs a basic sanity check on the submitted key — no control characters (one submitted key can never write more than one authorized_keys line) and at most 3000 bytes (dropbear — the consumer of the file on HAOS — ignores longer lines, which would leave a key that passes but never works). On releases before 1.10.0 the error OS Agent returns when clearing an already absent authorized_keys file (inverted error check) is treated as the empty state it reports. For add-on tokens both endpoints require the admin role (like /os/datadisk/wipe); requests proxied through Home Assistant Core are unaffected.

Since dropbear on HAOS is gated by ConditionFileNotEmpty on the authorized_keys file, which systemd only evaluates at unit start, the service is started after a key is added (a running dropbear re-reads the file on every authentication attempt, and starting an active unit is a no-op).

Type of change

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

Additional information

  • This PR fixes or closes issue: fixes #
  • This PR is related to issue:
  • Link to documentation pull request:
  • Link to cli pull request:
  • Link to client library pull request:

Related OS Agent pull requests: home-assistant/os-agent#273 (key validation and hardening of the wrapped D-Bus methods, released in OS Agent 1.10.0; add and clear work with both older and newer releases) and the ListSSHAuthKeys addition released in OS Agent 1.11.0, which the GET endpoint requires.

Checklist

  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • The code has been formatted using Ruff (ruff format supervisor tests)
  • Tests have been added to verify that the new code works.

If API endpoints or add-on configuration are added/changed:

The OS Agent has long exposed AddSSHAuthKey and ClearSSHAuthKeys on its
io.hass.os System D-Bus object, but the Supervisor never wrapped them, so
there was no way to manage root's SSH authorized keys through the
Supervisor API. Add POST /os/ssh/authorized_keys, which replaces the
configured keys with the submitted list (an empty list just clears them).

Since OS Agent writes each key verbatim to /root/.ssh/authorized_keys as
root, the endpoint validates strictly before anything is written: plain
public keys only (no options, no certificates), a key type allowlist
matching what dropbear on Home Assistant OS can verify, no control
characters (one submitted key can never write more than one line), the
base64 blob must embed the declared key type, and entries are capped at
dropbear's 3000 byte per-line limit. The endpoint is admin-only for
add-on tokens.

Replacement clears the existing keys and then adds each key. OS Agent
releases up to 1.10.x return an error when clearing an already absent
file (inverted error check, since fixed), which is the state of every
first-time user, so this specific error is treated as the empty state it
reports.

dropbear on Home Assistant OS is gated by ConditionFileNotEmpty on the
authorized_keys file, which systemd only evaluates when the unit starts,
so the service is started after a non-empty key set is written. A running
dropbear re-reads the file on every authentication attempt and needs no
restart.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a new Supervisor OS API endpoint to replace HAOS root’s SSH authorized_keys via OS Agent D-Bus methods, providing a write-only management path for debug-console SSH access while validating submitted keys before any host mutation.

Changes:

  • Add POST /os/ssh/authorized_keys, including strict validation of OpenSSH public keys (type allowlist, no control chars/options, base64 and embedded-type checks, 3000-byte line limit).
  • Implement OS manager logic to clear then append keys via OS Agent D-Bus, and start dropbear.service after writing a non-empty set.
  • Extend D-Bus mocks and add comprehensive API + D-Bus interface tests, including role restrictions and error handling paths.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
supervisor/api/init.py Registers the new /os/ssh/authorized_keys route.
supervisor/api/const.py Adds ATTR_KEYS constant for request schema.
supervisor/api/os.py Implements request schema, SSH public key validator, and API handler.
supervisor/api/middleware/security.py Restricts the new endpoint to admin role for add-on tokens.
supervisor/dbus/agent/system.py Adds OS Agent System proxy methods for adding/clearing SSH keys.
supervisor/os/manager.py Adds job to clear/append keys and start dropbear on HAOS.
tests/api/middleware/test_security.py Ensures add-on token role enforcement includes the new endpoint.
tests/api/test_os.py Adds endpoint tests covering success, validation failures, HAOS absence, OS Agent quirks, and dropbear start errors.
tests/dbus/agent/test_system.py Adds OS Agent System interface tests for new D-Bus calls.
tests/dbus_service_mocks/agent_system.py Extends mock System service with Add/Clear SSH key methods.

Comment thread supervisor/os/manager.py Outdated
Comment thread supervisor/api/os.py
@agners
agners marked this pull request as draft July 14, 2026 16:18
agners added 3 commits July 14, 2026 18:19
Review discussion questioned the full key validation (type allowlist,
base64 blob checks, canonicalization): OS Agent 1.10.0 validates
submitted keys itself and treats clearing an already absent
authorized_keys file as success, so the Supervisor can rely on it
instead of duplicating the logic.

Require OS Agent 1.10.0 or newer and reject requests on older releases
with 404, like the Raspberry Pi firmware endpoints do; this also makes
the missing-file compatibility shim for the clear call unnecessary. A
key rejected by OS Agent surfaces as an error response including its
validation message. The Supervisor keeps only a basic per-key sanity
check that runs before anything is written: no control characters (one
submitted key can never write more than one authorized_keys line) and
at most 3000 bytes (dropbear ignores longer lines, which would leave a
key that passes but never works).
Requiring OS Agent 1.10.0 would keep the feature unavailable until the
next OS update reaches users, while Supervisor updates roll out
independently. Drop the version requirement and accept that validation
is lossy on older releases: the Supervisor sanity check still prevents
writing more than one line per key or lines dropbear ignores, but
proper key validation only happens on OS Agent 1.10.0 or newer.

This brings back the need to tolerate the error OS Agent releases
before 1.10.0 return when clearing an already absent authorized_keys
file (inverted error check): match the complete os.Remove error message
for the authorized_keys path and treat it as the empty state clearing
aims for, on affected versions only.
@agners
agners marked this pull request as ready for review July 14, 2026 18:46

@mdegat01 mdegat01 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.

I am not a fan of one upsert API like this. If we want to keep it then it should be PUT or PATCH not POST but I think we should change the API layout per my comment below

Comment thread supervisor/os/manager.py Outdated
Comment on lines +531 to +543
_LOGGER.info("Replacing SSH authorized keys on host (%d keys)", len(keys))
try:
await self.sys_dbus.agent.system.clear_ssh_auth_keys()
except DBusError as err:
# On affected OS Agent releases the missing-file error is the
# empty state clearing aims for, so treat it as success there.
if (
self.sys_dbus.agent.version >= CLEAR_SSH_AUTH_KEYS_FIXED_VERSION
or CLEAR_SSH_AUTH_KEYS_MISSING_FILE_ERROR not in str(err)
):
raise HassOSError(
f"Can't clear SSH authorized keys: {err!s}", _LOGGER.error
) from err

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.

Do we have to require users pass in every key every time? Can we offer a way to add to existing keys instead of replacing the full list each time? Since OSAgent offers a designated clear authorized keys API it seems unnecessary frustrating to make one API that does an upsert. Wouldn't it be easier to have a POST API that adds one or more keys and a DELETE API that clears the file?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah agreed mirroring the OS Agent API makes more sense. Maybe we should have a get anyways at one point. I think I was concerned about unnecessary information leak when initially created the OS Agent implementation, but maybe that was a bit overly cautious.

Comment thread supervisor/api/__init__.py Outdated
web.get("/os/datadisk/list", api_os.list_data),
web.post("/os/datadisk/wipe", api_os.wipe_data),
web.post("/os/boot-slot", api_os.set_boot_slot),
web.post("/os/ssh/authorized_keys", api_os.ssh_authorized_keys),

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.

If we truly want this endpoint to replace the entire authorized keys file with this new set of keys and not add a key to the set we should use PUT here not POSTimo. Per mozilla guidelines:

The PUT HTTP method creates a new resource or replaces a representation of the target resource with the request content.

The difference between PUT and POST is that PUT is idempotent: calling it once is no different from calling it several times successively (there are no side effects).

As defined this is idempotent and replaces the resource (authorized key file in this case) so PUT is the better fit. But personally I would prefer this use POST, not be idempotent, and just append one or more keys to the existing file.

Comment thread supervisor/os/manager.py Outdated
Comment on lines +527 to +529
OS Agent validates each key since 1.10.0 and only offers clear and
append operations, so the replacement is not atomic: if an append is
rejected or fails, keys added before it remain in place.

@mdegat01 mdegat01 Jul 14, 2026

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.

This seems like a great reason to lay out the API like this:

  1. POST /os/ssh/authorized_keys - accepts exactly one key. Calls the add ssh key in OS agent with it
  2. DELETE /os/ssh/authorized_keys - accepts no arguments. Calls clear ssh auth keys in OS Agent

This makes the API easy to use, easy to translate to the CLI and ensures that no API call can partially succeed by having some OS Agent operations succeed before one fails. Since each API maps 1:1 with an OS Agent DBus API.

@home-assistant
home-assistant Bot marked this pull request as draft July 14, 2026 20:25
@home-assistant

Copy link
Copy Markdown

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

Review feedback preferred endpoints mapping 1:1 onto the OS Agent D-Bus
methods over a single replace-the-set API, whose POST semantics were
also questioned (an idempotent full replacement would be PUT).

POST /os/ssh/authorized_keys now takes a single key ({"key": "..."})
and appends it via AddSSHAuthKey; DELETE /os/ssh/authorized_keys
removes all keys via ClearSSHAuthKeys. Each call maps to exactly one
OS Agent operation, so no request can partially succeed. Clients that
want to replace the configured set clear and re-add; a GET (which
needs an OS Agent extension first) and an idempotent PUT can be added
later.

The per-key sanity check, the dropbear service start after adding a
key, and the tolerance for the missing-file clear error of OS Agent
releases before 1.10.0 carry over unchanged.
@agners
agners requested review from mdegat01 and a lite review from Copilot August 4, 2026 09:41
@agners
agners marked this pull request as ready for review August 4, 2026 09:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Human review recommended

It introduces new security-sensitive functionality (managing root SSH access and starting an SSH service) that warrants final human review despite solid test coverage.

Review details
  • Files reviewed: 10/10 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

@mdegat01 mdegat01 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.

I think I'd prefer we move these APIs to core only unless we really want to allow management from apps. And seems like we should stop the dropbear service after the keys are cleared.

r"|/network/.+"
r"|/observer/.+"
r"|/os/(?!datadisk/wipe).+"
r"|/os/(?!datadisk/wipe|ssh/authorized_keys).+"

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.

Should we make this a core only endpoint rather then a manager one? Putting it in manager would give many apps the ability to call it. I know its hardly the only attack avenue if you consider the idea of a malicious app but it still just doesn't seem like capability we want to allow apps to do.

The downside would be the SSH app also can't call it which is probably the one app we'd prefer to allow. But as long as we make the proposed UI this seems like an acceptable situation to only allow host ssh key management from HA UI and the host shell itself.

Comment thread supervisor/os/manager.py
):
raise HassOSError(
f"Can't clear SSH authorized keys: {err!s}", _LOGGER.error
) from err

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.

Shouldn't this stop dropbear service after a successful key clear? Since we've reset to initial state where the authorized key file is empty.

@home-assistant
home-assistant Bot marked this pull request as draft August 4, 2026 19:53
With only add and clear operations the authorized_keys file is
write-only for API consumers: a user cannot audit which keys grant
access to the box, or whether any exist at all — including keys that
were imported from USB or written by add-ons.

OS Agent 1.11.0 added a ListSSHAuthKeys D-Bus method. Expose it as
GET /os/ssh/authorized_keys, returning the configured entries verbatim.
The endpoint requires OS Agent 1.11.0 and returns 404 on older
releases, like the Raspberry Pi firmware endpoints do; add and clear
keep working on all OS Agent releases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants