Add Bolt TLS termination as optional feature

This commit is contained in:
Bart Broere committed 2026-11-04 14:12:10 +01:00
1 parent 5f879fd9a4
commit eb989c9155
12 files changed
+446 -53

No files matched your search

+126 -1
View File
@@ -61,6 +61,38 @@ boltage is configured through environment variables:
| `BOLTAGE_ADVERTISED_BOLT_PORT` | `$BOLTAGE_BOLT_PORT` | Bolt port advertised to the browser; set it when clients reach the Bolt port on another port. |
| `BOLTAGE_ADVERTISED_HOST` | `localhost` | Host reported in `SHOW DATABASES` and to clients that do not send a routing address. |
| `BOLTAGE_LOG_LEVEL` | `INFO` | `DEBUG` logs every generated SQL statement. |
| `BOLTAGE_TLS_CERT` | (unset) | PEM certificate (chain) for TLS, see [TLS](#tls). |
| `BOLTAGE_TLS_KEY` | (unset) | PEM private key of that certificate. |
| `BOLTAGE_BOLT_TLS` | `optional` with a certificate, else `disabled` | TLS on the Bolt port: `disabled`, `optional` or `required`. |
| `BOLTAGE_HTTPS` | `true` with a certificate, else `false` | Serve neo4j-browser over HTTPS instead of HTTP. |
### TLS
With `BOLTAGE_TLS_CERT` and `BOLTAGE_TLS_KEY` set, boltage accepts TLS on the Bolt
port and serves neo4j-browser over HTTPS:
```sh
BOLTAGE_TLS_CERT=/etc/boltage/cert.pem BOLTAGE_TLS_KEY=/etc/boltage/key.pem boltage
```
`BOLTAGE_BOLT_TLS` works like Neo4j's TLS level for Bolt. `optional`, the default
when a certificate is set, accepts TLS and plain connections on the same port, so
`bolt://` and `bolt+s://` both work. `required` only accepts TLS and makes the
discovery document advertise `neo4j+s://`. `disabled` ignores the certificate for
Bolt but still uses it for HTTPS unless `BOLTAGE_HTTPS=false`.
Clients connect with `bolt+s://` or `neo4j+s://` when they trust the certificate, and
with `bolt+ssc://` or `neo4j+ssc://` for a self-signed one. neo4j-browser served over
HTTPS connects to the Bolt port over a secure WebSocket (`wss://`), which needs the
Bolt port to accept TLS and the browser to trust its certificate; with a self-signed
certificate, open `https://localhost:7687/` once and accept it. The certificate is
read at start-up, so restart boltage after renewing it. For the container images,
see [TLS in the containers](#tls-in-the-containers).
The connection from boltage to PostgreSQL uses libpq, which takes its TLS settings
from [its environment
variables](https://www.postgresql.org/docs/current/libpq-envars.html), such as
`PGSSLMODE=verify-full` and `PGSSLROOTCERT`.
## How it works
@@ -375,6 +407,98 @@ is initialised, and pgAdmin comes with the server already registered as "boltage
in `.env` too. Data is kept in the `postgres-data` and `pgadmin-data` volumes;
`podman compose down -v` removes them.
### TLS in the containers
Both images serve plain Bolt and HTTP unless you turn on [TLS](#tls), either with a
self-signed certificate that the container generates, or with your own certificate.
In both cases the Bolt port keeps accepting plain connections as well, unless you
also set `BOLTAGE_BOLT_TLS=required`, and neo4j-browser moves to
<https://localhost:7474/>; set `BOLTAGE_HTTPS=false` to keep serving it over HTTP.
#### A self-signed certificate
Set `BOLTAGE_TLS_SELF_SIGNED=true`, and mount a volume on `/var/lib/boltage` to keep
the certificate when the container is re-created:
```sh
podman run -d --name boltage -e POSTGRES_PASSWORD=secret -e BOLTAGE_TLS_SELF_SIGNED=true \
-p 7474:7474 -p 7687:7687 -p 5050:5050 -p 5432:5432 \
-v boltage-data:/var/lib/postgresql -v boltage-pgadmin:/var/lib/pgadmin \
-v boltage-tls:/var/lib/boltage boltage
```
With compose, put `BOLTAGE_TLS_SELF_SIGNED=true` in `.env`; the certificate is kept
in the `boltage-tls` volume.
The certificate is valid for `localhost`, `127.0.0.1`, `::1` and
`BOLTAGE_ADVERTISED_HOST`. List other host names and addresses, comma-separated, in
`BOLTAGE_TLS_HOSTNAMES`. A new certificate is generated when that list changes and
30 days before the old one expires (it is valid for 825 days). Its SHA-256
fingerprint is logged at every start, so you can compare it with the one your
browser shows.
Drivers connect with `neo4j+ssc://localhost:7687`. In a web browser, accept the
certificate at <https://localhost:7687/> once, then open <https://localhost:7474/> and
accept it there too.
#### Your own certificate
Mount the certificate and key, and point `BOLTAGE_TLS_CERT` and `BOLTAGE_TLS_KEY` at
them:
```sh
podman run -d --name boltage -e POSTGRES_PASSWORD=secret \
-v ./tls:/etc/boltage/tls:ro \
-e BOLTAGE_TLS_CERT=/etc/boltage/tls/cert.pem -e BOLTAGE_TLS_KEY=/etc/boltage/tls/key.pem \
-p 7474:7474 -p 7687:7687 -p 5050:5050 -p 5432:5432 \
-v boltage-data:/var/lib/postgresql -v boltage-pgadmin:/var/lib/pgadmin boltage
```
Both images run boltage as user and group 999, which must be able to read the key.
Keep the key private on the host and give it to that user: with rootless Podman,
`podman unshare chown 999:999 tls/key.pem`, otherwise `sudo chown 999:999 tls/key.pem`;
then `chmod 0400 tls/key.pem`. On hosts with SELinux (Fedora, RHEL), mount with
`:ro,Z` instead of `:ro`.
#### Secrets
Podman secrets keep the key out of the host's file system and set its owner for you:
```sh
podman secret create boltage-tls-cert tls/cert.pem
podman secret create boltage-tls-key tls/key.pem
podman run -d --name boltage -e POSTGRES_PASSWORD=secret \
--secret boltage-tls-cert,uid=999,gid=999,mode=0444 \
--secret boltage-tls-key,uid=999,gid=999,mode=0400 \
-e BOLTAGE_TLS_CERT=/run/secrets/boltage-tls-cert \
-e BOLTAGE_TLS_KEY=/run/secrets/boltage-tls-key \
-p 7474:7474 -p 7687:7687 -p 5050:5050 -p 5432:5432 \
-v boltage-data:/var/lib/postgresql -v boltage-pgadmin:/var/lib/pgadmin boltage
```
With compose, add the secrets in a `compose.override.yaml` next to `compose.yaml`,
which compose reads automatically:
```yaml
services:
boltage:
environment:
BOLTAGE_TLS_CERT: /run/secrets/boltage-tls-cert
BOLTAGE_TLS_KEY: /run/secrets/boltage-tls-key
secrets:
- boltage-tls-cert
- boltage-tls-key
secrets:
boltage-tls-cert:
file: ./tls/cert.pem
boltage-tls-key:
file: ./tls/key.pem
```
Compose mounts file secrets from the host as they are, so the key needs the same
owner as above.
### Build arguments
`AGE_IMAGE` (base of the full image, default `apache/age:release_PG18_1.8.0`),
@@ -396,7 +520,8 @@ addressing it in [`plans/`](plans/README.md):
([plan](plans/03-age-unsupported-features.md)).
4. User and role management, `SHOW TRANSACTIONS`, `:sysinfo`
([plan](plans/04-users-transactions-sysinfo.md)).
5. TLS (`bolt+s://`, HTTPS) ([plan](plans/05-tls.md)).
5. [TLS](#tls) certificates are only read at start-up, so renewing one needs a
restart ([plan](plans/05-tls.md)).
6. Each query's full result is fetched from PostgreSQL before it is streamed to the
client, so very large results use memory in boltage
([plan](plans/06-streaming-results.md)).