Skip to content

feat(dev-box): merge user_config SSH keys, listen on port 22 - #172

Merged
Leechael merged 3 commits into
mainfrom
feat/user-config-keys
Nov 13, 2026
Merged

Leechael merged 3 commits into
mainfrom
feat/user-config-keys

Conversation

@Leechael

Copy link
Copy Markdown
Contributor

Summary

dstack 0.6 ships no dev OS image, so dev-box/ replaces it — but it only accepted keys pasted through SSH_AUTHORIZED_KEYS. Phala Cloud pushes workspace SSH keys into the guest's user_config, so on a Cloud deployment those keys silently did nothing. This makes the box consume both channels, and moves sshd to port 22 so connecting looks like the 0.5.x dev image again.

Reopens for review; three commits, one per concern.

Changes

Commit Change
feat(dev-box): serve sshd on port 22 sshd_config listens on 22; README connect recipe becomes <app-id>-22.<gateway-domain>:443
feat(dev-box): merge user_config SSH keys with the env keys entrypoint.sh merges SSH_AUTHORIZED_KEYS and user_config's ssh_authorized_keys, de-duplicated
docs(dev-box): document both SSH key channels and their trust boundary README: sources, merge behaviour, when workspace key changes land, root-equivalence of both channels

Key behaviour:

  • Merge, not override. Either channel gets you in; a platform-side key update never drops the deploy-time key; a key in both is listed once.
  • Path. /host/dstack/user_config — already reachable through the existing /:host:rslave mount. dstack exposes /dstack/user_config as a relative symlink to .host-shared/.user-config (dstack-util system_setup::link_files), so it resolves inside /host; no extra mount or fallback path needed.
  • Tolerant by design. Missing file, malformed JSON, missing field, null, non-string, empty value → "this channel has no keys". An unreadable platform file must not keep an operator out. exit 1 stays reserved for losing both channels.
  • jq streams. {"ssh_authorized_keys":"k"} garbage prints the key and then fails, so the clearing is applied to the whole assignment (config_keys=$(jq …) || config_keys=) rather than inside the substitution — a partial key from a config we just rejected is discarded.
  • Cleaning removes trailing whitespace and blank lines only; leading whitespace, command="…" options and comments survive. De-duplication is on the full cleaned line, so the same key with different restrictions is kept.

Testing

  • shellcheck -s sh, sh -n, dash -n clean on entrypoint.sh.
  • Parse/merge matrix run against the extracted logic under both /bin/sh and dash: both sources with a duplicate, env-only with the file absent, user_config-only with env unset, both empty → exit 1, malformed JSON ({), valid object + trailing garbage, valid object + truncated second document, missing field, null, empty string, number/array/object/bool, top-level array, empty file, blank/whitespace-only lines, no trailing newline.
  • Independent read-only review by a second reviewer: APPROVE, 0 blocking, same matrix plus an authorized_keys line with options/leading/trailing spaces.
  • ./dev.sh check-all — shellcheck portion clean for this example (repo's other examples keep their pre-existing warnings). The compose validation stage cannot run in the authoring environment (no docker compose plugin, no yamllint); it fails identically on main for every example.

Follow-ups (deliberately out of scope)

  1. Image digest. entrypoint.sh and sshd_config are baked into the image, and docker-compose.yaml still pins sha-5a930ae. Until CI builds sha-<newcommit> and a digest-bump PR lands (same flow as fix(dev-box): pin image by digest #170), the published example still behaves the old way.
  2. Regression tests. dev-box/ has no test file and dev.sh has no test entry point; testing the matrix properly needs the key collection extracted into its own script (an extra COPY in the Dockerfile), which is a separate change.
  3. Platform ACL. Both channels grant CVM root, and a workspace member who can change an app's SSH keys can therefore gain it on the next VM start. Whether that is acceptable belongs to Phala Cloud's key management policy; the README now states the trust boundary instead of implying the env var narrows it.

The box listened on 1022 while the dstack 0.5.x dev image used 22, so
the connection recipe differed from the one users already have. Port 22
also matches what the gateway maps for `<app-id>-22.<gateway-domain>`,
making gateway access the plain case again.

Reflected in the README connect section; no other 1022 references were
in this example.
Phala Cloud pushes workspace SSH keys into the guest's user_config, but
the box only read SSH_AUTHORIZED_KEYS, so those keys never worked and
the platform channel was dead weight.

The entrypoint now merges both channels and de-duplicates: the env key
stays valid even after a workspace key update, and a key present in both
is listed once. The user_config read goes through
/host/dstack/user_config, already reachable via the `/:host:rslave`
mount; dstack exposes it as a relative symlink into .host-shared, so it
resolves inside /host.

Tolerance is deliberate: a missing file, malformed JSON, a missing
field, a null, a non-string, or an empty value all mean "this channel
has no keys", because an unreadable platform file must not keep an
operator out. The non-zero exit stays reserved for losing both
channels.

jq streams, so `{"ssh_authorized_keys":"k"} garbage` prints the key and
then fails; clearing the whole assignment on failure discards that
partial key instead of trusting a config we just rejected.
The README only described SSH_AUTHORIZED_KEYS, so a reader could not
tell where workspace keys come from, that the two sources merge instead
of override, or that a workspace key change only lands on the next VM
start.

Also states the consequence plainly: either channel is a path to CVM
root, the box trusts every key the platform delivers, and there is no
switch to ignore user_config today. Which roles may change those keys
is platform policy, not something this example enforces.
@Leechael
Leechael merged commit 7725235 into main Nov 13, 2026
7 checks passed
@Leechael
Leechael deleted the feat/user-config-keys branch November 13, 2026 05:31
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