mirror of
https://github.com/osixia/container-openldap.git
synced 2026-11-06 12:40:36 -04:00
Add optional directory tree and scoped management account
This commit is contained in:
@@ -2,6 +2,11 @@
|
||||
|
||||
Guidance for coding agents working on `osixia/openldap` (v2), an OpenLDAP container image built on [osixia/baseimage](https://github.com/osixia/container-baseimage) (Alpine).
|
||||
|
||||
## Documentation
|
||||
|
||||
- Keep the main `README.md` concise and simple. For new options, add only the required environment-variable table rows there; put explanations in the relevant service README.
|
||||
- Document bootstrap features briefly alongside the other features in the existing bootstrap READMEs. Do not create a separate documentation page for a small feature unless explicitly requested, and do not add feature explanations to the main README.
|
||||
|
||||
## Repository layout
|
||||
|
||||
| Path | Role |
|
||||
|
||||
@@ -6,6 +6,11 @@ Project-specific changes only. See upstream [OpenLDAP release notes](https://www
|
||||
|
||||
### Added
|
||||
|
||||
- Add optional `OPENLDAP_BOOTSTRAP_DATA_TREE` to create users, groups and services
|
||||
OUs, and `OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT` for one scoped application account.
|
||||
It reads users/groups and resets user passwords, including with ppolicy safe
|
||||
modification, without hash/history reads or writes to profiles, memberships,
|
||||
service accounts or configuration. Both features are disabled by default.
|
||||
- Add `openldap-ctl validate-ldif` for files and directories to check data entry LDIF
|
||||
syntax, values and schemas without importing entries or stopping slapd.
|
||||
- Expose bootstrap settings for pending request limits, incoming LDAP message sizes
|
||||
|
||||
@@ -381,6 +381,13 @@ docker run --rm --user $UID -v $(pwd)/schema:/data \
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_LAST_BIND_PRECISION` | Minimum interval between `pwdLastSuccess` updates, in seconds | `3600` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_ROOT_DN` | Root DN for main database | `cn=admin,${OPENLDAP_BOOTSTRAP_SUFFIX}` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_ROOT_PASSWORD_HASHED` | Hashed password for main database root DN; generated if empty | `` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_TREE` | Create the users, groups and services OUs | `false` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_TREE_USERS_DN` | Users OU DN | `ou=users,${OPENLDAP_BOOTSTRAP_SUFFIX}` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_TREE_GROUPS_DN` | Groups OU DN | `ou=groups,${OPENLDAP_BOOTSTRAP_SUFFIX}` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_TREE_SERVICES_DN` | Services OU DN | `ou=services,${OPENLDAP_BOOTSTRAP_SUFFIX}` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT` | Enable user/group reads and user password resets | `false` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_DN` | Management account DN | `cn=management,${OPENLDAP_BOOTSTRAP_DATA_TREE_SERVICES_DN}` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_PASSWORD_HASHED` | Hashed management password; generated if empty when enabled | `` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_READONLY` | Enable read-only data account creation | `false` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_READONLY_DN` | DN of the read-only data account | `cn=readonly,${OPENLDAP_BOOTSTRAP_SUFFIX}` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_READONLY_PASSWORD_HASHED` | Hashed password of the read-only data account; generated if empty when enabled | `` |
|
||||
|
||||
@@ -21,6 +21,10 @@ own native runner, then publishes them together from their OCI archives
|
||||
After the shared version check, `tests/` runs integration scenarios on each built image:
|
||||
|
||||
- default bootstrap;
|
||||
- standard users/groups/services tree and management account: independent opt-ins,
|
||||
custom DNs and supplied OUs, generated passwords, user/group
|
||||
reads, password resets including application administrators, ppolicy-safe resets,
|
||||
hidden hashes/history, and denied profile, membership, entry and service changes;
|
||||
- global request limits: native defaults, custom values and incoming message size
|
||||
enforcement for anonymous and authenticated connections;
|
||||
- read-only bootstrap templates with projected-volume symlinks, custom overrides
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"dagger.io/dagger"
|
||||
)
|
||||
|
||||
// DataManagement checks optional tree creation and the scoped password-reset account.
|
||||
func DataManagement(ctx context.Context, client *dagger.Client, image *dagger.Container) error {
|
||||
passwords, err := rootPasswords(ctx, image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
base := merge(passwords, env{
|
||||
"OPENLDAP_BOOTSTRAP_DATA_TREE": "true",
|
||||
"OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT": "true",
|
||||
"OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_PASSWORD_HASHED": passwords["OPENLDAP_BOOTSTRAP_DATA_ROOT_PASSWORD_HASHED"],
|
||||
})
|
||||
|
||||
for _, mode := range []string{"disabled", "tree", "plain", "policy", "custom", "supplied"} {
|
||||
if err := func() error {
|
||||
usersDN := "ou=users," + suffix
|
||||
groupsDN := "ou=groups," + suffix
|
||||
servicesDN := "ou=services," + suffix
|
||||
managementDN := "cn=management," + servicesDN
|
||||
settings := merge(base)
|
||||
nodeImage := image
|
||||
switch mode {
|
||||
case "disabled":
|
||||
settings["OPENLDAP_BOOTSTRAP_DATA_TREE"] = "false"
|
||||
settings["OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT"] = "false"
|
||||
case "tree":
|
||||
settings["OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT"] = "false"
|
||||
case "policy":
|
||||
settings = merge(settings, env{
|
||||
"OPENLDAP_BOOTSTRAP_PPOLICY": "true",
|
||||
"OPENLDAP_BOOTSTRAP_MEMBEROF": "true",
|
||||
"OPENLDAP_BOOTSTRAP_REFINT": "true",
|
||||
"OPENLDAP_BOOTSTRAP_REFINT_NOTHING_DN": "cn=empty-group," + servicesDN,
|
||||
"OPENLDAP_BOOTSTRAP_UNIQUE": "true",
|
||||
"OPENLDAP_BOOTSTRAP_AUDITLOG_ENABLED": "true",
|
||||
"OPENLDAP_BOOTSTRAP_DATA_READONLY": "true",
|
||||
"OPENLDAP_BOOTSTRAP_DATA_READONLY_PASSWORD_HASHED": passwords["OPENLDAP_BOOTSTRAP_DATA_ROOT_PASSWORD_HASHED"],
|
||||
})
|
||||
case "custom":
|
||||
usersDN = "ou=people," + suffix
|
||||
groupsDN = "ou=roles," + suffix
|
||||
servicesDN = "ou=machines," + suffix
|
||||
managementDN = "cn=sso," + servicesDN
|
||||
case "supplied":
|
||||
settings["OPENLDAP_BOOTSTRAP_DATA_TREE"] = "false"
|
||||
nodeImage = image.WithNewFile("/container/services/openldap-bootstrap/assets/ldif/data/custom/30-custom-tree.ldif", fmt.Sprintf(`dn: %s
|
||||
objectClass: organizationalUnit
|
||||
ou: users
|
||||
|
||||
dn: %s
|
||||
objectClass: organizationalUnit
|
||||
ou: groups
|
||||
|
||||
dn: %s
|
||||
objectClass: organizationalUnit
|
||||
ou: services
|
||||
`, usersDN, groupsDN, servicesDN))
|
||||
}
|
||||
settings = merge(settings, env{
|
||||
"OPENLDAP_BOOTSTRAP_DATA_TREE_USERS_DN": usersDN,
|
||||
"OPENLDAP_BOOTSTRAP_DATA_TREE_GROUPS_DN": groupsDN,
|
||||
"OPENLDAP_BOOTSTRAP_DATA_TREE_SERVICES_DN": servicesDN,
|
||||
"OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_DN": managementDN,
|
||||
})
|
||||
node := "management-" + mode
|
||||
service, err := startNode(ctx, nodeImage, node, settings, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stopNodes(ctx, []*dagger.Service{service})
|
||||
return check(ctx, image, nil, env{
|
||||
"TEST_NODES": node,
|
||||
"TEST_MANAGEMENT_MODE": mode,
|
||||
"TEST_USERS_DN": usersDN,
|
||||
"TEST_GROUPS_DN": groupsDN,
|
||||
"TEST_SERVICES_DN": servicesDN,
|
||||
"TEST_MANAGEMENT_DN": managementDN,
|
||||
}, dataManagementCheck)
|
||||
}(); err != nil {
|
||||
return fmt.Errorf("data management %s: %w", mode, err)
|
||||
}
|
||||
}
|
||||
|
||||
generated, err := expect(ctx, withEnv(image, merge(base, env{"OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_PASSWORD_HASHED": ""})), true,
|
||||
[]string{"Generated database management user password:", "config file testing succeeded"}, "container", "--skip-process")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := generated.container.WithExec([]string{"container", "run", "--", "slapcat", "-n", "1", "-a", "(cn=management)"}).Stdout(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.Contains(data, "cn: management") || !strings.Contains(data, "userPassword:: ") {
|
||||
return fmt.Errorf("generated management account/password missing")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:embed testdata/data-management.sh
|
||||
var dataManagementCheck string
|
||||
@@ -19,6 +19,7 @@ type scenario struct {
|
||||
|
||||
var scenarios = []scenario{
|
||||
{"default bootstrap", Default},
|
||||
{"standard data tree and management account", DataManagement},
|
||||
{"global request limits", GlobalLimits},
|
||||
{"read-only bootstrap templates", ReadOnlyTemplates},
|
||||
{"alternative schemas", AlternativeSchemas},
|
||||
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
#!/bin/bash
|
||||
# shellcheck disable=SC2153
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# if container log level is trace:
|
||||
# print commands and their arguments as they are executed
|
||||
container log level eq trace && set -x
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source /tmp/openldap-test-lib.sh
|
||||
|
||||
URI="ldap://${TEST_NODES}:3890"
|
||||
USER_DN="uid=alice,${TEST_USERS_DN}"
|
||||
APP_ADMIN_DN="uid=alice-admin,${TEST_USERS_DN}"
|
||||
GROUP_DN="cn=cloud-admins,${TEST_GROUPS_DN}"
|
||||
SERVICE_DN="cn=other-service,${TEST_SERVICES_DN}"
|
||||
OUTSIDE_DN="uid=outside,${TEST_SUFFIX}"
|
||||
NEW_PASSWORD="Changed-passw0rd-2026"
|
||||
NEXT_PASSWORD="Another-passw0rd-2026"
|
||||
|
||||
eventually has_entry "${URI}" "${TEST_SUFFIX}"
|
||||
|
||||
if [ "${TEST_MANAGEMENT_MODE}" = "disabled" ]; then
|
||||
for dn in "${TEST_USERS_DN}" "${TEST_GROUPS_DN}" "${TEST_SERVICES_DN}" "${TEST_MANAGEMENT_DN}"; do
|
||||
expect_rc 32 bind_search "${URI}" "${ADMIN_DN}" -b "${dn}" -s base dn
|
||||
done
|
||||
ok 'tree and management are disabled by default'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for dn in "${TEST_USERS_DN}" "${TEST_GROUPS_DN}" "${TEST_SERVICES_DN}"; do
|
||||
expect_rc 0 bind_search "${URI}" "${ADMIN_DN}" -b "${dn}" -s base objectClass
|
||||
expect_output '^objectClass: organizationalUnit$'
|
||||
done
|
||||
|
||||
if [ "${TEST_MANAGEMENT_MODE}" = "tree" ]; then
|
||||
expect_rc 32 bind_search "${URI}" "${ADMIN_DN}" -b "${TEST_MANAGEMENT_DN}" -s base dn
|
||||
ok 'tree creation does not enable the management account'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
expect_rc 0 bind_search "${URI}" "${ADMIN_DN}" -b "${TEST_MANAGEMENT_DN}" -s base objectClass userPassword
|
||||
expect_output '^objectClass: simpleSecurityObject$'
|
||||
expect_output '^userPassword:'
|
||||
expect_rc 0 ldapwhoami -x -H "${URI}" -D "${TEST_MANAGEMENT_DN}" -w "${TEST_PASSWORD}"
|
||||
|
||||
for dn in "${USER_DN}" "${APP_ADMIN_DN}" "${OUTSIDE_DN}"; do
|
||||
uid="${dn%%,*}"
|
||||
expect_rc 0 bind_modify "${URI}" "${ADMIN_DN}" -a <<LDIF
|
||||
dn: ${dn}
|
||||
objectClass: inetOrgPerson
|
||||
uid: ${uid#uid=}
|
||||
cn: Test User
|
||||
sn: User
|
||||
displayName: Test User
|
||||
mail: ${uid#uid=}@example.org
|
||||
userPassword: ${TEST_PASSWORD}
|
||||
LDIF
|
||||
done
|
||||
|
||||
expect_rc 0 bind_modify "${URI}" "${ADMIN_DN}" -a <<LDIF
|
||||
dn: ${GROUP_DN}
|
||||
objectClass: groupOfNames
|
||||
cn: cloud-admins
|
||||
member: ${APP_ADMIN_DN}
|
||||
|
||||
dn: ${SERVICE_DN}
|
||||
objectClass: organizationalRole
|
||||
objectClass: simpleSecurityObject
|
||||
cn: other-service
|
||||
userPassword: ${TEST_PASSWORD}
|
||||
LDIF
|
||||
|
||||
# Searches use the same user/group scope and attributes as Authelia.
|
||||
expect_rc 0 bind_search "${URI}" "${TEST_MANAGEMENT_DN}" -b "${TEST_SUFFIX}" \
|
||||
'(&(uid=alice)(objectClass=inetOrgPerson))' uid displayName mail userPassword
|
||||
expect_output "^dn: ${USER_DN}$"
|
||||
expect_output '^displayName: Test User$'
|
||||
expect_output '^mail: alice@example.org$'
|
||||
expect_no_output '^userPassword:'
|
||||
expect_rc 0 bind_search "${URI}" "${TEST_MANAGEMENT_DN}" -b "${TEST_GROUPS_DN}" \
|
||||
"(&(member=${APP_ADMIN_DN})(objectClass=groupOfNames))" cn member
|
||||
expect_output '^cn: cloud-admins$'
|
||||
expect_output "^member: ${APP_ADMIN_DN}$"
|
||||
ok 'management can read user profiles and group memberships'
|
||||
|
||||
# An extended operation hashes server-side, including when ppolicy is disabled.
|
||||
# Application administrators are ordinary users for password-reset permissions.
|
||||
for dn in "${USER_DN}" "${APP_ADMIN_DN}"; do
|
||||
expect_rc 0 ldappasswd -x -H "${URI}" -D "${TEST_MANAGEMENT_DN}" -w "${TEST_PASSWORD}" -s "${NEW_PASSWORD}" "${dn}"
|
||||
expect_rc 0 ldapwhoami -x -H "${URI}" -D "${dn}" -w "${NEW_PASSWORD}"
|
||||
expect_rc 49 ldapwhoami -x -H "${URI}" -D "${dn}" -w "${TEST_PASSWORD}"
|
||||
expect_rc 0 bind_search "${URI}" "${ADMIN_DN}" -b "${dn}" -s base userPassword
|
||||
expect_output '^userPassword:: '
|
||||
done
|
||||
ok 'management resets ordinary and application-admin passwords without the old password'
|
||||
|
||||
if [ "${TEST_MANAGEMENT_MODE}" = "policy" ]; then
|
||||
# Authelia's standard LDAP Modify also needs to work with pwdSafeModify=TRUE.
|
||||
expect_rc 0 bind_modify "${URI}" "${TEST_MANAGEMENT_DN}" <<LDIF
|
||||
dn: ${USER_DN}
|
||||
changetype: modify
|
||||
replace: userPassword
|
||||
userPassword: ${NEXT_PASSWORD}
|
||||
LDIF
|
||||
expect_rc 0 ldapwhoami -x -H "${URI}" -D "${USER_DN}" -w "${NEXT_PASSWORD}"
|
||||
expect_rc 0 bind_search "${URI}" "${ADMIN_DN}" -b "${USER_DN}" -s base userPassword pwdHistory
|
||||
expect_output '^userPassword:: '
|
||||
expect_output '^pwdHistory:'
|
||||
expect_rc 0 bind_search "${URI}" "${TEST_MANAGEMENT_DN}" -b "${USER_DN}" -s base userPassword pwdHistory
|
||||
expect_no_output '^(userPassword|pwdHistory):'
|
||||
expect_rc 0 bind_search "${URI}" "cn=readonly,${TEST_SUFFIX}" -b "${USER_DN}" -s base cn pwdHistory
|
||||
expect_no_output '^pwdHistory:'
|
||||
expect_fail ldappasswd -x -H "${URI}" -D "cn=readonly,${TEST_SUFFIX}" -w "${TEST_PASSWORD}" -s "${NEW_PASSWORD}" "${USER_DN}"
|
||||
expect_fail ldappasswd -x -H "${URI}" -D "${APP_ADMIN_DN}" -w "${NEW_PASSWORD}" -a "${NEW_PASSWORD}" -s short
|
||||
expect_output 'quality|too short|Constraint violation'
|
||||
ok 'policy-enabled LDAP Modify hashes passwords and keeps history private'
|
||||
fi
|
||||
|
||||
# Write-only password access must not even permit comparison of stored hashes.
|
||||
expect_rc 50 ldapcompare -x -H "${URI}" -D "${TEST_MANAGEMENT_DN}" -w "${TEST_PASSWORD}" "${USER_DN}" "userPassword:${NEW_PASSWORD}"
|
||||
|
||||
expect_fail bind_modify "${URI}" "${TEST_MANAGEMENT_DN}" <<LDIF
|
||||
dn: ${USER_DN}
|
||||
changetype: modify
|
||||
replace: mail
|
||||
mail: takeover@example.org
|
||||
LDIF
|
||||
expect_fail bind_modify "${URI}" "${TEST_MANAGEMENT_DN}" <<LDIF
|
||||
dn: ${GROUP_DN}
|
||||
changetype: modify
|
||||
add: member
|
||||
member: ${USER_DN}
|
||||
LDIF
|
||||
|
||||
expect_fail bind_modify "${URI}" "${TEST_MANAGEMENT_DN}" -a <<LDIF
|
||||
dn: uid=forbidden,${TEST_USERS_DN}
|
||||
objectClass: inetOrgPerson
|
||||
uid: forbidden
|
||||
cn: forbidden
|
||||
sn: forbidden
|
||||
LDIF
|
||||
expect_fail bind_modify "${URI}" "${TEST_MANAGEMENT_DN}" -a <<LDIF
|
||||
dn: cn=forbidden,${TEST_GROUPS_DN}
|
||||
objectClass: groupOfNames
|
||||
cn: forbidden
|
||||
member: ${USER_DN}
|
||||
LDIF
|
||||
expect_fail ldapdelete -x -H "${URI}" -D "${TEST_MANAGEMENT_DN}" -w "${TEST_PASSWORD}" "${USER_DN}"
|
||||
expect_fail ldapdelete -x -H "${URI}" -D "${TEST_MANAGEMENT_DN}" -w "${TEST_PASSWORD}" "${GROUP_DN}"
|
||||
expect_fail ldapmodrdn -x -H "${URI}" -D "${TEST_MANAGEMENT_DN}" -w "${TEST_PASSWORD}" "${USER_DN}" uid=renamed
|
||||
ok 'management cannot edit profiles, memberships, create, delete or rename entries'
|
||||
|
||||
for dn in "${SERVICE_DN}" "${TEST_MANAGEMENT_DN}" "${OUTSIDE_DN}"; do
|
||||
expect_rc 32 bind_search "${URI}" "${TEST_MANAGEMENT_DN}" -b "${dn}" -s base '*' '+'
|
||||
expect_fail ldappasswd -x -H "${URI}" -D "${TEST_MANAGEMENT_DN}" -w "${TEST_PASSWORD}" -s "${NEW_PASSWORD}" "${dn}"
|
||||
expect_rc 0 ldapwhoami -x -H "${URI}" -D "${dn}" -w "${TEST_PASSWORD}"
|
||||
done
|
||||
expect_fail bind_modify "${URI}" "${TEST_MANAGEMENT_DN}" <<LDIF
|
||||
dn: olcDatabase={1}mdb,cn=config
|
||||
changetype: modify
|
||||
replace: olcRootPW
|
||||
olcRootPW: ${NEW_PASSWORD}
|
||||
LDIF
|
||||
expect_fail ldappasswd -x -H "${URI}" -D "${TEST_MANAGEMENT_DN}" -w "${TEST_PASSWORD}" -s "${NEW_PASSWORD}" "${ADMIN_DN}"
|
||||
expect_rc 0 ldapwhoami -x -H "${URI}" -D "${ADMIN_DN}" -w "${TEST_PASSWORD}"
|
||||
ok 'service accounts, entries outside users/groups and LDAP configuration remain protected'
|
||||
|
||||
expect_rc 0 ldappasswd -x -H "${URI}" -D "${APP_ADMIN_DN}" -w "${NEW_PASSWORD}" -a "${NEW_PASSWORD}" -s "${NEXT_PASSWORD}"
|
||||
expect_rc 0 ldapwhoami -x -H "${URI}" -D "${APP_ADMIN_DN}" -w "${NEXT_PASSWORD}"
|
||||
ok 'normal self-service password changes remain available'
|
||||
@@ -26,6 +26,15 @@ OPENLDAP_BOOTSTRAP_DATA_LAST_BIND_PRECISION=3600
|
||||
OPENLDAP_BOOTSTRAP_DATA_ROOT_DN="cn=admin,${OPENLDAP_BOOTSTRAP_SUFFIX}"
|
||||
OPENLDAP_BOOTSTRAP_DATA_ROOT_PASSWORD_HASHED=""
|
||||
|
||||
OPENLDAP_BOOTSTRAP_DATA_TREE=false
|
||||
OPENLDAP_BOOTSTRAP_DATA_TREE_USERS_DN="ou=users,${OPENLDAP_BOOTSTRAP_SUFFIX}"
|
||||
OPENLDAP_BOOTSTRAP_DATA_TREE_GROUPS_DN="ou=groups,${OPENLDAP_BOOTSTRAP_SUFFIX}"
|
||||
OPENLDAP_BOOTSTRAP_DATA_TREE_SERVICES_DN="ou=services,${OPENLDAP_BOOTSTRAP_SUFFIX}"
|
||||
|
||||
OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT=false
|
||||
OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_DN="cn=management,${OPENLDAP_BOOTSTRAP_DATA_TREE_SERVICES_DN}"
|
||||
OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_PASSWORD_HASHED=""
|
||||
|
||||
OPENLDAP_BOOTSTRAP_DATA_READONLY=false
|
||||
OPENLDAP_BOOTSTRAP_DATA_READONLY_DN="cn=readonly,${OPENLDAP_BOOTSTRAP_SUFFIX}"
|
||||
OPENLDAP_BOOTSTRAP_DATA_READONLY_PASSWORD_HASHED=""
|
||||
|
||||
@@ -15,6 +15,7 @@ ldif/
|
||||
│ ├── tls/ # TLS settings (OPENLDAP_BOOTSTRAP_TLS=true)
|
||||
│ ├── tls-required/ # Enforce TLS (OPENLDAP_BOOTSTRAP_TLS_REQUIRED=true)
|
||||
│ ├── database-readonly/ # Read-only data DB user (OPENLDAP_BOOTSTRAP_DATA_READONLY=true)
|
||||
│ ├── data-management/ # Scoped application account ACLs (OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT=true)
|
||||
│ ├── monitor-readonly/ # Read-only monitor user (OPENLDAP_BOOTSTRAP_MONITOR_READONLY=true)
|
||||
│ ├── auditlog/ # Audit file (OPENLDAP_BOOTSTRAP_AUDITLOG_ENABLED=true)
|
||||
│ ├── ppolicy/ # Password policy overlay (OPENLDAP_BOOTSTRAP_PPOLICY=true)
|
||||
@@ -26,6 +27,8 @@ ldif/
|
||||
│ └── custom/ # User-provided config LDIF files
|
||||
└── data/ # LDAP directory data (populated on first bootstrap)
|
||||
├── base/ # Root organization entry (always applied)
|
||||
├── data-tree/ # Users, groups and services OUs (OPENLDAP_BOOTSTRAP_DATA_TREE=true)
|
||||
├── data-management/ # Application account (OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT=true)
|
||||
├── database-readonly/ # Read-only account entry (OPENLDAP_BOOTSTRAP_DATA_READONLY=true)
|
||||
├── monitor-readonly/ # Read-only monitor account (OPENLDAP_BOOTSTRAP_MONITOR_READONLY=true)
|
||||
├── ppolicy/ # Password policy group and default policy (OPENLDAP_BOOTSTRAP_PPOLICY=true)
|
||||
@@ -76,6 +79,28 @@ Core OpenLDAP configuration applied on every bootstrap. Files are numbered to co
|
||||
|
||||
ACL files ending in `-close.ldif` append a final `access to * by * none` rule to deny any unmatched access, ensuring a safe default.
|
||||
|
||||
## data/data-tree and data/data-management
|
||||
|
||||
`OPENLDAP_BOOTSTRAP_DATA_TREE=true` creates `ou=users`, `ou=groups` and `ou=services`
|
||||
below the suffix. `OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT=true` creates one account,
|
||||
by default `cn=management,ou=services,<suffix>`, to read users/groups and reset user
|
||||
passwords. It cannot edit profiles or groups or read password hashes/history.
|
||||
Both options are independent and disabled by default.
|
||||
|
||||
The OU DNs, account DN and password hash are configurable through the matching
|
||||
`OPENLDAP_BOOTSTRAP_DATA_TREE_*_DN` and `OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_*` variables.
|
||||
If custom LDIFs supply the OUs, leave tree creation disabled and load them before
|
||||
`80-data-management-account.ldif`. Do not duplicate automatically created OUs.
|
||||
|
||||
## config/data-management
|
||||
|
||||
`63-data-management-acl.ldif.template` limits password writes to the users branch.
|
||||
Its `=wm` privileges allow administrative resets without the old password, without
|
||||
hash read/compare. With ppolicy, these resets bypass quality, history-reuse and
|
||||
minimum-age checks: enforce password requirements in the application.
|
||||
Service accounts and configuration remain protected; other identities keep their
|
||||
usual ACLs.
|
||||
|
||||
## config/ppolicy-readonly
|
||||
|
||||
`64-password-history-acl.ldif.template` denies `pwdHistory` access to the data
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
# Grant only write and password-administrator privileges, without hash read/compare.
|
||||
# The manage bit allows ppolicy resets without knowing the old password.
|
||||
olcAccess: to dn.subtree="${OPENLDAP_BOOTSTRAP_DATA_TREE_USERS_DN}" attrs=userPassword
|
||||
by dn.exact="${OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_DN}" =wm
|
||||
by * break
|
||||
olcAccess: to attrs=${OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_HIDDEN_ATTRIBUTES}
|
||||
by dn.exact="${OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_DN}" none
|
||||
by * break
|
||||
olcAccess: to dn.subtree="${OPENLDAP_BOOTSTRAP_DATA_TREE_USERS_DN}"
|
||||
by dn.exact="${OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_DN}" read
|
||||
by * break
|
||||
olcAccess: to dn.subtree="${OPENLDAP_BOOTSTRAP_DATA_TREE_GROUPS_DN}"
|
||||
by dn.exact="${OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_DN}" read
|
||||
by * break
|
||||
olcAccess: to dn.base="${OPENLDAP_BOOTSTRAP_SUFFIX}" attrs=entry,objectClass,o,dc
|
||||
by dn.exact="${OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_DN}" read
|
||||
by * break
|
||||
olcAccess: to *
|
||||
by dn.exact="${OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_DN}" none
|
||||
by * break
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
dn: ${OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_DN}
|
||||
objectClass: top
|
||||
objectClass: organizationalRole
|
||||
objectClass: simpleSecurityObject
|
||||
cn: ${OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_CN}
|
||||
userPassword: ${OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_PASSWORD_HASHED}
|
||||
@@ -0,0 +1,14 @@
|
||||
dn: ${OPENLDAP_BOOTSTRAP_DATA_TREE_USERS_DN}
|
||||
objectClass: top
|
||||
objectClass: organizationalUnit
|
||||
ou: ${OPENLDAP_BOOTSTRAP_DATA_TREE_USERS_OU}
|
||||
|
||||
dn: ${OPENLDAP_BOOTSTRAP_DATA_TREE_GROUPS_DN}
|
||||
objectClass: top
|
||||
objectClass: organizationalUnit
|
||||
ou: ${OPENLDAP_BOOTSTRAP_DATA_TREE_GROUPS_OU}
|
||||
|
||||
dn: ${OPENLDAP_BOOTSTRAP_DATA_TREE_SERVICES_DN}
|
||||
objectClass: top
|
||||
objectClass: organizationalUnit
|
||||
ou: ${OPENLDAP_BOOTSTRAP_DATA_TREE_SERVICES_OU}
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
# shellcheck disable=SC2034
|
||||
# shellcheck disable=SC1091
|
||||
# shellcheck disable=SC2153
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# if container log level is trace:
|
||||
# print commands and their arguments as they are executed
|
||||
container log level eq trace && set -x
|
||||
|
||||
if [ "${OPENLDAP_BOOTSTRAP_DATA_TREE}" = "true" ]; then
|
||||
|
||||
source "/container/services/openldap-bootstrap/assets/scripts/helpers/common.sh"
|
||||
|
||||
check_args "$@"
|
||||
|
||||
TYPE="$1"
|
||||
|
||||
if [ "${TYPE}" = "env" ]; then
|
||||
ENV_FILE="$2"
|
||||
|
||||
OPENLDAP_BOOTSTRAP_DATA_TREE_USERS_OU=$(get_dn_attr ou "${OPENLDAP_BOOTSTRAP_DATA_TREE_USERS_DN}")
|
||||
OPENLDAP_BOOTSTRAP_DATA_TREE_GROUPS_OU=$(get_dn_attr ou "${OPENLDAP_BOOTSTRAP_DATA_TREE_GROUPS_DN}")
|
||||
OPENLDAP_BOOTSTRAP_DATA_TREE_SERVICES_OU=$(get_dn_attr ou "${OPENLDAP_BOOTSTRAP_DATA_TREE_SERVICES_DN}")
|
||||
|
||||
prepare_env_file "${ENV_FILE}" OPENLDAP_BOOTSTRAP_DATA_TREE_USERS_OU OPENLDAP_BOOTSTRAP_DATA_TREE_GROUPS_OU OPENLDAP_BOOTSTRAP_DATA_TREE_SERVICES_OU
|
||||
|
||||
elif [ "${TYPE}" = "ldif" ]; then
|
||||
SOURCE_DIR="$2"
|
||||
OUTPUT_DIR="$3"
|
||||
|
||||
prepare_ldif_files "${SOURCE_DIR}/data-tree" "${OUTPUT_DIR}"
|
||||
fi
|
||||
|
||||
fi
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
# shellcheck disable=SC2034
|
||||
# shellcheck disable=SC1091
|
||||
# shellcheck disable=SC2153
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# if container log level is trace:
|
||||
# print commands and their arguments as they are executed
|
||||
container log level eq trace && set -x
|
||||
|
||||
if [ "${OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT}" = "true" ]; then
|
||||
|
||||
source "/container/services/openldap-bootstrap/assets/scripts/helpers/common.sh"
|
||||
|
||||
check_args "$@"
|
||||
|
||||
TYPE="$1"
|
||||
|
||||
if [ "${TYPE}" = "env" ]; then
|
||||
ENV_FILE="$2"
|
||||
|
||||
source "/container/services/openldap-bootstrap/assets/scripts/helpers/password.sh"
|
||||
|
||||
OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_CN=$(get_dn_attr cn "${OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_DN}")
|
||||
|
||||
ensure_hashed_password OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_PASSWORD_HASHED "database management user"
|
||||
|
||||
OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_HIDDEN_ATTRIBUTES="userPassword,shadowLastChange"
|
||||
if [ "${OPENLDAP_BOOTSTRAP_PPOLICY}" = "true" ]; then
|
||||
OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_HIDDEN_ATTRIBUTES+=",pwdHistory"
|
||||
fi
|
||||
|
||||
prepare_env_file "${ENV_FILE}" OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_CN OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_PASSWORD_HASHED OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_HIDDEN_ATTRIBUTES
|
||||
|
||||
elif [ "${TYPE}" = "ldif" ]; then
|
||||
SOURCE_DIR="$2"
|
||||
OUTPUT_DIR="$3"
|
||||
|
||||
prepare_ldif_files "${SOURCE_DIR}/data-management" "${OUTPUT_DIR}"
|
||||
fi
|
||||
|
||||
fi
|
||||
@@ -12,9 +12,11 @@ Scripts are executed in numeric order. Most are feature-gated by an environment
|
||||
| Script | Trigger | Description |
|
||||
|--------|---------|-------------|
|
||||
| `00-base.sh` | always | Core bootstrap: root passwords, modules, schemas and base LDIF files, including global request limits |
|
||||
| `05-data-tree.sh` | `OPENLDAP_BOOTSTRAP_DATA_TREE=true` | Creates the users, groups and services OUs |
|
||||
| `10-tls.sh` | `OPENLDAP_BOOTSTRAP_TLS=true` | Applies TLS certificate and protocol configuration |
|
||||
| `15-tls-required.sh` | `OPENLDAP_BOOTSTRAP_TLS_REQUIRED=true` | Enforces minimum 128-bit SSF (warns if TLS is not also enabled) |
|
||||
| `20-database-readonly.sh` | `OPENLDAP_BOOTSTRAP_DATA_READONLY=true` | Creates a read-only service account for the data database |
|
||||
| `25-data-management.sh` | `OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT=true` | Creates one account below the services branch, reading users/groups and resetting user passwords without hash/history access or other write permissions |
|
||||
| `30-monitor-readonly.sh` | `OPENLDAP_BOOTSTRAP_MONITOR_READONLY=true` | Creates a read-only service account for the monitor database |
|
||||
| `35-auditlog.sh` | `OPENLDAP_BOOTSTRAP_AUDITLOG_ENABLED=true` | Configures file-based auditing on the data database |
|
||||
| `40-refint.sh` | `OPENLDAP_BOOTSTRAP_REFINT=true` | Configures the referential integrity overlay and optional replacement DN |
|
||||
|
||||
Reference in New Issue
Block a user