A modern, web-based SSH console and SSH key management tool.
Bastillion gives you a clean, browser-based way to manage SSH access across all your systems — like a bastion host with a friendly dashboard. It does two things:
-
Web-based SSH terminal — once a host is registered, authorized users can open one or more live terminal sessions to it directly from the browser, with commands optionally broadcast across every open session at once (think tmux's synchronized panes, but for a fleet of remote hosts instead of local panes).
-
SSH key management — Bastillion holds its own SSH keypair and pushes/rotates public keys across the hosts you register, so individual users never need to hold or manage long-lived keys to those systems themselves.
- Log in with 2-factor authentication (Authy or Google Authenticator)
- Manage and distribute SSH public keys, and disable/rotate them centrally
- Launch secure multi-session web shells and share commands across sessions
- Record every session and replay it on demand — audit-ready evidence for any compliance framework
- Group systems into Profiles and control exactly who can reach what
- Save and re-run Composite Scripts across a whole fleet at once
- Stack TLS/SSL over SSH for extra protection
Three real, independent SSH sessions — one command, typed once, run everywhere.
- How It Works
- What's New
- Licensing
- Installation Options
- Prerequisites
- Download and Run
- Build from Source
- TLS / HTTPS
- Configuration
- More Screenshots
- License
Bastillion sits between your users and the systems they need to reach, acting as a trusted third party rather than a simple password vault. Here's the whole lifecycle, end to end.
On first startup, before anything else, Bastillion generates an Ed25519 keypair for itself — this is the one key that ever gets pushed to your hosts. It's shown in the console output and always visible under Settings.
An admin adds a host under Manage → Systems (user, host, port, and the path to that
host's authorized_keys file). Bastillion authenticates once with a password or
passphrase you supply, then pushes its own public key into that host's authorized_keys.
From then on it connects using that key — no stored passwords, ever. Status flips to
Success the moment the key is in place.
Systems get grouped into named Profiles — think "Production," "Staging," "Database Tier." Users are then linked to profiles under Manage → Users, which is the only thing that controls who can reach what. Revoke a profile assignment and that access is gone immediately, no key rotation needed.
Assigned users open Secure Shell → Terminals, pick one or more systems, and get live, resizable, xterm-based terminals in the browser, side by side. Type once, and it goes to every terminal marked active — the same keystroke, the same command, the same output shape, across as many hosts as you selected.
Because every host trusts the same application key (not one key per user), disabling it once under Manage SSH Keys revokes access everywhere immediately — no need to touch target systems by hand, no hunting down which server has which stale key.
Everything typed and every byte returned in those terminals is recorded automatically. Managers open Audit Sessions, filter by user or system, and replay any session — side by side for sessions that spanned multiple hosts, with a text filter to jump straight to the lines that matter. Output streams into the page as it loads, so even a session that dumped hundreds of megabytes of logs replays without breaking a sweat.
If you need to show an auditor who ran what, where, and when — this is that evidence,
captured out of the box. Practically every compliance framework has a privileged-access
audit-trail requirement somewhere (PCI DSS, HIPAA, SOC 2, ISO 27001 — pick yours), and
this checks that box without a commercial PAM product. Sessions are kept for 90 days by
default (deleteAuditLogAfter), and recording can be switched off with
ENABLE_INTERNAL_AUDIT=false — see Auditing.
- Licensing — free at up to 5 systems, paid tiers available at loophole.company/pricing.html (see Licensing below)
- Session audit & replay, on by default — every terminal session is recorded and can be replayed under Audit Sessions, streamed to the browser so even huge sessions load instantly
- Runs as a self-contained jar (
java -jar) with HTTPS out of the box — see Download and Run - Upgraded to Java 21 and Jakarta EE 11
- Full support for Ed25519 (default) and Ed448 SSH keys
- v4 → v5 migration tool to bring over users, systems, keys, and audit logs from an existing instance — see
tools/migrate - Hardened with a CSRF filter and app-wide security headers
Bastillion runs unlicensed at up to 5 registered systems — enough to try it for real before buying. A license raises that cap.
- Buy a license at loophole.company/pricing.html
(Starter/Team/Business — priced by system count). Payment redirects back and downloads a
.licfile automatically. - Open the
.licfile and copy its contents (one line). - Set it via the
LICENSE_KEYenvironment variable:or paste it intoexport LICENSE_KEY=<paste license file contents here>
licenseKeyinBastillionConfig.propertiesinstead — the environment variable takes precedence if both are set. - Restart Bastillion. Settings shows the licensee, system cap, and expiry, with a warning starting 90 days before it expires.
Licenses are annual and don't auto-renew — no card kept on file. Buy again from the same pricing page when you get the expiry warning.
Free: https://github.com/bastillion-io/Bastillion/releases
apt-get install openjdk-21-jdk| Application | Android | iOS |
|---|---|---|
| Authy | Google Play | iTunes |
| Google Authenticator | Google Play | iTunes |
Download the latest jar from Releases:
java -jar bastillion-<version>.jarAccess in browser: https://<server-ip>:8443 — see TLS / HTTPS below for the
self-signed certificate Bastillion generates on first run.
Default credentials:
username: admin
password: changeme
Runs in the foreground; stop with Ctrl+C. For background/daemon operation use whatever your
platform normally uses for a long-running Java process — nohup java -jar ... &, a systemd
unit, a container, etc.
Install Maven 3+:
apt-get install mavenBuild and run (packages a self-contained jar with an embedded Jetty server — see
io.bastillion.Main — and runs it):
mvn package
java -jar target/bastillion-5.0.0-SNAPSHOT.jarOr for local dev without repackaging on every change:
mvn compile exec:javaListens on https://localhost:8443 by default, same as the downloaded release above — see
TLS / HTTPS below for how that certificate gets set up and how to use your
own instead.
Bastillion generates its own self-signed certificate on first startup and serves HTTPS —
nothing to configure. Browsers will show a warning once (it's self-signed, not issued by a
CA); click through it, same as you would for any other self-hosted appliance. The
certificate and its password persist across restarts (keystore/bastillion.p12 under
CONFIG_DIR, password stored the same encrypted way as the database
password).
Use your own CA-signed certificate instead of the self-signed default — e.g. a free one from Let's Encrypt:
-
Issue the certificate with certbot (requires a real DNS name pointing at this host, and port 80 reachable for the HTTP-01 challenge):
sudo certbot certonly --standalone -d bastillion.example.com
This writes
fullchain.pemandprivkey.pemto/etc/letsencrypt/live/bastillion.example.com/. -
Convert the cert/key pair to PKCS12, the keystore format Bastillion expects:
openssl pkcs12 -export \ -in /etc/letsencrypt/live/bastillion.example.com/fullchain.pem \ -inkey /etc/letsencrypt/live/bastillion.example.com/privkey.pem \ -out bastillion.p12 -name bastillion -passout pass:changeit
-
Point Bastillion at it and restart:
export KEYSTORE_PATH=/path/to/bastillion.p12 export KEYSTORE_PASSWORD=changeit
Browsers will now trust the connection with no warning. Let's Encrypt certificates expire every 90 days —
certbot renewfollowed by re-running steps 2–3 (and a restart) keeps it current;certbot renew --deploy-hookcan automate that.
Behind a reverse proxy or load balancer that already terminates TLS (nginx, Cloud Run, etc.) — disable Bastillion's own HTTPS and let it serve plain HTTP instead:
export TLS_ENABLED=falseDefaults to port 8080 in this mode; set PORT to change it.
Every setting below can be set as an environment variable — take the property name and
insert an underscore before each capital letter, then uppercase it: licenseKey →
LICENSE_KEY, dbUser → DB_USER, sshKeyType → SSH_KEY_TYPE. This is the recommended
way to configure Bastillion, especially in containers — no file to mount or bake in.
BastillionConfig.properties still works as a fallback (env vars always win if both are
set), and is where any value Bastillion generates for you at first startup — like a random
DB password — gets persisted. See src/main/resources/BastillionConfig.properties for the
full list of settings and their defaults.
Consolidating everything under one directory (e.g. a single Docker volume mount):
CONFIG_DIR is the one setting to reach for. Everything Bastillion persists —
BastillionConfig.properties, the self-signed TLS keystore (keystore/bastillion.p12), the
H2 database and the SSH host key pair (both under keydb/), and bastillion.jceks — lives
under it by default, so pointing CONFIG_DIR at one place relocates all of it:
export CONFIG_DIR=/data/bastillion/KEYSTORE_PATH and DB_CONNECTION_URL still exist for pointing just one of those at a
different location on its own (a real cert, a remote DB) — see TLS / HTTPS and
the "Database Settings" section below — but neither is needed just to consolidate everything
into CONFIG_DIR.
CONFIG_DIR itself defaults to ./config relative to the working directory. Upgrading an
existing instance that never set it? Older releases stored state directly in the working
directory instead of ./config — Bastillion detects that on first startup with this version
and moves it into ./config (or into CONFIG_DIR, if you've now set one) automatically.
SSH Key Management
# Disable key management (append instead of overwrite)
export KEY_MANAGEMENT_ENABLED=false
# authorized_keys refresh interval in minutes (no refresh for <=0)
export AUTH_KEYS_REFRESH_INTERVAL=120
# Force user key generation and strong passphrases
export FORCE_USER_KEY_GENERATION=falseCustom SSH Key Pair
By default Bastillion generates its own Ed25519 keypair on first startup. To use your own instead, the easiest way is through the UI: Settings → Replace Application SSH Key (manager accounts only) — paste in a private key, public key, and passphrase if it has one, and it takes effect immediately, no restart needed.
authorized_keys on every one of them
first. The Settings page requires an extra confirmation checkbox once you have systems
registered, precisely because of this.
Already have systems registered and need to rotate the key anyway? Pre-stage the new key
through Bastillion itself rather than editing authorized_keys by hand everywhere:
- Set
FORCE_USER_KEY_GENERATION=falseso Manage SSH Keys → Add SSH Key lets you paste an existing public key instead of only generating a new one. - Add the new key there against a profile covering all your systems, and confirm (under
Manage SSH Keys, or each system's status) that it's actually landed everywhere — keep
AUTH_KEYS_REFRESH_INTERVALin mind, since that's what pushes it out. - Only once you're sure it's on every system, replace the application key in Settings.
- Set
FORCE_USER_KEY_GENERATIONback to its previous value, then once you've confirmed the new application key has propagated to every system (again, mindAUTH_KEYS_REFRESH_INTERVAL), remove the key you added in step 2 from Manage SSH Keys — it was only staged there to pre-seedauthorized_keysand isn't needed going forward.
For scripted/headless setups, the same thing can be done via environment variables and a restart instead:
# Regenerate and import SSH keys
export RESET_APPLICATION_SSH_KEY=true
# Private key
export PRIVATE_KEY=/Users/you/.ssh/id_rsa
# Public key
export PUBLIC_KEY=/Users/you/.ssh/id_rsa.pub
# Passphrase (leave blank if none)
export DEFAULT_SSH_PASSPHRASE=myPa$$w0rdOnce registered, you can drop these — the key pair is already stored in the database.
SSH_KEY_TYPE (rsa, ecdsa, ed25519, or ed448) only matters when Bastillion is
generating a fresh key, not when importing one — the type of an imported key is read from
the key itself:
# SSH key type ('rsa', 'ecdsa', 'ed25519', or 'ed448')
# Supported options:
# rsa - Classic, widely compatible (configurable length, default 4096)
# ecdsa - Faster, smaller keys (P-256/384/521 curves)
# ed25519 - Default and recommended (≈ RSA-4096, secure and fast)
# ed448 - Extra-strong (≈ RSA-8192, slower and less supported)
export SSH_KEY_TYPE=ed25519Database Settings
Embedded H2 example:
export DB_USER=bastillion
export DB_PASSWORD=p@$$w0rd!!
export DB_DRIVER=org.h2.Driver
export DB_CONNECTION_URL=jdbc:h2:file:keydb/bastillion;CIPHER=AES;Remote H2 example:
export DB_CONNECTION_URL=jdbc:h2:tcp://<host>:<port>/~/bastillion;CIPHER=AES;External Authentication (LDAP)
Enable external auth:
export JAAS_MODULE=ldap-olConfigure jaas.conf:
ldap-ol {
com.sun.security.auth.module.LdapLoginModule SUFFICIENT
userProvider="ldap://hostname:389/ou=example,dc=bastillion,dc=com"
userFilter="(&(uid={USERNAME})(objectClass=inetOrgPerson))"
authzIdentity="{cn}"
useSSL=false
debug=false;
};
To map LDAP roles to Bastillion profiles:
ldap-ol-with-roles {
org.eclipse.jetty.jaas.spi.LdapLoginModule required
debug="false"
useLdaps="false"
contextFactory="com.sun.jndi.ldap.LdapCtxFactory"
hostname="<SERVER>"
port="389"
bindDn="<BIND-DN>"
bindPassword="<BIND-DN PASSWORD>"
authenticationMethod="simple"
forceBindingLogin="true"
userBaseDn="ou=users,dc=bastillion,dc=com"
userRdnAttribute="uid"
userIdAttribute="uid"
userPasswordAttribute="userPassword"
userObjectClass="inetOrgPerson"
roleBaseDn="ou=groups,dc=bastillion,dc=com"
roleNameAttribute="cn"
roleMemberAttribute="member"
roleObjectClass="groupOfNames";
};
Admins are added upon first login and can be assigned system profiles. Users are synced with profiles when their LDAP role names match Bastillion profiles.
Auditing
Session auditing is enabled by default: terminal output is stored in Bastillion's database
and can be reviewed under Audit Sessions (manager accounts only). Output is streamed to
the browser, so even sessions with very large amounts of terminal output can be replayed.
Audit history is kept for deleteAuditLogAfter days (90 by default). Disable it with:
export ENABLE_INTERNAL_AUDIT=falseThere is also a file-based audit log, disabled by default. Enable it in log4j2.xml by uncommenting:
io.bastillion.manage.util.SystemAuditaudit-appender
https://github.com/bastillion-io/Bastillion/blob/main/src/main/resources/log4j2.xml#L19-L22
Migrating from v4
Upgrading from an old Bastillion v4 install and want to keep your users, systems, profiles,
scripts, and (most importantly) the application's existing SSH keypair instead of starting
over? tools/migrate/ has a standalone migration tool for exactly that — it exports every
table from the old H2 database (decrypting the app-level-encrypted columns with the OLD
instance's keystore) to a JSON file, then imports it into a fresh v5 instance (re-encrypting
with the NEW instance's keystore). Existing users can log in with their current passwords
immediately after — no forced resets.
cd tools/migrate
# 1. Export the old database
./migrate.sh export /opt/Bastillion-jetty/jetty/bastillion/WEB-INF/classes/ ~/bastillion-export.json
# 2. Start the new v5 instance once against the config dir you're migrating into, then
# stop it (Ctrl+C) once it's finished booting - this creates the schema, jceks, and
# default admin user.
cd ../..
java -DCONFIG_DIR=/data/bastillion/ -jar target/bastillion-5.0.0-SNAPSHOT.jar
# 3. Import into the new database (full replace of all 12 tables)
cd tools/migrate
./migrate.sh import /data/bastillion/ ~/bastillion-export.json --yes-replace-all-data
# 4. Delete the export file - it contains decrypted secrets
rm ~/bastillion-export.jsonSee tools/migrate/README.md for the full details — finding your old install's config directory, what exactly gets migrated, and the security notes on the plaintext export file.
Login, 2FA enrollment, the main menu, and a few other screens not already shown in How It Works above.
|
Login — username/password, with an optional OTP access code field for 2FA. |
Two-Factor Setup — scan the QR code with Authy or Google Authenticator. |
|
Main Menu — scoped to what the logged-in user is allowed to see. |
Manage Profiles — group systems into named profiles that control access. |
|
Manage Users — create accounts and assign them a user type; users are then linked to profiles to grant access to specific systems. |
Terminals — pick one or more systems (optionally filtered by profile) to open simultaneously. |
|
Composite Scripts — save a script once and execute it across every selected terminal. |
User Settings — change your password, pick a terminal theme, and view the public key Bastillion uses to authenticate to registered systems. |
Bastillion is available under the Prosperity Public License.
Full list of third-party dependencies and their licenses in 3rdPartyLicenses.md.
Loophole, LLC — Sean Kavanagh













