Files
sso-manager-node/Dockerfile.openldap
T
wmantly ecd21c4984 feat: v1.17.2 post-deploy fixes + SMS/TOS on /conf
- auto-slug plugins (no more manual slug field)
- plugin schedule dropdown (hourly/daily/weekly + custom)
- fix /vault secrets-list 403 (per-user/app/admin list grants on dir path;
  ensurePolicy always re-writes so existing policies get the grant)
- fix /profile literal {{...}} tags (header uid span, members label id,
  admin-actions moved inside jq-repeat=user scope)
- fix plugin editing (Edit modal non-secret only; secrets have own modal)
- nmap: apk add nmap in Dockerfile.openldap + clearer missing-binary error
- add SMS (VoIP.ms) config card to /conf (password masked, leave-blank-to-keep)
- move Terms of Service editor from Overview to /conf

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 22:47:37 -04:00

229 lines
11 KiB
Docker

# Theta42 SSO Manager with OpenLDAP - All-in-One Dockerfile
# App + OpenLDAP in a single container, for development/testing or a
# self-contained single-node deployment. For production, run a dedicated
# LDAP server and configure the app via app_* env vars / mounted secrets.js.
# ── Git commit hash (build-time only) ────────────────────────────────────────
# The final image intentionally has no git binary and no .git directory (kept
# lean, per .dockerignore), so `git rev-parse` always fails at runtime and
# build_info.js silently fell back to "unknown". Resolve it here instead,
# where .git IS available (build context), and bake just the short hash into
# a file — this stage itself is discarded, only /commit.txt survives via the
# COPY --from below. Reuses the main base image (already pulled for the real
# build below) rather than a separate one, so this adds no extra image pull.
#
# GIT_COMMIT lets a caller override the resolved hash instead of computing it
# from .git in this build context. Needed when this repo is built as a git
# submodule (e.g. from theta-env): a submodule's .git is a pointer FILE, not
# a directory — the real object database lives in the superproject's
# .git/modules/, outside this repo's own directory and therefore outside
# Docker's build context entirely, so `git rev-parse` can never resolve it
# from in here no matter what. theta-env's setup.sh passes --build-arg
# GIT_COMMIT=$(git -C sso-manager-node rev-parse --short HEAD), computed on
# the host where the submodule resolves correctly.
ARG GIT_COMMIT=""
FROM node:20-alpine AS gitinfo
ARG GIT_COMMIT
WORKDIR /repo
COPY .git ./.git
RUN if [ -n "$GIT_COMMIT" ]; then \
echo "$GIT_COMMIT" > /commit.txt; \
else \
{ apk add --no-cache git \
&& git rev-parse --short HEAD > /commit.txt; } 2>/dev/null || echo unknown > /commit.txt; \
fi
# ── OpenLDAP from source ─────────────────────────────────────────────────────
# We build slapd from OpenLDAP master rather than installing Alpine's packages,
# for exactly one feature: the `nestgroup` overlay (ITS#10161, Howard Chu,
# 2024-03-21), which evaluates nested groups server-side. Nothing in any 2.6.x
# release can do this -- verified: 2.6.13 ships 26 overlay modules and
# nestgroup is not among them -- and the alternative is resolving nesting
# separately in every consumer (this app, SSSD on each host, jump-host, proxy),
# where any consumer that forgets silently under-grants access.
#
# Consequence to know about: master ships LMDB 1.0.0, whose on-disk format the
# 0.9.x used by 2.6.x cannot read, and vice versa
# ("MDB_INVALID: File is not an LMDB file"). Moving an existing directory onto
# this image is a slapcat/slapadd migration, not a restart. See DEPLOYMENT.md.
FROM node:20-alpine AS ldapbuild
# groff is not optional despite producing nothing we ship: the build descends
# into doc/man unconditionally and its Makefile calls soelim, which groff
# provides. Without it the whole `make` fails at the man-page stage
# ("soelim: not found") long after slapd itself has compiled fine.
RUN apk add --no-cache \
build-base autoconf automake libtool \
openssl-dev cyrus-sasl-dev \
git make pkgconf util-linux-dev groff
# Pinned to an exact commit, not a branch tip. This is the directory server the
# whole lab authenticates against; an unpinned `master` would mean every image
# rebuild silently ships whatever landed upstream that morning, and a bad day on
# master would take out logins with no way to tell what changed.
#
# TODO: drop this whole from-source stage once nestgroup ships in a release.
# It is master-only today (ITS#10161, 2024-03-21); the 2.7 roadmap has slipped
# from Fall 2024 to Fall 2025 and is still unreleased. When 2.7 lands with
# nestgroup, revert to `apk add openldap openldap-overlay-nestgroup ...` --
# the entrypoint already probes for nestgroup.so and needs no change, and the
# app already keys off app_ldap__nestedGroupsServerSide either way.
ARG OPENLDAP_COMMIT=350e9eb38b2270c2bad97c61ee02e85fb8f3196d
WORKDIR /src
RUN git init -q . \
&& git remote add origin https://git.openldap.org/openldap/openldap.git \
&& git fetch -q --depth 1 origin "${OPENLDAP_COMMIT}" \
&& git checkout -q FETCH_HEAD \
&& git rev-parse HEAD > /opt-openldap-commit.txt
# Overlays are built as loadable modules (=mod) because docker-entrypoint.sh
# `moduleload`s them individually; nestgroup joins that set.
RUN ./configure \
--prefix=/opt/openldap \
--enable-slapd \
--enable-modules \
--enable-mdb \
--enable-memberof=mod \
--enable-refint=mod \
--enable-ppolicy=mod \
--enable-dynlist=mod \
--enable-nestgroup=mod \
--enable-syncprov=mod \
--enable-auditlog=mod \
--with-tls=openssl \
--with-cyrus-sasl \
&& make depend \
&& make -j"$(nproc)" \
&& make install
# pw-sha2 provides {SSHA512}, which every existing user password is stored as.
# It lives in contrib and is not covered by the configure flags above, so it is
# built separately against the just-built tree -- omitting it would make every
# user password unverifiable.
RUN cd contrib/slapd-modules/passwd/sha2 \
&& make prefix=/opt/openldap OPENLDAP_SRC=/src \
&& cp .libs/pw-sha2.so* /opt/openldap/libexec/openldap/
FROM node:20-alpine
# Runtime libraries the from-source slapd links against, plus the app's own
# deps. No openldap* packages here: everything LDAP comes from /opt/openldap.
# libltdl (module loading -- slapd is useless without it, since every overlay
# is a loadable module) and libuuid are pulled in by the source build but are
# NOT dependencies of anything else here, so they must be named explicitly;
# omitting them fails at runtime with "Error relocating ... lt_dlopenext:
# symbol not found", not at build time.
RUN apk add --no-cache \
openssl \
libsasl \
libltdl \
libuuid \
dumb-init \
bash \
redis \
nmap \
&& rm -rf /var/cache/apk/*
COPY --from=ldapbuild /opt/openldap /opt/openldap
# Which upstream commit this slapd was built from — so a running container can
# answer "what am I actually running" without rebuilding.
COPY --from=ldapbuild /opt-openldap-commit.txt /opt/openldap/COMMIT
# The Alpine openldap package used to create these; nothing does now, and
# docker-entrypoint.sh runs slapd as -u ldap -g ldap.
RUN addgroup -S ldap 2>/dev/null || true \
&& adduser -S -D -H -G ldap ldap 2>/dev/null || true
# docker-entrypoint.sh invokes slapd/slappasswd/ldapadd/ldapsearch by bare name
# and probes a list of candidate module directories, so putting the from-source
# tree first on PATH is all that is needed to redirect it. Schemas are symlinked
# into the conventional location because the entrypoint's slapd.conf includes
# /etc/openldap/schema/*.schema, and the app's own schemas (theta42, sudo,
# openssh-lpk) are copied there too.
ENV PATH="/opt/openldap/bin:/opt/openldap/sbin:/opt/openldap/libexec:${PATH}"
RUN mkdir -p /etc/openldap/schema \
&& for f in /opt/openldap/etc/openldap/schema/*.schema; do \
ln -sf "$f" "/etc/openldap/schema/$(basename "$f")"; \
done
WORKDIR /app
# Create required directories. slapd runs as the ldap user; the app process
# runs as root in this image (matches the bare-metal systemd unit).
RUN mkdir -p /var/lib/ldap /etc/ldap/sasl2 && \
chown -R ldap:ldap /var/lib/ldap /etc/ldap/sasl2
# Copy application source and install production dependencies.
# .dockerignore excludes nodejs/node_modules so npm ci builds a clean tree.
COPY nodejs/package*.json ./
RUN npm ci --omit=dev
COPY nodejs/app.js ./
COPY nodejs/bin ./bin
COPY nodejs/conf ./conf
COPY nodejs/controller ./controller
COPY nodejs/middleware ./middleware
COPY nodejs/models ./models
COPY nodejs/routes ./routes
COPY nodejs/services ./services
COPY nodejs/utils ./utils
COPY nodejs/views ./views
COPY nodejs/public ./public
COPY nodejs/plugins ./plugins
# routes/index.js reads path.join(__dirname, '../../tos.md') at boot. With the
# app flattened into /app, __dirname is /app/routes and ../../ resolves to /,
# so the file must exist at /tos.md (mirroring the repo where tos.md sits one
# level above the nodejs/ app dir). Without this the app crashes on startup.
COPY tos.md /tos.md
# Documentation, served in-app at /docs (routes/docs.js) so it's readable
# without internet access. Same flattened-path convention as tos.md above.
COPY README.md /README.md
COPY CHANGELOG.md /CHANGELOG.md
COPY DEPLOYMENT.md /DEPLOYMENT.md
COPY API.md /API.md
COPY directory_spec.md /directory_spec.md
COPY docs /docs
# Baked commit hash from the gitinfo stage (see build_info.js).
COPY --from=gitinfo /commit.txt ./.build_commit
# Copy startup script
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Custom LDAP schemas the app's user model depends on. addPosixAccount
# (nodejs/models/user_ldap.js) tags every user entry with objectClasses
# theta42Person, sudoRole, and ldapPublicKey and writes dateOfBirth /
# sudoHost,sudoCommand,sudoUser / sshPublicKey — so the directory must know
# all three or user create/update fails (theta42Person: LDAP 0x15; sudoRole /
# ldapPublicKey: objectClassViolation 65). .dockerignore excludes ops/ from
# the build context except ops/schema/*.schema, which is why these COPYs work.
# theta42.schema : dateOfBirth + theta42Person (mirrors ldap-setup.sh §5)
# sudo.schema : sudoRole (AUXILIARY — see file header) + sudo* attributes
# openssh-lpk.schema : sshPublicKey + ldapPublicKey (AUXILIARY)
COPY ops/schema/theta42.schema /etc/openldap/schema/theta42.schema
COPY ops/schema/sudo.schema /etc/openldap/schema/sudo.schema
COPY ops/schema/openssh-lpk.schema /etc/openldap/schema/openssh-lpk.schema
# Expose ports
# 3001: SSO Manager web interface (HTTP — terminate TLS at the front proxy)
# 389: LDAP (plain + StartTLS) — used internally by the app; map to host only
# if you want LAN clients to bind without TLS (not recommended).
# 636: LDAPS — direct LDAP binds over the network (TLS): Linux host auth
# (PAM/SSSD, sudo, SSH keys) and LDAP-native apps
EXPOSE 3001 389 636
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1
# dumb-init reaps zombies and forwards signals to the node process the
# entrypoint execs into. Without it SIGTERM from `docker stop` is ignored
# and the container hits the 10s kill timeout.
ENTRYPOINT ["dumb-init", "/usr/local/bin/docker-entrypoint.sh"]
CMD ["node", "bin/www"]