Repository navigation
Releases: Loophole-LLC/Bastillion
Release list
6.0.0
Security
- High: SSH sessions never verified host keys. Every connection was opened with
StrictHostKeyChecking=no, which does not mean "warn" — the key a host presents was never compared to anything. A bastion is the worst place for that: it hands the host the application private key, and on an authentication fallback the user's own password or key passphrase, so anything that could answer on a managed system's address once could collect credentials for it. Host keys are now verified against a newhost_keytable, with the mode set byhostKeyVerification:accept-new(default) records a key on first sight and refuses the connection if it ever changes,strictadditionally requires a manager to approve each new host key, andoffrestores the old behavior. - Request parameters could rewrite the application SSH public key shown to every user.
BaseKontrollerbound parameters into static fields, andUserSettingsKtrlholds the application public key in a static@Modelfield — so?publicKey=...rewrote the key displayed on the page that tells users to install it in theirauthorized_keys, for everyone.?themeMap['x']=ylikewise grew a shared map without bound. Reachable by any authenticated user. The binder now refuses static fields and the palettes are unmodifiable. - Any third-party page could end every signed-in user's session.
CSRFFilterinvalidated the session on a token mismatch, an absent token failed identically, and the filter covers page GETs — so<img src="https://host/admin/menu.html">on an unrelated site logged out whoever loaded it. No token knowledge and nothing forged. The request is still refused; the session now survives being targeted. - The user list could be sorted by its password column.
SortedSetfiltered characters rather than column names and the result was concatenated intoorder by, so?sortedSet.orderByField=passwordleaked the relative order of every stored hash, with no quoting or metacharacter needed. Each query now declares its sortable columns and an unrecognized field drops the clause. - Fabricated "Authentication Success" records could be written to the login audit log. The log is one record per line, assembled from the submitted username, which needs no credentials to set — a newline in it appended records of the attacker's choosing.
AuditLogUtil.safenow strips control characters and caps field length. - A downloaded private key could be unencrypted while the UI said otherwise.
rewrapWithOpenSSHKeygendiscardedssh-keygen's exit status and returned the temp file regardless; on failuressh-keygenleaves that file untouched, so the method returned the plaintext PEM it was handed. It now checks the exit status and verifies the result really is encrypted by reading theopenssh-key-v1cipher name, drains output before waiting, closes the child's stdin, and applies a timeout. - A key name could inject HTTP response headers. Key names are user-supplied and went into
Content-Dispositionunescaped on both the private key and certificate downloads. Both now derive the filename through one sanitizer. authorized_keyswas rewritten through a shell command.addPubKeybuiltecho '<keys>' > fileand interpolated the host's existing file contents unvalidated, so an apostrophe already in that file — legal, and common in a key comment — closed the quoting. Replaced with SFTP: no shell on either end, so the bug class is gone rather than filtered.- The login throttle could be filled with junk to disable it.
getClientIPAddressused the whole forwarded-for header as the throttle key, but each hop appends to it, so a client-varied prefix made every request its own entry. It now takes the first address and requires it to parse as an IP literal, and the map is bounded, sweeping expired windows and evicting the oldest in one guarded batch. - Removed
X-XSS-Protection. It drove the XSS Auditor, which Chrome and Edge removed and Firefox and Safari never shipped, and while live1; mode=blockwas itself usable to disable scripts selectively and to leak cross-site information. HSTS moves toSecurityHeadersFilter, wheremax-age,includeSubDomainsandpreloadare configurable — the latter two off by default, because a browser that cached them refuses plain HTTP to those names for the whole max-age. - Public key lookups behind the download endpoints now scope ownership in SQL rather than after the fact, and certificate serial numbers fail loudly instead of restarting at 1 when the authority row is missing.
- Two documented environment variables were silently ignored. The camelCase-to-
SCREAMING_SNAKE_CASEconversion keeps a run of capitals glued to the word after it, sodefaultSSHPassphraseresolved only asDEFAULT_SSHPASSPHRASEandresetApplicationSSHKeyonly asRESET_APPLICATION_SSHKEY— while the README told operators to exportDEFAULT_SSH_PASSPHRASEandRESET_APPLICATION_SSH_KEY. Anyone following the documentation had their SSH key passphrase quietly read from the properties file instead, and their key reset quietly not happen. Both spellings are now accepted, for every property.
Added
- An SSH certificate authority. Bastillion can sign short-lived OpenSSH certificates instead of relying solely on distributed
authorized_keys, enabled withsshCertificateAuth. The CA private key is generated on first start and never leaves the database — certificates are built and signed in-process rather than by shelling out tossh-keygen -s.- Sessions to managed systems authenticate with a certificate carrying the Bastillion username as its key id, so the target host's own logs name the person behind the connection. Default lifetime 5 minutes (
sshCertificateValiditySeconds). - Users can download a certificate for their own registered key and use their own SSH client directly, no longer only the browser terminal. Default lifetime 8 hours (
sshUserCertificateValiditySeconds). - Host certificates are trusted by registering a host CA, published to the verifier as
@cert-authority, with revoked keys published as@revoked. - The CA public key is shown and downloadable from user settings, for installing as
TrustedUserCAKeyson managed hosts. - Each system has a Test cert action that opens a throwaway connection with a certificate and reports whether the host accepted it, without touching the system's recorded status.
- Sessions to managed systems authenticate with a certificate carrying the Bastillion username as its key id, so the target host's own logs name the person behind the connection. Default lifetime 5 minutes (
- A Host Keys screen, listing every key Bastillion has seen with its fingerprint and when it was first trusted, with approve, revoke and forget actions, plus a navigation badge counting keys currently blocking connections.
keyManagement, replacing the old booleankeyManagementEnabledwith three modes:manage(Bastillion ownsauthorized_keys, as before),append(add Bastillion's key and leave everything else on the host alone), andoff(never write toauthorized_keysat all, for installs authenticating purely by certificate).- An Auth column on the systems screen, showing whether the host accepted a certificate, a key, or a password on the last connection — read back from what the host actually accepted, not from what was offered.
- A
HOSTKEYFAILstatus distinct from a generic failure, since a refused host key needs a specific human decision rather than looking like a dead port or a bad password. - Copy-to-clipboard on the public key and CA fields, and a startup warning when a certificate lifetime is long enough to outlive revocation.
Changed
authorized_keysis written over SFTP via a staged write and a rename, rather than streamed over the top of the existing file. A connection lost part way used to leave a half-written or empty file — and inmanagemode that file is rewritten unattended on every host by the refresh timer, so losing it took the application key with it and locked everyone out of that host. The host now has either the old file or the new one, and the file always ends with a newline so anything else appending to it starts its own line.- With certificates enabled, the application key is offered as a second identity behind the certificate. Offering the certificate alone cut off every host that had not had
TrustedUserCAKeysconfigured yet — including hosts with the key already in theirauthorized_keys— which stranded the operator, the refresh timer being how Bastillion reaches a host to manage it in the first place. Rollout is now incremental: a host takes the certificate once it trusts the CA and keeps working on its key until then. - The RSA default key length is 4096 rather than falling back to a short length when
sshKeyLengthis unset or not valid for the key type.
Fixed
- The certificate download refused every request,
getPublicKey(id)never having populated the owning user id it was checked against. - A failure with no message rendered as a literal
Error: nullin the systems screen's dialogs. - Distributing a user's key no longer reports success when the write was skipped, and the application key being uncertifiable is now stated rather than silently falling back.
Dependencies
com.h2database:h22.4.240 → 2.5.252org.bouncycastle:bcprov-jdk18on1.85.2 → 1.86org.apache.commons:commons-lang33.20.0 → 3.21.0org.eclipse.jetty:*12.1.12 → 12.1.14org.slf4j:slf4j-api2.0.18 → 2.0.20
Upgrade note: the new tables and columns are created on first start and need no action — upgrading from 5.2.x has been tested end to end. Host key verification defaults to accept-new, so existing systems keep connecting and their keys are recorded as they are first seen; set hostKeyVerification=strict to require approval instead, or off to keep the pre-6.0.0 behavior. The certificate authority is off until sshCertificateAuth is set. keyManagementEnabled=true/false is still honored and maps to manage/append.
...
5.2.1
Security
- High: SAML/LDAP login permanently deleted other users' SSH keys. Profile-sync on every SAML/LDAP login ran an unscoped
DELETE FROM user_map WHERE profile_id=?, wiping every other user's profile membership for that profile instead of just the logging-in user's. The subsequentdeleteUnassignedKeysByProfile()cleanup then treated those users as unassigned and permanently deleted their registered SSH public keys, with no automatic recovery. Any authenticated SAML/LDAP login — even one with a low-privilege IdP role — could lock every other user (including admins) out of their assigned systems and destroy their keys.UserProfileDB.assignProfilesToUser()andassignProfileToUser()now scope both deletes toprofile_id=? and user_id=?. Reported by @tonghuaroot (GHSA-577v-6px6-m424).
Dependencies
com.github.mwiede:jsch2.28.6 → 2.28.7github/codeql-action4.37.7 → 4.37.9actions/setup-java5 → 6
Upgrade note: Critical for any deployment using SAML or LDAP authentication — upgrade immediately. Profile assignments for affected users re-sync automatically on their next login, but deleted SSH keys do not come back on their own; a manager must manually re-register them.
5.2.0
Security
- Critical: unauthenticated admin bypass. Route and role checks matched on the raw request URI with a substring
contains()instead of the container-normalized servlet path. A crafted URI like/x;/manage/viewUsers.ktrlcould reach any/manage/*or/admin/*controller — including full account creation — with zero authentication. Now matched ongetServletPath()with an exact match. - SSH terminal WebSocket had no real auth gate. The
/admin/terms.wsupgrade request bypasses the servlet-containerAuthFilterentirely; it was only "failing safe" by accident, via an uncaught NPE.SecureShellWS.onOpennow explicitly validates a live admin auth token before allowing the connection. - Added login throttling. Brute-force login attempts were previously unbounded. Now rate-limited per client IP (configurable via
maxLoginAttemptsPerIP/loginThrottleWindowMinutes, default 10 attempts / 5 minutes) rather than per-account, so an attacker can't lock out a known admin by deliberately failing their password. - Fixed a file upload path traversal.
UploadAndPushKtrl.push()didn't sanitize the uploaded filename before using it to build SFTP push/cleanup paths, allowing a crafted../../../etc/passwd-style value to read or delete arbitrary local files. - Hardened DB connection handling. All 12 DAO classes moved to try-with-resources, closing two real connection leaks: one in the terminal output-polling loop (a leak every 25ms on exception) and one on every failed external-auth attempt.
Added
- SAML 2.0 SSO, alongside the existing LDAP/JAAS auth — works with Entra ID, Okta, ADFS, or any SAML 2.0 IdP.
- Self-signed SP signing/encryption keys generated automatically (no config needed); every outgoing
AuthnRequestis signed. - New
/saml/metadataendpoint for IdP-side setup via URL import. - Optional encrypted-assertion support (
samlWantEncryptedAssertions). - Config is entirely env-var/file driven — no new admin UI, matching the LDAP pattern.
- SAML role/group claims map onto Bastillion profiles the same way LDAP groups already do.
- Self-signed SP signing/encryption keys generated automatically (no config needed); every outgoing
Changed
- Modernized UI throughout: new logo/branding (SVG), responsive navbar, consistent button styling across admin and manage views.
- Terminal polish: fixed resizing, cursor/clipboard input, selection clearing after copy, and a duplicate-session output race.
- Fixed zsh sessions leaking
PROMPT_EOL_MARKmarkers into terminal/audit output. - Raised the free system limit.
- Refreshed all product screenshots.
- Upgraded embedded Jetty from 11 (Jakarta EE 9) to 12 (Jakarta EE 10).
Dependencies
xmlsec2.2.6 → 4.0.4 (also closes CVE-2023-44483, pinned similarly to the existingmina-corepin)jakarta.servlet-api6.0.0 → 6.1.0commons-codec1.22.0 → 1.22.1com.github.mwiede:jsch2.28.4 → 2.28.6org.bouncycastle:bcprov-jdk18on1.85 → 1.85.2org.junit.jupiter:junit-jupiter6.1.2 → 6.1.3maven-clean-plugin3.4.1 → 3.5.0frontend-maven-plugin2.0.1 → 2.0.2grunt1.6.2 → 1.6.3,brace-expansion(npm, transitive)github/codeql-action4 → 4.37.7
Upgrade note: if you run behind LDAP/JAAS today, this release is a drop-in upgrade — no config changes required. SAML is opt-in via new env vars. Given the auth-bypass and WS-terminal-auth fixes above, upgrading promptly is strongly recommended for all deployments.
5.1.0
🎬 Session audit & replay — now a first-class feature, on by default
Every terminal session is now recorded and can be replayed from Audit Sessions in the
UI — who ran what, where, and when, side by side for sessions that spanned multiple hosts,
with a text filter to jump straight to the lines that matter.
This feature existed behind enableInternalAudit but was experimental: reviewing a session
loaded its entire output into the browser in one shot, which hung the tab on any session
with real output. That's fixed properly:
- Output streams from the database to the browser and renders incrementally — sessions
with hundreds of megabytes of terminal output replay without breaking a sweat - Fixed a server-side cleanup loop that was quadratic in session size and could spin a
request thread forever when output contained a backspace at the start of a line - Full ANSI/VT100 control sequences (colors, cursor movement, screen modes, window titles)
are now stripped from replayed output — no more[?1049hnoise fromtopand friends
Practically every compliance framework has a privileged-access audit-trail requirement
somewhere — PCI DSS, HIPAA, SOC 2, ISO 27001 — and this checks that box without a
commercial PAM product.
Upgrade note: enableInternalAudit now defaults to true on fresh installs; existing
installs keep whatever their persisted config says. Retention is deleteAuditLogAfter
(90 days by default); disable recording entirely with ENABLE_INTERNAL_AUDIT=false. If you
scripted against the old getJSONTermOutputForSession.ktrl endpoint, it's replaced by the
streaming streamTermOutputForSession.ktrl (plain text, one line per record).
🔒 Hardening
- CSRF token comparison is now constant-time
📦 Dependencies
- Bouncy Castle
bcprov-jdk18on1.84 → 1.85 - JUnit Jupiter 6.1.1 → 6.1.2
5.0.0
Highlights
- Licensing model — Bastillion now runs unlicensed at up to 5 registered systems.
Paid tiers (Starter/Team/Business) raise the cap; a.licfile is supplied via the
LICENSE_KEYenv var orlicenseKeyinBastillionConfig.properties. New
LicenseUtilvalidates the license, enforces the system cap, and surfaces
licensee/expiry info (with a 90-day pre-expiry warning) under Settings. - Self-contained jar — the app now runs as
java -jarwith an embedded Jetty
server and HTTPS out of the box (newio.bastillion.Main), instead of requiring a
separate servlet container distribution. - Upgraded to Java 21 and Jakarta EE 11, with dependency updates across the board.
- Full Ed25519 (default) and Ed448 SSH key support.
- v4 → v5 migration tool (
tools/migrate) — a standalone export/import utility
that moves users, systems, profiles, scripts, public keys, audit logs, and the
Bastillion application SSH key/identity from a v4 H2 database into a fresh v5
instance, re-encrypting secrets under the new instance's keystore. See
tools/migrate/README.mdfor the full procedure.
Security
- New
SecurityHeadersFilteradds hardened response headers app-wide. - New
CSRFFilter(part of the bundled MVC framework) protects state-changing
requests. - Session cookies now set
securein addition to the existinghttp-only. EncryptionUtilandKeyStoreUtilwere reworked;AuthDB/password verification
now supports the newer PBKDF2 hash format alongside the legacy single-round
SHA-256 formats (including v4's own concatenation scheme), so users can migrate
and log in with existing passwords unchanged.
Internal / build
- The previously external
io.bastillion:lmvcframework dependency is now merged
directly into the codebase asloophole.mvc, so the whole app builds from one
pom.xml. - Added unit test coverage:
AppConfigTest,EncryptionUtilTest,LicenseUtilTest,
and new tests for theloophole.mvcframework (dispatcher, base controller, CSRF
and security filters). - CI (
.github/workflows/github-build.yml) now runs the build/test on every push
and pull request, in addition to the existing hourly schedule — previously it
only ran on cron, so commits weren't verified until the next scheduled run. - README overhauled with an updated feature walkthrough, licensing section, and
screenshots.
Upgrading from v4
Use tools/migrate to bring your data forward — see tools/migrate/README.md.
Password hashes migrate as-is; existing users can log in immediately without a
reset.
4.0.1
🚀 Bastillion Release Notes
What’s New
- ✅ Upgraded to Java 21 and Jakarta EE 11 — modern language features, improved performance, and long-term support.
- 🔐 Added full support for Ed25519 and Ed448 SSH key generation, with Ed25519 now set as the default key type.
- 📦 Updated all libraries and dependencies to their latest stable releases for enhanced security, stability, and compatibility.
Migration Guide (from v3.10.00)
To migrate from a previous installation:
-
Ensure Java 21 is installed
Bastillion now requires Java 21. Confirm it's installed and available in your environment:java --version
On Ubuntu, you can install it via:
sudo apt update sudo apt install -y openjdk‑21‑jdk
-
Backup your existing database
The H2 data store is usually named:bastillion.h2.db(Older versions may use
keybox.h2.db.) -
Copy the following files and directories from your old installation:
jetty/bastillion/WEB-INF/classes/keydb/→ copy the entire folder and its contentsjetty/bastillion/WEB-INF/classes/bastillion.jceksjetty/bastillion/WEB-INF/classes/BastillionConfig.properties
-
Review and migrate configuration files as needed:
jaas.conflog4j2.xmlkeystorejetty-ssl.xmljetty-http.xml
-
Restart Bastillion
Confirm that the application is connected to its previous database and that all services start cleanly under Jetty.
Additional Notes
- Ed25519 keys (256-bit) are now the default for improved speed and security.
- Ed448 keys (448-bit) are supported for environments requiring extended cryptographic strength.
- Migration preserves existing authorized keys and stored credentials if configuration paths are copied correctly.
3.15.00
Update 3rd party dependencies to latest including Jetty.
To migrate from >= 3.10.00
- Backup previous bastillion.h2.db data store (possibly named keybox.h2.db)
- Copy old jetty/bastillion/WEB-INF/classes/keydb folder (and it's contents) to the jetty/bastillion/WEB-INF/classes directory of the new installation.
- Copy old jetty/bastillion/WEB-INF/classes/bastillion.jceks to the /jetty/bastillion/WEB-INF/classes directory of the new installation.
- Copy old jetty/bastillion/WEB-INF/classes/BastillionConfig.properties to the /jetty/bastillion/WEB-INF/classes directory of the new installation.
- Run the bastillion-upgrade-4.00.01.jar on the database copied to the new installation
java -jar bastillion-upgrade-4.00.01.jar /jetty/bastillion/WEB-INF/classes/BastillionConfig.properties
- Adjust settings or copy as needed for the jaas.conf, log4j2.xml, keystore, jetty-ssl.xml and jetty-http.xml
3.14.0
New look and feel - Upgraded to Bootstrap v5, the latest Jetty, and updated all 3rd party libraries to the latest.
To migrate from >= 3.10.00
- Backup previous bastillion.h2.db data store (possibly named keybox.h2.db)
- Copy old jetty/bastillion/WEB-INF/classes/keydb folder (and it's contents) to the jetty/bastillion/WEB-INF/classes directory of the new installation.
- Copy old jetty/bastillion/WEB-INF/classes/bastillion.jceks to the /jetty/bastillion/WEB-INF/classes directory of the new installation.
- Copy old jetty/bastillion/WEB-INF/classes/BastillionConfig.properties to the /jetty/bastillion/WEB-INF/classes directory of the new installation.
- Run the bastillion-upgrade-4.00.01.jar on the database copied to the new installation
java -jar bastillion-upgrade-4.00.01.jar /jetty/bastillion/WEB-INF/classes/BastillionConfig.properties
- Adjust settings or copy as needed for the jaas.conf, log4j2.xml, keystore, jetty-ssl.xml and jetty-http.xml
3.13.00
Update h2 to the latest version fix issue upgrading due to incompatibilities with H2 v2 - #417
To migrate from >= 3.10.00
- Backup previous bastillion.h2.db data store (possibly named keybox.h2.db)
- Copy old jetty/bastillion/WEB-INF/classes/keydb folder (and it's contents) to the jetty/bastillion/WEB-INF/classes directory of the new installation.
- Copy old jetty/bastillion/WEB-INF/classes/bastillion.jceks to the /jetty/bastillion/WEB-INF/classes directory of the new installation.
- Copy old jetty/bastillion/WEB-INF/classes/BastillionConfig.properties to the /jetty/bastillion/WEB-INF/classes directory of the new installation.
- Run the bastillion-upgrade-4.00.01.jar on the database copied to the new installation
java -jar bastillion-upgrade-4.00.01.jar /jetty/bastillion/WEB-INF/classes/BastillionConfig.properties
- Adjust settings or copy as needed for the jaas.conf, log4j2.xml, keystore, jetty-ssl.xml and jetty-http.xml
3.12.02
Update jetty, jsch, and h2 to the latest version
To migrate from 3.10.00
- Backup previous bastillion.h2.db data store (possibly named keybox.h2.db)
- Copy old jetty/bastillion/WEB-INF/classes/keydb folder (and it's contents) to the jetty/bastillion/WEB-INF/classes directory of the new installation.
- Copy old jetty/bastillion/WEB-INF/classes/bastillion.jceks to the /jetty/bastillion/WEB-INF/classes directory of the new installation.
- Copy old jetty/bastillion/WEB-INF/classes/BastillionConfig.properties to the /jetty/bastillion/WEB-INF/classes directory of the new installation.
- Run the bastillion-upgrade-4.00.00.jar on the database copied to the new installation
java -jar bastillion-upgrade-4.00.00.jar /jetty/bastillion/WEB-INF/classes/BastillionConfig.properties
- Adjust settings or copy as needed for the jaas.conf, log4j2.xml, keystore, jetty-ssl.xml and jetty-http.xml