Skip to content

Sharing should control SSH access: evaluate owner/collaborators at connect time #471

Description

@horner

Problem

The Sharing card on a container (owner + collaborators) only controls who sees the container in the manager. It has no effect on SSH.

Today any directory user who matches SSSD_LDAP_ACCESS_FILTER (default (objectClass=*)) and has an LDAP sshPublicKey can SSH into every container — shared or not. Sharing and SSH access should be the same thing: not shared ⇒ only the owner can log in; shared ⇒ owner + collaborators.

Why not fix it in LDAP?

  • Every container binds to the same authentik LDAP outpost as ldap-proxyuser with identical queries. The directory cannot tell which container is asking, so it cannot answer per-container.
  • A per-container ldap_access_filter (e.g. (memberOf=cn=ct-<id>,…)) still needs a per-container value rendered into sssd.conf, which only happens at boot, adds a second source of truth (authentik groups vs the manager DB), and inherits SSSD access-cache latency.

Approach: evaluate at connect time

sshd already runs a program per login (AuthorizedKeysCommand). Use it, plus a PAM account hook for the password/MFA path, to ask the manager whether <user> is allowed on <this container>.

sequenceDiagram
    participant U as ssh client
    participant S as sshd (container)
    participant M as Manager API
    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 or collaborator) / 403
    alt allowed
        S->>L: sss_ssh_authorizedkeys alice
        L-->>S: keys
    else denied
        S-->>U: no keys → permission denied
    end
Loading
  • The manager DB (Container.username + ContainerCollaborator) stays the single source of truth.
  • Share/unshare takes effect on the next connection — no timers, versions, sshd reloads.
  • Authentication is unchanged: keys still come from the user's LDAP sshPublicKey.

Manager (create-a-container)

  • Migration: Containers.sshAccessTokenHash (argon2, nullable; null = not enrolled).
  • middlewares/containerToken.js: Bearer token → argon2.verify against the container's hash.
  • GET /api/v1/containers/:id/ssh-access/:username204 owner/collaborator, 403 otherwise, 400 invalid username. Cache-Control: no-store; denials logged.
  • POST /api/v1/containers/:id/ssh-access/token (owner/admin) → mint/rotate, plaintext returned once.
  • bin/create-container.js mints a token and injects CONTAINER_SSH_TOKEN, MANAGER_URL, CONTAINER_ID as reserved env keys (not user-editable, hidden from the serialized env).
  • Serializer: sshAccess.enforced; Sharing card copy updated + "Not enforced" badge for unenrolled containers.

Base image (images/base)

  • ssh-access-check <user>: reads /etc/ssh-access/{token,url,id} (root, 0600), validates the username, calls the endpoint. Manager unreachable ⇒ honour /var/cache/ssh-access/<user> (touched on every allow, 24 h TTL), otherwise deny. No token file ⇒ allow with a logged warning (unenrolled containers behave as today).
  • AuthorizedKeysCommand /usr/local/bin/ssh-access-authorized-keys %u (check, then exec sss_ssh_authorizedkeys), AuthorizedKeysCommandUser root.
  • /etc/pam.d/sshd: account required pam_exec.so quiet /usr/local/bin/ssh-access-check-pam.
  • Boot oneshot ssh-access-setup.service writes /etc/ssh-access/* from PID 1's environment; environment.sh filters the token out of /etc/environment.

Existing containers (follow-up PR)

bin/enroll-ssh-access.sh, run on each Proxmox node with an admin API key: for each pct list vmid → look up the container → mint a token → pct push the files/hooks → pct exec sshd -t && systemctl reload ssh. Idempotent, --dry-run, no container restart.

Verification

  • Jest: token middleware; endpoint (owner/collaborator 204, stranger 403, bad username 400); mint permissions; serializer flag; reserved env keys not overridable.
  • Shell test for ssh-access-check with a mocked curl (204 / 403 / timeout ± cache).
  • Disposable container: owner OK; collaborator OK; stranger with a valid LDAP key denied on both key and password+MFA paths; unshare ⇒ denied on next attempt; manager down ⇒ owner OK via cache, stranger denied.

Out of scope

  • Terminating live sessions on unshare.
  • ldapusers passwordless sudo — sharing remains admin-level trust inside the container.
  • Proxmox ACLs for collaborators.
  • Admin break-glass (Proxmox console remains available).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions