Security Azure

JumpServer on Ubuntu 24.04 on Azure User Guide

| Product: JumpServer on Ubuntu 24.04 LTS on Azure

Overview

JumpServer is an open source privileged access management (PAM) bastion: a single audited doorway in front of the infrastructure you care about. Operators sign in to JumpServer rather than to your servers, JumpServer holds the real credentials in an encrypted vault and injects them at connection time, and every session it brokers is recorded and replayable. It fronts SSH and SFTP, RDP and VNC, databases, and Kubernetes, so one gateway covers the estate.

The cloudimg image ships the free and open source, GPL-3.0 licensed JumpServer Community Edition, run the officially supported way as the upstream containers orchestrated by Docker Compose under systemd. All images are pinned by digest and captured into the VM, so your instance starts without pulling anything. The enterprise XPack components are not present and not enabled — this is the Community Edition, end to end.

Because JumpServer is itself the thing that guards your credentials, nothing ships with a known secret. A unique SECRET_KEY (the master key that encrypts every vaulted credential), a unique component BOOTSTRAP_TOKEN, a unique database password, a unique cache password, a unique administrator password and a unique API token are all generated on your VM on first boot, before the port is reachable. Backed by 24/7 cloudimg support.

JumpServer is a trademark of its respective owner. This image is produced by cloudimg and is not affiliated with, endorsed by, or sponsored by JumpServer. It ships the free and open source GPL-3.0 Community Edition software, unmodified.

The JumpServer console dashboard after signing in with the per VM administrator credentials

What is included:

  • JumpServer Community Edition v4.10.17 — core, celery, koko, lion, chen and web, each pinned by image digest
  • koko, the SSH and SFTP character gateway, published on port 2222
  • lion for browser based RDP and VNC, and chen for the browser based database client
  • A bundled PostgreSQL 16 database, pinned by digest, reachable only inside a private Docker network and never published to a host port
  • Valkey 8.1 (BSD-3-Clause) as the cache, in place of upstream's source-available Redis, likewise never published to a host port
  • A dedicated 100 GiB data disk mounted at /var/lib/jumpserver, holding the database, the vault contents and every session recording
  • jumpserver.service and jumpserver-firstboot.service as systemd units, enabled and active on boot
  • A unique SECRET_KEY, BOOTSTRAP_TOKEN, database password, cache password, admin password and API token generated per VM on first boot, never baked into the image
  • A clean, empty database on first boot: no default login, no shipped secret, no prior data
  • 24/7 cloudimg support

Prerequisites

An active Azure subscription, an SSH key pair, and a VNet plus subnet in the target region. JumpServer runs a multi container stack, so Standard_B4ms (4 vCPU / 16 GiB RAM) is the recommended starting size; upstream's own floor is 4 cores and 8 GiB. NSG inbound: allow 22/tcp from your management network for host administration, 80/tcp for the web console, and 2222/tcp for the SSH bastion gateway. The console binds to the address the VM resolves on first boot; for production, put it behind your own domain and a real certificate (see the final section).

Step 1 - Deploy from the Azure Marketplace

Sign in to the Azure Portal, choose Create a resource, search the Marketplace for JumpServer by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size (Standard_B4ms or larger); under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22) and HTTP (80). Then Review + create and Create. Add an inbound rule for 2222/tcp once the VM exists so operators can reach the SSH gateway.

Step 2 - Deploy from the Azure CLI

az vm create \
  --resource-group <your-rg> \
  --name jumpserver \
  --image <marketplace-image-urn> \
  --size Standard_B4ms \
  --admin-username azureuser \
  --ssh-key-values ~/.ssh/id_ed25519.pub \
  --vnet-name <your-vnet> --subnet <your-subnet> \
  --public-ip-sku Standard

az vm open-port --resource-group <your-rg> --name jumpserver --port 80   --priority 1010
az vm open-port --resource-group <your-rg> --name jumpserver --port 2222 --priority 1020

Step 3 - Connect to your VM

ssh azureuser@<vm-public-ip>

Step 4 - Confirm the services are running

First boot generates every secret and brings the stack up. jumpserver-firstboot.service runs once, then jumpserver.service keeps the Compose project running.

sudo systemctl is-active jumpserver.service docker.service
sudo docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'

Expected output:

active
active
NAMES            IMAGE                         STATUS
jms_web          jumpserver/web:v4.10.17-ce    Up 2 minutes (healthy)
jms_lion         jumpserver/lion:v4.10.17-ce   Up 2 minutes (healthy)
jms_celery       jumpserver/core:v4.10.17-ce   Up 2 minutes
jms_koko         jumpserver/koko:v4.10.17-ce   Up 2 minutes (healthy)
jms_chen         jumpserver/chen:v4.10.17-ce   Up 2 minutes (healthy)
jms_core         jumpserver/core:v4.10.17-ce   Up 2 minutes (healthy)
jms_postgresql   postgres:16.10-bookworm       Up 3 minutes (healthy)
jms_redis        valkey/valkey:8.1-bookworm    Up 3 minutes (healthy)

The JumpServer Community Edition containers running under systemd and Docker

The core API and the SSH gateway both report their own health endpoint:

curl -s -o /dev/null -w 'core: HTTP %{http_code}\n' http://127.0.0.1/api/health/
curl -s -o /dev/null -w 'koko: HTTP %{http_code}\n' http://127.0.0.1/koko/health/

Expected output:

core: HTTP 200
koko: HTTP 200

koko reporting 200 is the one to watch: it is the component that actually brokers SSH sessions, so a healthy koko means the bastion can do its job.

Step 5 - Secure by default: no login ships with the image

The API refuses unauthenticated calls, and the datastores are not reachable from the host at all — PostgreSQL and Valkey publish no host port and live only on the private container network.

curl -s -o /dev/null -w 'no token: HTTP %{http_code}\n' http://127.0.0.1/api/v1/users/users/
sudo ss -ltn | awk 'NR==1 || /:80 |:2222 |:5432 |:6379 /'
sudo docker ps --format '{{.Names}}  {{.Ports}}' | grep -E 'postgresql|redis'

Expected output — only 80 (console) and 2222 (SSH gateway) are bound on a host interface, and neither datastore maps a port:

no token: HTTP 401
State  Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0      4096         0.0.0.0:2222      0.0.0.0:*
LISTEN 0      4096         0.0.0.0:80        0.0.0.0:*
jms_postgresql  5432/tcp
jms_redis       6379/tcp

The API refuses requests with no token, and neither PostgreSQL nor Valkey is reachable from the host

The upstream default administrator password does not work either — first boot replaces it before the console is reachable:

curl -s -o /dev/null -w "default password: HTTP %{http_code}\n" \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"ChangeMe"}' \
  http://127.0.0.1/api/v1/authentication/auth/

Expected output (400 — the credential is rejected, no token is issued):

default password: HTTP 400

Step 6 - Your per VM credentials

First boot writes the generated credentials to a root only file:

sudo ls -l /root/jumpserver-credentials.txt
sudo sed -E 's/(pass|PASSWORD|TOKEN)=.*/\1=********/' /root/jumpserver-credentials.txt | head -14

Expected output (secrets masked here; the real file holds the values):

-rw------- 1 root root 1587 Jul 19 06:11 /root/jumpserver-credentials.txt
# JumpServer — generated on first boot by jumpserver-firstboot.service
# These credentials are unique to this VM. Store them somewhere safe.

jumpserver.url=http://10.0.0.22/
jumpserver.admin.user=admin
jumpserver.admin.pass=********
# Canonical keys consumed by the Marketplace smoke verifier + guide substitution:
JUMPSERVER_ADMIN_PASSWORD=********
JUMPSERVER_API_TOKEN=********

The per VM access notes, with the administrator password and API token masked

The API token is a JumpServer private token for the admin user. It keeps working after you change the console password, so automation does not break:

TOKEN=$(sudo grep '^JUMPSERVER_API_TOKEN=' /root/jumpserver-credentials.txt | cut -d= -f2-)
curl -s -o /dev/null -w 'with token: HTTP %{http_code}\n' \
  -H "Authorization: Token ${TOKEN}" http://127.0.0.1/api/v1/users/users/

Expected output:

with token: HTTP 200

Step 7 - Sign in to the console

Browse to http://<vm-public-ip>/ and sign in as admin with the password from the credentials file. JumpServer requires you to choose a new password at your first interactive sign in — do that, and store the new one safely, because the credentials file is not updated when you change it.

The JumpServer sign in screen, which gates the console until you enter the per VM administrator credentials

Once in, the Console dashboard shows live session counts, user and asset totals, and recent activity.

The JumpServer Console dashboard, signed in as the administrator

Step 8 - Add an asset and vault its credential

An asset is a machine you want to reach. An account is the credential on that machine — and JumpServer stores it encrypted under this VM's SECRET_KEY, so your operators never need to hold it.

In the Console, go to Assets, choose Create, and add a host: give it a name, its address, and the Linux platform. Then add an account to it — a username plus either a password or an SSH private key — and JumpServer vaults the secret. Finally, under Authorizations, grant a user or group permission to that asset with that account.

The asset list, showing a Linux host with one vaulted account

The same thing over the API, using the token from Step 6:

TOKEN=$(sudo grep '^JUMPSERVER_API_TOKEN=' /root/jumpserver-credentials.txt | cut -d= -f2-)
curl -s -H "Authorization: Token ${TOKEN}" \
  'http://127.0.0.1/api/v1/assets/hosts/?limit=5' \
  | head -c 400; echo

This returns the JSON list of hosts JumpServer currently manages.

Step 9 - Connect through the bastion

Operators do not SSH to the target. They SSH to JumpServer, on port 2222, and JumpServer opens the session for them using the vaulted credential. Running ssh -p 2222 <jumpserver-user>@<vm-ip> gives an interactive menu of everything that user is permitted to reach. To go straight to one target, use koko's direct connect form — the JumpServer user, the account, the asset address, then the JumpServer host:

ssh -p 2222 '<jumpserver-user>@<account>@<asset-address>@<vm-ip>'

The session below was brokered through the gateway: the shell that answers is the target's, reached with a credential the operator never saw.

A real SSH session brokered through the koko bastion gateway to a managed Linux host

Step 10 - Every session is recorded

That is the point of a bastion: the audit trail. Go to the Audits workspace, then Asset sessions, and open Historical sessions. Each brokered session is listed with the JumpServer user, the target, the account used, the protocol and the start time — and Playback replays exactly what happened.

The session audit list, with the recorded bastion session available for playback and download

Recordings, the vault and the database all live on the dedicated data disk:

mountpoint -q /var/lib/jumpserver && echo "/var/lib/jumpserver is a dedicated volume"
df -h /var/lib/jumpserver | tail -1

Expected output:

/var/lib/jumpserver is a dedicated volume
/dev/sda         98G  8.5G   85G   10% /var/lib/jumpserver

Keeping this on its own disk means you can grow or snapshot your audit history independently of the OS disk.

Step 11 - Production: your own domain with TLS

The image serves the console over HTTP on port 80 so it works the moment the VM boots. For production, terminate TLS in front of it: point a DNS name at the VM, put a reverse proxy or an Azure Application Gateway in front with a certificate for that name, and restrict 80/tcp to that proxy. Keep 2222/tcp reachable for the SSH gateway, and restrict 22/tcp to your management network — operators should be using the bastion, not the host.

Back up /var/lib/jumpserver (database, vault and recordings) and keep /root/jumpserver-credentials.txt somewhere safe. The SECRET_KEY in /opt/jumpserver/config.txt decrypts every vaulted credential: without it, a restored database is unreadable.

Support

cloudimg provides 24/7 support for this image. Contact us at support@cloudimg.co.uk or visit www.cloudimg.co.uk.