How to run the console as a lasting service: in a container on a server with Docker, or on Kubernetes (both recommended), or as a plain Java program without a container (possible, but discouraged; section 10), and signing people in either with their eDirectory password or through an OpenID Connect provider (single sign-on). QUICKSTART.md is the short version for trying it out on your own machine.
- Plan
- Run it on a server with Docker
- Run it on Kubernetes
- Set it up
- Sign-in without an authorization server
- Sign-in with an authorization server (single sign-on)
- The console's certificate
- Behind a reverse proxy or load balancer
- Check the deployment
- Without a container (not recommended)
- One instance. The console keeps its settings, sessions, trust store and audit log in one data volume and in memory, so run exactly one copy. It cannot be scaled out or run active-active; for availability, rely on the container restarting and back up the volume (see UPGRADING.md).
- Its address. Choose the host name people will use, such as
https://console.example.com. It becomesCONSOLE_PUBLIC_URL. It must be exactly what browsers see: the console checks it on every change, puts it in the single sign-on redirect URI, and puts its host in the certificate it makes. - HTTPS only. The console serves HTTPS itself, on port 8443 (
CONSOLE_PORT; inside the container, when it runs in one). There is no plain HTTP port. - Network. Inbound: HTTPS from people's browsers (or from your proxy). Outbound: LDAPS (normally 636) to every eDirectory server it works with, and the connections listed in UPGRADING.md; with single sign-on, HTTPS to the provider (its certificate from a public CA, or from a CA you add to the console's trust store).
- Accounts you need. A tree administrator for setup (it becomes the break-glass account), and a service account for finding people by short name and for single sign-on (section 5).
- Size. Small: 1 CPU and 1 GB of memory are plenty; the volume needs well under 1 GB (the audit log grows slowly, one file a month).
On a Linux server with Docker Engine and the compose plugin, make a folder (for example /opt/directory-console)
and download compose.yaml from the
latest release into it. Change
two things in it: the port browsers use, and the public address.
services:
console:
image: ${CONSOLE_IMAGE:-ghcr.io/pointbluetechnology/directory-console:latest}
restart: unless-stopped
ports:
- "443:8443"
environment:
CONSOLE_PUBLIC_URL: https://console.example.com
# the rest as released: the data volume, the read-only root, no capabilitiesLeave the hardening lines as released (read_only, tmpfs, cap_drop, no-new-privileges). To choose when
you upgrade, pin a version instead of latest (see UPGRADING.md).
If your network needs a proxy for outbound HTTPS, add HTTPS_PROXY under environment:.
Start it, and read the setup link from the log:
docker compose up -ddocker compose logs consolerestart: unless-stopped brings the console back after a reboot as long as Docker starts at boot
(systemctl enable docker on most distributions). Then go to Set it up.
Use version 0.3.10 or later on Kubernetes: 0.3.9 and earlier stop at start on a volume owned by root (as the
local-path provisioner makes them), and answer probes sent by IP address with 421.
container/kubernetes.yaml has a namespace, a volume claim, a deployment of one
pod, a service, and an ingress for ingress-nginx to uncomment. It was tried on k3s 1.36 with the local-path
storage class and ingress-nginx. Edit CONSOLE_PUBLIC_URL, and set the image tag to a version rather than latest
(a pinned version upgrades only when you change it; see
UPGRADING.md). Then:
kubectl apply -f kubernetes.yamlkubectl -n directory-console logs deploy/directory-consoleThe log shows the setup link. What the manifest does, and why:
- One replica,
Recreateupdates. Never two pods at once: they would share, and fight over, one volume. - A
ReadWriteOncevolume claim for/data. Your cluster's default storage class provides it; setstorageClassNameto choose another. When the volume is owned by root andfsGroupdoes not apply to it, the log says so once: the console leaves/data's own permissions as they are, and everything it writes inside is owner-only. - The same hardening as the container: non-root user 65532 (
fsGroup65532 so the volume is writable), a read-only root file system with/tmpin memory, no capabilities, no privilege escalation, the runtime's default seccomp profile. - Probes on
/api/v2/health/live, over HTTPS. Not/api/v2/health/readyfor readiness: that answers 503 until setup is finished, which would keep the setup page itself out of reach. - A
ClusterIPservice on port 443. Reach it through an ingress or aLoadBalancerservice; see section 8. To try it before that:kubectl -n directory-console port-forward svc/directory-console 8443:443and usehttps://localhost:8443(withCONSOLE_PUBLIC_URLset to that address while you try).
The admin commands run in the pod:
kubectl -n directory-console exec deploy/directory-console -- java -jar /app/console.jar admin verify-auditTo back up, copy the volume's contents with your usual tool (a volume snapshot, or Velero), or download a backup from Settings > History and backup.
Open the setup link from the log. It shows the console certificate's fingerprint, as the log does: check they match. Enter one of your eDirectory servers and sign in as a tree administrator. That tree becomes the console's first directory, and that administrator its break-glass account, who can always sign in with a password.
Then, in Settings > Directory, choose the Tier: Production (the default) asks for the directory's name to be typed before every change; Lab does not.
People sign in with their eDirectory name and password, and act with their own eDirectory rights.
- Service account (Settings > Directory). Without one, people must type their full name (DN), such as
cn=jsmith,ou=staff,o=acme. With one, they typejsmith. Give it an account that can read the people who will sign in (browse and readcn), and nothing more. - Search bases: the containers where short names are looked for. None means the whole tree.
- When a name matches several people: let them choose their account, or ask for the full name.
- Role mappings (Settings > Role mappings): map eDirectory groups to the console's roles (viewer, editor, operator, admin), optionally limited to containers. Someone with no role cannot use the console, whatever their eDirectory rights.
- Sessions (Settings > Directory): idle sign-out and the longest session.
The provider (Entra ID, Okta, Keycloak, OpenText Access Manager or any OpenID Connect provider) says who the person is. The console finds their eDirectory account through the service account, and then acts as them with eDirectory's proxied authorization, so they still have only their own rights, in Identity Manager too. Accounts that are disabled, expired or locked by intruder detection cannot sign in this way. Password sign-in stays available alongside it, unless you limit it to the break-glass and listed accounts (step 7 below).
At the provider, register an application (a "web" or "confidential" client):
- Redirect URI:
https://console.example.com/api/v2/oidc/callback, yourCONSOLE_PUBLIC_URLfollowed by/api/v2/oidc/callback. Settings > Single sign-on shows the exact value to register. - Flow: authorization code. Either keep a client secret, or make it a public client (PKCE, no secret).
- Claims: a claim that names the eDirectory account, such as
preferred_usernameoremail; and, to map provider groups to console roles, a groups claim in the ID token. - Note the issuer URL (its
/.well-known/openid-configurationmust be reachable from the console), the client id, and the secret.
The service account finds people for single sign-on, and the console acts as them through it. In eDirectory, give it the Supervisor right over the people who may sign in this way (for example on their container), and no broader rights. Proxied authorization needs that right.
In the console, Settings > Single sign-on:
- Issuer, Client id, Client secret (or Public client), Scopes (
openid profile emailunless your provider needs others), and a Button label for the sign-in page ("Sign in with …"). - Claim and Matches the attribute: for example the claim
preferred_usernamematchescn, oremailmatchesmail. Exactly one account must match. - Under these containers: only people there can sign in this way. Keep it to the containers the service account has Supervisor over.
- Never act as: accounts that must never be signed in through the provider, such as the tree administrator and other administrators. They sign in with a password.
- Groups claim and Provider groups: map the provider's groups to console roles, as role mappings do for
eDirectory groups. A dotted path such as
realm_access.rolesworks for nested claims. - Try the mapping: enter a claim value and see which account it finds. Then sign in with the button. Check its rights (at the top of the page, and under Settings > Directory) reads the service account's own rights on a few people under each container: whether it can find them and act as them, and whether it has more than it needs (Supervisor over the whole tree, or over a break-glass account).
- Password sign-in (optional): choose Single sign-on only, except the accounts below to stop everyone else signing in with a password. The break-glass accounts always may; list any other accounts that should (for example administrators who need a way in when the provider is down), one DN per line. They keep the roles their groups give them. The sign-in page then offers single sign-on first, with a link to the password form. While this is on, single sign-on and the service account cannot be removed.
The provider's certificate must come from a public CA, or from a CA the console trusts: add your internal CA under Settings > Trusted certificates (for every directory, or for this one). It takes effect at the next sign-in, without a restart. Each directory has its own provider settings: sign in to a directory to set up its single sign-on.
On first start the console makes a self-signed certificate for CONSOLE_PUBLIC_URL's host and prints its
fingerprint. Browsers warn about it until you replace it, in Settings > Console certificate, in one of two ways:
- Upload a certificate from your own CA: a PKCS#12 file with its password, or PEM (the certificate with its
intermediate CAs, and the private key, encrypted or not). It must name the host in
CONSOLE_PUBLIC_URL, unless a reverse proxy in front connects to the console by another name (a box on the form allows that). It is installed at once, without a restart; renew it by uploading again before it expires (the expiry check warns). - From an ACME CA (Let's Encrypt, or an internal one such as step-ca), proven with DNS-01: the console adds a TXT record through an RFC 2136 dynamic update signed with a TSIG key, and renews by itself. While this is on, upload is not offered.
Behind a reverse proxy that holds the public certificate, the console's own certificate only needs to satisfy the proxy (next section).
- The proxy connects to the console over HTTPS (port 8443, or 443 on the Kubernetes service). Either pass TLS through untouched (the console's certificate is what browsers see), or end TLS at the proxy and open a new HTTPS connection to the console; then have the proxy trust the console's certificate, or skip verifying it on that hop if your policy allows.
CONSOLE_PUBLIC_URLis the proxy's address, the one browsers use. The console refuses changes from any other origin, and its session cookie needs HTTPS end to end.- Pass the
Hostheader through unchanged, and setX-Forwarded-For. - Forward
/mcptoo, with itsAuthorizationheader, if people will use AI agents (agent keys, from 0.4.0). A proxy that forwards only the page and/apileaves agents unable to connect. - Settings > Reverse proxy: list the proxy's addresses (IPs or CIDR ranges). Only then does the console believe
its
X-Forwarded-For, so audit entries and sign-in limits see each person's own address. - Kubernetes with ingress-nginx: either
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"(TLS ends at the ingress) ornginx.ingress.kubernetes.io/ssl-passthrough: "true"(the controller must be started with--enable-ssl-passthrough). List the controller pods' addresses (or the pod network's range) as trusted proxies.
https://console.example.com/api/v2/health/liveanswers{"version":…,"status":"live"}, and/api/v2/health/readyanswersreadyonce setup is finished.- The certificate browsers see is the one you expect.
https://console.example.com/mcpanswers 401 withWWW-Authenticate: Bearerto a request without an agent key (a 404 means the proxy does not forward it).- A person with a role signs in with a short name; one without a role is refused.
- With single sign-on: the button signs someone in as their own account, and an account under Never act as is refused. With password sign-in limited to single sign-on, an unlisted account's password is refused, and a break-glass account still signs in.
- The audit log (Audit log page, Check the chain) shows those sign-ins.
- A backup downloads from Settings > History and backup, and you have stored it and its passphrase apart.
The console can run as an ordinary Java program, without Docker or Kubernetes. We strongly advise against it. The container is the supported install: these steps work, but you take on what the image does for you.
- Every update is manual. Each release you stop the console, replace its files by hand and start it again;
docker compose pullis not there to do it, and going back means keeping the previous files yourself. - Java is yours to keep patched. The image carries a current Java 21 runtime; here you install and update it.
- The hardening is yours to rebuild. The image runs as a non-root user with a read-only file system and no capabilities; outside it, that is up to the service manager (an example follows).
- The update banner and the docs assume the image. Their upgrade steps are for compose and Kubernetes.
- Linux with Java 21 (a JRE is enough; the image uses Java 21, and that is what is tested).
- The console's files:
console.jarand thelib/folder beside it. They are in the published image under/app; on any machine with Docker, copy them out, for example for 0.3.16:
docker create --name dc-files ghcr.io/pointbluetechnology/directory-console:0.3.16docker cp dc-files:/app ./directory-console-0.3.16 && docker rm dc-filesOr build them from source (see the README): container/build.sh --artifacts leaves them in container/artifacts/.
- A user for the service, and a data folder only it can read (
chmod 700), such as/var/lib/directory-console.
Put the files in a folder of their own, such as /opt/directory-console/0.3.16, and point /opt/directory-console/current
at it, so an update can switch folders and a rollback can switch back. Then run, as the service user:
CONSOLE_DATA=/var/lib/directory-console CONSOLE_PORT=8443 CONSOLE_PUBLIC_URL=https://console.example.com:8443 java -XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError -cp "/opt/directory-console/current/console.jar:/opt/directory-console/current/lib/*" com.pointblue.console.server.MainThe setup link and the certificate's fingerprint are printed when it starts, as in a container (with systemd:
journalctl -u directory-console). The admin commands run the same way, with the same CONSOLE_DATA:
CONSOLE_DATA=/var/lib/directory-console java -jar /opt/directory-console/current/console.jar admin verify-auditA systemd unit that keeps some of the container's limits:
[Unit]
Description=Directory Console
After=network-online.target
Wants=network-online.target
[Service]
User=dconsole
Environment=CONSOLE_DATA=/var/lib/directory-console
Environment=CONSOLE_PORT=8443
Environment=CONSOLE_PUBLIC_URL=https://console.example.com:8443
# the path to your Java 21
ExecStart=/usr/bin/java -XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError -cp "/opt/directory-console/current/console.jar:/opt/directory-console/current/lib/*" com.pointblue.console.server.Main
Restart=on-failure
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/var/lib/directory-console
CapabilityBoundingSet=
[Install]
WantedBy=multi-user.targetPort 8443 needs no privileges. For 443, put a reverse proxy in front (section 8), or forward the port in the firewall, rather than running Java as root.
- Back up the data folder: stop the service and copy it whole (
cp -a /var/lib/directory-console …), or download a backup from Settings > History and backup first. Read the release's notes in UPGRADING.md. - Put the new release's files in a new folder; do not copy them over the old ones (the
lib/jars change names between releases). - Point
currentat the new folder and restart the service.
To go back: point current at the previous folder, and restore the data backup if the release notes say the
configuration format changed.