From 665a41cfde527181e3743d3fe8216028f9bd49ab Mon Sep 17 00:00:00 2001 From: William Mantly Date: Sat, 11 Jul 2026 22:24:12 -0400 Subject: [PATCH] Load sudo + openssh-lpk schemas in the all-in-one image; fix schema COPY MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SSO app (nodejs/models/user_ldap.js addPosixAccount) tags every new user with objectClasses [inetOrgPerson, sudoRole, ldapPublicKey, posixAccount, top, theta42Person] and writes sudoHost/sudoCommand/sudoUser + sshPublicKey. The all-in-one image's slapd.conf only included core/cosine/inetorgperson/nis + theta42, so creating a user failed: sudoRole and ldapPublicKey were unknown objectClasses (LDAP objectClassViolation 65) and sudoHost/sudoCommand/sudoUser and sshPublicKey were unknown attributes. Ship the two missing schemas and include them in slapd.conf: - ops/schema/sudo.schema (sudoRole + sudo* attributes) - ops/schema/openssh-lpk.schema (sshPublicKey + ldapPublicKey) sudoRole is AUXILIARY here, not STRUCTURAL as in upstream sudo. The app attaches sudoRole directly onto the user entry, which is already inetOrgPerson (STRUCTURAL); two unrelated structural classes violate RFC 4512 and OpenLDAP rejects with 65. AUXILIARY lets it coexist with inetOrgPerson — the app's per-user-sudoers model. sudo's LDAP backend still finds entries via (objectClass=sudoRole) regardless. ldapPublicKey is AUXILIARY as in upstream openssh-lpk. Also fix the build error from the previous theta42 schema PR: .dockerignore excluded all of ops/, so 'COPY ops/schema/theta42.schema' failed at build time ('not found' — file is git-tracked but stripped from the context). Re- include ops/schema/*.schema with !exceptions, matching the existing README.md/tos.md pattern. Co-Authored-By: Claude --- .dockerignore | 5 +- Dockerfile.openldap | 19 +++++--- docker-entrypoint.sh | 2 + ops/schema/openssh-lpk.schema | 20 ++++++++ ops/schema/sudo.schema | 86 +++++++++++++++++++++++++++++++++++ 5 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 ops/schema/openssh-lpk.schema create mode 100644 ops/schema/sudo.schema diff --git a/.dockerignore b/.dockerignore index 61dc586..fd11775 100644 --- a/.dockerignore +++ b/.dockerignore @@ -37,8 +37,11 @@ Dockerfile* docker-compose.yml .dockerignore -# Ops scripts (not needed in container) +# Ops scripts (not needed in container) — except the custom LDAP schema files, +# which Dockerfile.openldap COPYs into the image for the bundled slapd. ops/ +!ops/schema/ +!ops/schema/*.schema # Secrets (mount at runtime instead) nodejs/conf/secrets.js diff --git a/Dockerfile.openldap b/Dockerfile.openldap index fddd1c7..181ba98 100644 --- a/Dockerfile.openldap +++ b/Dockerfile.openldap @@ -68,12 +68,19 @@ COPY tos.md /tos.md COPY docker-entrypoint.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/docker-entrypoint.sh -# theta42 custom schema (dateOfBirth + theta42Person). The app adds -# objectClass theta42Person when a user has a dateOfBirth, so the directory -# must know it or user create/update fails with LDAP 0x15. Mirrors the -# cn=config LDIF ops/ldap-setup.sh loads on bare metal, in .schema form so -# docker-entrypoint.sh can `include` it in the static slapd.conf. -COPY ops/schema/theta42.schema /etc/openldap/schema/theta42.schema +# 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) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 514cf47..c3d002c 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -79,6 +79,8 @@ include /etc/openldap/schema/cosine.schema include /etc/openldap/schema/inetorgperson.schema include /etc/openldap/schema/nis.schema include /etc/openldap/schema/theta42.schema +include /etc/openldap/schema/sudo.schema +include /etc/openldap/schema/openssh-lpk.schema # Module loading (pw-sha2 provides {SSHA512} used by the app for user passwords; # ppolicy/memberof/refint are the overlays the app depends on). On OpenLDAP 2.5+ diff --git a/ops/schema/openssh-lpk.schema b/ops/schema/openssh-lpk.schema new file mode 100644 index 0000000..874c0f2 --- /dev/null +++ b/ops/schema/openssh-lpk.schema @@ -0,0 +1,20 @@ +# OpenSSH LDAP Public Key (openssh-lpk) schema, the widely-used Buchan +# openssh-lpk definition (OIDs under 1.3.6.1.4.1.24552.500.1). +# +# The SSO app (nodejs/models/user_ldap.js) stores each user's SSH public key in +# the `sshPublicKey` attribute and tags the user entry with the `ldapPublicKey` +# auxiliary objectClass, so the directory must know both for user create/update +# to succeed. ldapPublicKey is AUXILIARY (as in upstream openssh-lpk), so it +# attaches cleanly onto the inetOrgPerson user entry. + +attributetype ( 1.3.6.1.4.1.24552.500.1.1 + NAME 'sshPublicKey' + DESC 'SSH public key' + EQUALITY octetStringMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 ) + +objectclass ( 1.3.6.1.4.1.24552.500.1.2 + NAME 'ldapPublicKey' + DESC 'SSH public key user' + SUP top AUXILIARY + MAY sshPublicKey ) \ No newline at end of file diff --git a/ops/schema/sudo.schema b/ops/schema/sudo.schema new file mode 100644 index 0000000..f8721a5 --- /dev/null +++ b/ops/schema/sudo.schema @@ -0,0 +1,86 @@ +# sudo LDAP schema (sudoUser / sudoHost / sudoCommand / sudoOption / etc. + +# the sudoRole objectClass), based on the canonical schema shipped with sudo +# (doc/schema.OpenLDAP, OIDs under 1.3.6.1.4.1.15953.9). +# +# ONE DEVIATION from upstream: sudoRole is AUXILIARY here, not STRUCTURAL. +# The SSO app (nodejs/models/user_ldap.js addPosixAccount) attaches sudoRole +# directly onto each user entry, which is already inetOrgPerson — a STRUCTURAL +# class. RFC 4512 forbids two unrelated structural object classes on one entry, +# and OpenLDAP rejects that with objectClassViolation (65). Making sudoRole +# AUXILIARY lets it coexist with inetOrgPerson on the user entry, which is the +# app's per-user-sudoers model. This does not affect sudo's LDAP backend: it +# searches by (objectClass=sudoRole) + sudoUser/sudoHost/sudoCommand, which work +# the same regardless of structural vs auxiliary. + +attributetype ( 1.3.6.1.4.1.15953.9.1.1 + NAME 'sudoUser' + DESC 'User(s) who may run sudo' + EQUALITY caseExactMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) + +attributetype ( 1.3.6.1.4.1.15953.9.1.2 + NAME 'sudoHost' + DESC 'Host(s) who may run sudo' + EQUALITY caseExactMatch + SUBSTR caseExactSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) + +attributetype ( 1.3.6.1.4.1.15953.9.1.3 + NAME 'sudoCommand' + DESC 'Command(s) to be executed by sudo' + EQUALITY caseExactMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) + +attributetype ( 1.3.6.1.4.1.15953.9.1.4 + NAME 'sudoRunAs' + DESC 'User(s) impersonated by sudo (deprecated; use sudoRunAsUser)' + EQUALITY caseExactMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) + +attributetype ( 1.3.6.1.4.1.15953.9.1.5 + NAME 'sudoOption' + DESC 'Options(s) followed by sudo' + EQUALITY caseExactMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) + +attributetype ( 1.3.6.1.4.1.15953.9.1.6 + NAME 'sudoRunAsUser' + DESC 'User(s) impersonated by sudo' + EQUALITY caseExactMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) + +attributetype ( 1.3.6.1.4.1.15953.9.1.7 + NAME 'sudoRunAsGroup' + DESC 'Group(s) impersonated by sudo' + EQUALITY caseExactMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) + +attributetype ( 1.3.6.1.4.1.15953.9.1.8 + NAME 'sudoNotBefore' + DESC 'Start of time interval for which the entry is valid' + EQUALITY generalizedTimeMatch + ORDERING generalizedTimeOrderingMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 ) + +attributetype ( 1.3.6.1.4.1.15953.9.1.9 + NAME 'sudoNotAfter' + DESC 'End of time interval for which the entry is valid' + EQUALITY generalizedTimeMatch + ORDERING generalizedTimeOrderingMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 ) + +attributetype ( 1.3.6.1.4.1.15953.9.1.10 + NAME 'sudoOrder' + DESC 'An integer to order the sudoRole entries' + EQUALITY integerMatch + ORDERING integerOrderingMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 ) + +objectclass ( 1.3.6.1.4.1.15953.9.2.1 + NAME 'sudoRole' + DESC 'Authorization Class for sudo' + SUP top AUXILIARY + MAY ( sudoUser $ sudoHost $ sudoCommand $ sudoRunAs $ sudoRunAsUser $ + sudoRunAsGroup $ sudoOption $ sudoOrder $ sudoNotBefore $ + sudoNotAfter $ description ) ) \ No newline at end of file