Create an SSH authenticated jasper proxy
| Environment Variable | Description | Default Value |
|---|---|---|
HOST_KEY |
SSH Server host key. If not set will also check for the file /secrets/host_key. |
|
AUTHORIZED_KEYS |
List of public SSH keys to admit access. The comment field may contain a user tag and optionally the user origin to support multiple users. If not set will also check for the file /config/authorized_keys. |
|
UPSTREAM |
URL for upstream Jasper API. | http://localhost:8081/ |
TOKEN |
JWT bearer token set to Authorization header. | |
USER_TAG |
Sets User-Tag header. Overridden by the user tag in the authorized_keys comment field. Requires upstream server to have JASPER_ALLOW_USER_TAG_HEADER set. |
|
USER_ROLE |
Sets User-Role header. Requires upstream server to have JASPER_ALLOW_USER_ROLE_HEADER set. |
|
LOCAL_ORIGIN |
Sets Local-Origin header. Overridden by the user origin in the authorized_keys comment field. Requires upstream server to have JASPER_ALLOW_LOCAL_ORIGIN_HEADER set. |
|
READ_ACCESS |
Sets Read-Access header. Requires upstream server to have JASPER_ALLOW_AUTH_HEADERS set. |
|
WRITE_ACCESS |
Sets Write-Access header. Requires upstream server to have JASPER_ALLOW_AUTH_HEADERS set. |
|
TAG_READ_ACCESS |
Sets Tag-Read-Access header. Requires upstream server to have JASPER_ALLOW_AUTH_HEADERS set. |
|
TAG_WRITE_ACCESS |
Sets Tag-Write-Access header. Requires upstream server to have JASPER_ALLOW_AUTH_HEADERS set. |
|
STORAGE_ACCESS |
Comma separated list of tag@origin users granted read-only SFTP access to their origin's Jasper storage folder. See Storage access. |
|
SSHD_LOG_LEVEL |
Sets the LogLevel in sshd_config. | INFO |
CONFIG_CHANGE_MODE |
Handles a semantic /config/authorized_keys change: restart exits immediately; drain remains healthy for a Deployment rollout and requires Kubernetes API access. |
restart |
AUTHORIZED_KEYS_CONFIGMAP_NAME |
ConfigMap fetched from the Kubernetes API while draining. Required when CONFIG_CHANGE_MODE=drain. |
|
NAMESPACE |
Namespace containing the authorized-keys ConfigMap. If unset, the service-account namespace is used. |
Users listed in STORAGE_ACCESS (for example admin@backup, or admin plus
LOCAL_ORIGIN when the key comment has no origin) can download files such as
backup zips from the Jasper storage folder for their origin, for example with
sftp or rclone sync. Access is read-only: uploads, deletes and renames are
rejected.
Mount the Jasper storage volume on /var/lib/jasper. Jasper stores each origin
in /var/lib/jasper/<origin>, including the leading @, and uses
/var/lib/jasper/default for the default origin. If the folder for a user's
origin does not exist, a warning is logged and that user gets no storage access.
Each user is chrooted into /opt/chrooted-sftp/<user>, and the storage folder is
bind-mounted read-only on /storage. The SFTP session starts in that folder.
Users with storage access can still open their API tunnel. Other users keep
port forwarding only and cannot open an SFTP session.
Bind mounting requires --cap-add SYS_ADMIN, and on hosts with AppArmor
--security-opt apparmor=unconfined. Without them a warning is logged and
storage access is skipped. In Docker Compose:
services:
jasper-ssh:
image: ghcr.io/cjmalloy/jasper-ssh
cap_add:
- SYS_ADMIN
security_opt:
- apparmor:unconfined
environment:
STORAGE_ACCESS: admin@backup
volumes:
- jasper-storage:/var/lib/jasperDownload files with:
sftp -P 22 <user>@host # files are in /storage
rclone sync :sftp,host=HOST,port=22,user=USER,key_file=~/.ssh/id_ed25519:/storage ./backupWhen the mounted /config/authorized_keys changes, the health check compares
the normalized key set, so reordering keys does not request a restart. If a user
loses any key, all of that user's existing sessions are terminated. Shutdown
remains latched even if the original file contents are restored. In the default
restart mode, the server exits immediately so a container restart policy can
replace it and load the new keys. In drain mode, the health check applies
per-user revocations but remains healthy; the rollout controller replaces the
pod so its preStop hook can drain the remaining sessions. If the hook cannot
fetch authoritative keys from the Kubernetes API, it immediately falls back to
restart behavior instead of draining against a potentially stale projected
volume.
Configure /shutdown.sh as an executable Kubernetes preStop hook:
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 20
template:
spec:
terminationGracePeriodSeconds: 604800
containers:
- name: jasper-ssh
lifecycle:
preStop:
exec:
command: ["/shutdown.sh"]The hook immediately stops the SSH listener so no new connections are accepted,
applies the same per-user revocations as the health check, then waits for the
remaining established SSH connections to close. It continues fetching the
ConfigMap from the Kubernetes API while it drains so revocations are applied
without waiting for projected-volume updates. Drain mode requires
AUTHORIZED_KEYS_CONFIGMAP_NAME and get access to that ConfigMap for the SSH
pod's service account. If an API fetch fails before or during draining, the hook
signals the container to restart immediately. It has no internal timeout. The one-week
terminationGracePeriodSeconds is only an upper bound; disruptions or
administrative deletion can terminate sessions earlier.
maxUnavailable: 0 preserves availability during an ordinary rollout, while
maxSurge: 20 permits up to 20 extra pods but does not strictly limit how many
pods may be terminating. Deployments with more than 20 replicas require
controller-orchestrated batches for a strict termination concurrency limit.
The optional controller image is published as
ghcr.io/cjmalloy/jasper-ssh-controller. It uses in-cluster authentication,
watches one authorized-keys ConfigMap, and patches the configured SSH
Deployment's pod-template annotation with the ConfigMap resourceVersion.
Repeated events for an already represented version do not produce another
patch. Deployments are patched in both restart and drain modes. In drain
mode, the resulting Deployment rollout invokes the old pods' preStop hooks
while their liveness checks remain healthy.
| Environment variable | Description | Default |
|---|---|---|
NAMESPACE |
Namespace containing the ConfigMap and Deployment. | default |
AUTHORIZED_KEYS_CONFIGMAP_NAME |
Authorized-keys ConfigMap to watch. Required. | |
SSH_DEPLOYMENT_NAME |
jasper-ssh Deployment to patch. Required. | |
ROLLOUT_ANNOTATION_KEY |
Pod-template annotation used to request rollouts. | jasper-ssh.cjmalloy.com/authorized-keys-resource-version |
ROLLOUT_DELAY |
Optional non-negative Go duration before reconciliation, allowing projected keys to reach existing pods first. | 0s |
HEALTH_ADDRESS |
Controller health server listen address. | :8080 |
The controller exposes /livez and /readyz on its health address and handles
SIGINT and SIGTERM gracefully. Mount the watched ConfigMap at
/config/authorized_keys. An example namespaced ServiceAccount, Role, and
RoleBinding is available at controller/rbac.yaml; update its resource
names to match your ConfigMap and Deployment.
Run the jasper-ssh Bash integration suite with Docker Compose:
docker compose -f compose.test.yml up --build --wait \
keygen http-backend config-tester target-server target-server-restart \
shutdown-hook
docker compose -f compose.test.yml up --build --no-deps \
--abort-on-container-exit --exit-code-from test-runner test-runner
docker compose -f compose.test.yml down -vThe jasper-ssh suite verifies the headers sent to the upstream service, semantic
key comparison, per-user revocation, restart behavior, and termination draining.
Controller tests are separate and run with go test ./... from controller/.