feat(ssh): sharing list controls who may SSH into a container - #472
Open
horner wants to merge 1 commit into
Open
feat(ssh): sharing list controls who may SSH into a container#472horner wants to merge 1 commit into
horner wants to merge 1 commit into
Conversation
Owner + collaborators are now the SSH allowlist, evaluated by the manager on every connection. Closes #471 (part 1 of 2; enrollment script for existing containers follows). Manager: - Containers.sshAccessTokenHash + Container.rotate/ensure/verifySshAccessToken - GET /api/v1/containers/:id/ssh-access/:username (container-token auth): 204 owner/collaborator, 403 otherwise - POST /sites/:siteId/containers/:id/ssh-access/token (owner/admin) mints - CONTAINER_ID / CONTAINER_SSH_TOKEN reserved env keys injected on create, preserved across reconfigure; MANAGER_URL seeded as a default env var - serializer: sshAccessEnforced Base image: - ssh-access-check (AuthorizedKeysCommand wrapper + PAM account hook) asks the manager; cached allow honoured when the manager is unreachable; unenrolled containers behave as before - ssh-access-setup.service copies the token out of PID 1's env into /etc/ssh-access (kept out of /etc/environment) Client: Sharing copy + SshAccessBadge. Docs + OpenAPI updated.
| next(); | ||
| } | ||
|
|
||
| router.use(asyncHandler(containerTokenAuth)); |
Contributor
There was a problem hiding this comment.
Pull request overview
Implements per-container SSH authorization based on the container’s Sharing list (owner + collaborators), enforced at SSH connect time by having the base image call back to the manager via a container-scoped token.
Changes:
- Add manager endpoints + model/migrations to mint/verify per-container SSH-access tokens and answer “may user X SSH into container Y?”
- Update the base image to gate both SSH key lookup (AuthorizedKeysCommand) and PAM account checks via the manager callback, with outage-safe cached allow.
- Update UI, OpenAPI, and docs to reflect “SSH enforced / not enforced” and the new MANAGER_URL setting.
Reviewed changes
Copilot reviewed 24 out of 26 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| mie-opensource-landing/docs/users/creating-containers/web-gui.md | Documents who can SSH in and the “SSH enforced” UX behavior. |
| mie-opensource-landing/docs/developers/database-schema.md | Documents the new sshAccessTokenHash column semantics. |
| mie-opensource-landing/docs/admins/ldap-servers.md | Documents MANAGER_URL default env var and how enforcement interacts with LDAP filter. |
| images/base/test-ssh-access-check.sh | Adds a stubbed-curl shell test for ssh-access-check. |
| images/base/ssh-access-setup.sh | Boot-time enrollment: writes /etc/ssh-access/* from PID 1 environment. |
| images/base/ssh-access-setup.service | Ensures enrollment runs before SSH accepts logins. |
| images/base/ssh-access-check.sh | Core allow/deny logic with cache fallback when manager is unavailable. |
| images/base/ssh-access-authorized-keys.sh | AuthorizedKeysCommand wrapper that gates LDAP key serving. |
| images/base/environment.sh | Filters CONTAINER_SSH_TOKEN out of /etc/environment. |
| images/base/Dockerfile | Installs scripts, adds sshaccess user, wires PAM + systemd enablement. |
| images/base/50-sss-ssh-authorizedkeys.conf | Switches sshd to use the new AuthorizedKeysCommand wrapper as sshaccess. |
| create-a-container/routers/api/v1/ssh-access.js | Adds container-token-authenticated SSH access check endpoint. |
| create-a-container/routers/api/v1/index.js | Mounts the new SSH access router under /api/v1. |
| create-a-container/routers/api/v1/containers.js | Adds sshAccessEnforced serialization + token mint/rotate endpoint. |
| create-a-container/routers/api/v1/tests/ssh-access.api.test.js | Adds API tests for allow/deny, token auth, minting, and reserved env behavior. |
| create-a-container/routers/api/v1/tests/containers.serialize.test.js | Updates serializer test stub for the new sshAccessEnforced field. |
| create-a-container/openapi.v1.yaml | Documents new endpoints and sshAccessEnforced schema field. |
| create-a-container/models/container.js | Adds token hashing/verification, enforcement flag, env reserved keys handling. |
| create-a-container/migrations/20260905000001-seed-manager-url-env-var.js | Seeds MANAGER_URL into default container env vars setting. |
| create-a-container/migrations/20260905000000-add-container-ssh-access-token.js | Adds Containers.sshAccessTokenHash nullable column. |
| create-a-container/client/src/pages/containers/ContainersListPage.tsx | Updates Sharing copy + shows SSH enforcement badge in modal. |
| create-a-container/client/src/pages/containers/ContainerFormPage.tsx | Updates Sharing copy + shows SSH enforcement badge. |
| create-a-container/client/src/lib/types.ts | Adds sshAccessEnforced to the Container type. |
| create-a-container/client/src/components/containers/SshAccessBadge.tsx | New badge component for SSH enforced/not enforced status. |
| create-a-container/bin/reconfigure-container.js | Preserves/reuses SSH token across reconfigure via env carry-over. |
| create-a-container/bin/create-container.js | Mints/reuses SSH token and injects reserved env keys on create. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+23
to
+24
| install -d -m 0750 -o root -g sshaccess "$CONF_DIR" | ||
| install -d -m 0755 -o sshaccess -g sshaccess "$CACHE_DIR" |
| if (!Number.isInteger(id) || id <= 0 || !token) { | ||
| throw new ApiError(401, 'unauthorized', 'Container token required'); | ||
| } | ||
| const container = await Container.findByPk(id, { include: [{ association: 'collaborators' }] }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part 1 of #471 — the Sharing list (owner + collaborators) becomes the SSH allowlist, evaluated by the manager on every connection. Part 2 (enrollment script for existing containers, run per Proxmox node) follows once this is merged.
How it works
sequenceDiagram participant U as ssh client participant S as sshd (container) participant M as Manager participant L as SSSD / LDAP U->>S: connect as alice S->>S: AuthorizedKeysCommand ssh-access-authorized-keys alice S->>M: GET /api/v1/containers/:id/ssh-access/alice (Bearer container token) M-->>S: 204 owner/collaborator · 403 otherwise alt allowed S->>L: sss_ssh_authorizedkeys alice else denied S-->>U: no keys → permission denied end S->>M: PAM account: ssh-access-check (same call, covers password/MFA)sshdreloads.sshPublicKey); this only adds authorization.Manager (
create-a-container)Containers.sshAccessTokenHash(argon2, nullable) +MANAGER_URLseeded into default container env vars.Container.sshAllowsUser(),rotateSshAccessToken(),ensureSshAccessToken(currentLxcEnv)(reuses the running token on reconfigure),verifySshAccessToken().CONTAINER_ID/CONTAINER_SSH_TOKENare reserved env keys — stripped from user/admin input, injected bybuildLxcEnvConfig({ sshAccessToken }).GET /api/v1/containers/:id/ssh-access/:username— container-token auth;204/403/400bad username /401bad token;Cache-Control: no-store; denials logged.POST /sites/:siteId/containers/:id/ssh-access/token— owner/admin mints/rotates (used by the part-2 enrollment script).sshAccessEnforced.Base image (
images/base)ssh-access-check— one script, two callers:AuthorizedKeysCommandwrapper (AuthorizedKeysCommandUser sshaccess) andpam_execin the account phase. Token is passed to curl via stdin config, never argv.ssh-access-setup.service— copies the three env vars from PID 1 into/etc/ssh-access/(0640 root:sshaccess);environment.shfiltersCONTAINER_SSH_TOKENout of/etc/environment.Client
Sharing copy now says what it does;
SshAccessBadgeshows SSH enforced / SSH not enforced.Tests
routers/api/v1/__tests__/ssh-access.api.test.js— owner/collaborator 204, stranger 403, unshare → 403, invalid usernames 400, missing/wrong/rotated/user-API-key tokens 401, unenrolled 401, mint permissions (collaborator 403, stranger 404), reserved keys not overridable, token reuse/rotation.images/base/test-ssh-access-check.sh— 19 checks with a stubbedcurl(allow/deny/cache/outage/401/invalid user/PAM_USER).npx jest: 118 pass; the one failure (mcp-proxy › not mounted when no MCP server is configured) is pre-existing onmain.Deploy notes
MANAGER_URLto the manager URL reachable from containers.Out of scope
Killing live sessions on unshare;
ldapuserspasswordless sudo (sharing = admin-level trust); Proxmox ACLs for collaborators.