Skip to content

feat(ssh): sharing list controls who may SSH into a container - #472

Open
horner wants to merge 1 commit into
mainfrom
feat/ssh-access-sharing
Open

feat(ssh): sharing list controls who may SSH into a container#472
horner wants to merge 1 commit into
mainfrom
feat/ssh-access-sharing

Conversation

@horner

@horner horner commented Sep 6, 2026

Copy link
Copy Markdown
Member

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)
Loading
  • Manager DB stays the single source of truth; share/unshare applies on the next connection. No timers, versions, or sshd reloads.
  • Authentication is unchanged (LDAP sshPublicKey); this only adds authorization.
  • Manager unreachable ⇒ recent cached allow is honoured (24 h), so the owner keeps access through an outage while strangers stay out.
  • Containers without a token (created before this) behave exactly as before and show SSH not enforced in the UI.

Manager (create-a-container)

  • Migration Containers.sshAccessTokenHash (argon2, nullable) + MANAGER_URL seeded into default container env vars.
  • Container.sshAllowsUser(), rotateSshAccessToken(), ensureSshAccessToken(currentLxcEnv) (reuses the running token on reconfigure), verifySshAccessToken().
  • CONTAINER_ID / CONTAINER_SSH_TOKEN are reserved env keys — stripped from user/admin input, injected by buildLxcEnvConfig({ sshAccessToken }).
  • GET /api/v1/containers/:id/ssh-access/:username — container-token auth; 204 / 403 / 400 bad username / 401 bad 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).
  • Serializer: sshAccessEnforced.

Base image (images/base)

  • ssh-access-check — one script, two callers: AuthorizedKeysCommand wrapper (AuthorizedKeysCommandUser sshaccess) and pam_exec in 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.sh filters CONTAINER_SSH_TOKEN out of /etc/environment.

Client

Sharing copy now says what it does; SshAccessBadge shows 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 stubbed curl (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 on main.

Deploy notes

  1. Run migrations; set Settings → Default Container Environment Variables → MANAGER_URL to the manager URL reachable from containers.
  2. Rebuild/publish the base image.
  3. New containers are enforced automatically. Existing containers: part 2.

Out of scope

Killing live sessions on unshare; ldapusers passwordless sudo (sharing = admin-level trust); Proxmox ACLs for collaborators.

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.
Copilot AI lite review requested due to automatic review settings September 6, 2026 04:54
next();
}

router.use(asyncHandler(containerTokenAuth));

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

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' }] });
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.

3 participants