✨ Commits existing images to git
- Implemented sync-roles.sh to synchronize Active Directory group memberships as MariaDB roles and user accounts. - Created Containerfile for building a custom ldap2pg image with patched YAML decoding. - Added provision-user-db.sh for provisioning user databases and roles based on LDAP synchronization. - Introduced a new Containerfile for MariaDB with PAM authentication via pam_ldap. - Configured PAM service for MariaDB logins and added necessary plugins for LDAP authentication. - Implemented health checks and entrypoint scripts for MariaDB and pgpool containers. - Enhanced PostgreSQL image with additional extensions and custom pg_hba.conf for improved security and functionality.
This commit is contained in:
20 files changed
+1023
No files matched your search
@@ -0,0 +1,16 @@
|
||||
ARG ALPINE=latest
|
||||
FROM alpine:${ALPINE}
|
||||
|
||||
RUN apk add --no-cache \
|
||||
bash \
|
||||
openldap-clients \
|
||||
mariadb-client \
|
||||
ca-certificates \
|
||||
&& update-ca-certificates
|
||||
|
||||
WORKDIR /workspace
|
||||
COPY --chmod=0755 sync-roles.sh /workspace/sync-roles.sh
|
||||
COPY --chmod=0755 provision-user-db.sh /workspace/provision-user-db.sh
|
||||
|
||||
ENTRYPOINT ["/bin/bash"]
|
||||
CMD ["sync-roles.sh"]
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# provision-user-db.sh — MariaDB port of the postgres provisioner.
|
||||
#
|
||||
# For every PAM login account created by sync-roles.sh: create a personal
|
||||
# database, make the user its de-facto owner, and grant the anchor roles
|
||||
# (readers/writers/ddl) the right level of access on it. Then apply the same for
|
||||
# per-database scoped roles (<db>_readers | <db>_writers | <db>_ddl).
|
||||
#
|
||||
# MariaDB makes this simpler than postgres:
|
||||
# * database == schema, and GRANT ... ON `db`.* already covers FUTURE tables,
|
||||
# so there is no ALTER DEFAULT PRIVILEGES loop to maintain.
|
||||
# * there is no PUBLIC pseudo-role and no per-database owner to fix, so the
|
||||
# "revoke PUBLIC" and "fix owner" steps simply disappear.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
: "${READERS_ROLE:?}"
|
||||
: "${WRITERS_ROLE:?}"
|
||||
: "${DDL_ROLE:?}"
|
||||
|
||||
: "${MYSQL_HOST:?}"; : "${MYSQL_PORT:?}"; : "${MYSQL_USER:?}"
|
||||
MARIADB=(mariadb --host "${MYSQL_HOST}" --port "${MYSQL_PORT}"
|
||||
--user "${MYSQL_USER}" --batch --skip-column-names)
|
||||
sql() { "${MARIADB[@]}" -e "$1"; }
|
||||
# Escaping is context-dependent: in a '...' literal a backslash escapes the next
|
||||
# character (default sql_mode), while in a `...` identifier it is literal.
|
||||
esc_str() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e "s/'/''/g"; }
|
||||
esc_id() { printf '%s' "$1" | sed 's/`/``/g'; }
|
||||
|
||||
grant_db_to_roles() {
|
||||
local db; db="$(esc_id "$1")"
|
||||
sql "GRANT SELECT ON \`${db}\`.* TO \`$(esc_id "$READERS_ROLE")\`;"
|
||||
sql "GRANT SELECT, INSERT, UPDATE, DELETE ON \`${db}\`.* TO \`$(esc_id "$WRITERS_ROLE")\`;"
|
||||
sql "GRANT ALL PRIVILEGES ON \`${db}\`.* TO \`$(esc_id "$DDL_ROLE")\`;"
|
||||
}
|
||||
|
||||
# ── 1. Per-user personal databases ──────────────────────────────────────────
|
||||
echo "[*] Discovering PAM login accounts"
|
||||
users="$(sql "SELECT User FROM mysql.user WHERE plugin='pam' AND User <> '' ORDER BY User;")"
|
||||
|
||||
if [ -z "$users" ]; then
|
||||
echo "[*] No PAM users found; nothing to provision."
|
||||
else
|
||||
while IFS= read -r u; do
|
||||
[ -z "$u" ] && continue
|
||||
edb="$(esc_id "$u")"; sdb="$(esc_str "$u")"
|
||||
echo "[*] Provisioning DB for ${u}"
|
||||
sql "CREATE DATABASE IF NOT EXISTS \`${edb}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_uca1400_ai_ci;"
|
||||
# Owner-equivalent: full rights on their own DB, with grant option.
|
||||
sql "GRANT ALL PRIVILEGES ON \`${edb}\`.* TO '${sdb}'@'%' WITH GRANT OPTION;"
|
||||
grant_db_to_roles "$u"
|
||||
done <<< "$users"
|
||||
fi
|
||||
|
||||
# ── 2. Per-database scoped roles (<db>_readers | _writers | _ddl) ────────────
|
||||
echo "[*] Applying per-database scoped role grants"
|
||||
perdb="$(sql "
|
||||
SELECT User FROM mysql.user
|
||||
WHERE is_role='Y' AND User REGEXP '_(readers|writers|ddl)\$'
|
||||
ORDER BY User;")"
|
||||
|
||||
while IFS= read -r role; do
|
||||
[ -z "$role" ] && continue
|
||||
db="${role%_*}" # strip the trailing _readers/_writers/_ddl
|
||||
kind="${role##*_}" # readers | writers | ddl
|
||||
exists="$(sql "SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$(esc_str "$db")';")"
|
||||
[ -z "$exists" ] && { echo " - skip ${role} (no database '${db}')"; continue; }
|
||||
edb="$(esc_id "$db")"; erole="$(esc_id "$role")"
|
||||
echo " - ${kind} on \`${db}\` -> ${role}"
|
||||
case "$kind" in
|
||||
readers) sql "GRANT SELECT ON \`${edb}\`.* TO \`${erole}\`;" ;;
|
||||
writers) sql "GRANT SELECT, INSERT, UPDATE, DELETE ON \`${edb}\`.* TO \`${erole}\`;" ;;
|
||||
ddl) sql "GRANT ALL PRIVILEGES ON \`${edb}\`.* TO \`${erole}\`;" ;;
|
||||
esac
|
||||
done <<< "$perdb"
|
||||
|
||||
echo "[*] Done."
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# sync-roles.sh — the ldap2pg analogue for MariaDB.
|
||||
#
|
||||
# There is no ldap2pg for MariaDB, so this reproduces the parts of ldap2pg.yaml
|
||||
# we rely on: read AD group membership and materialise it as MariaDB roles plus
|
||||
# per-user login accounts. Human accounts authenticate via PAM -> AD (no
|
||||
# password is stored in MariaDB); the anchor roles carry the actual privileges,
|
||||
# which provision-user-db.sh grants per database.
|
||||
#
|
||||
# Model (mirrors ldap2pg.yaml):
|
||||
# Global groups under OU=Global,OU=Database,OU=Groups:
|
||||
# owners | readers | writers | ddl -> members get the matching role
|
||||
# Per-DB groups under OU=Database,OU=Groups:
|
||||
# <db>_readers | <db>_writers | <db>_ddl -> scoped roles
|
||||
#
|
||||
# Those groups are engine-agnostic: they say WHAT privileges a member gets, not
|
||||
# on which server. ENGINE_GROUP_DN is the orthogonal gate — a member is only
|
||||
# provisioned here if they are also in that group, which is what keeps the
|
||||
# shared role groups from creating MariaDB accounts for Postgres-only users.
|
||||
#
|
||||
# Connection: MYSQL_HOST/PORT/USER + MYSQL_PWD (read from the environment by the
|
||||
# mariadb client, so the password never appears on the command line). Point this
|
||||
# at the backend directly, NOT MaxScale — same as ldap2pg talking to the PG
|
||||
# primary rather than through pgpool.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
: "${LDAPURI:?}"; : "${LDAPBINDDN:?}"; : "${LDAPPASSWORD:?}"
|
||||
: "${GLOBAL_BASE:?}"
|
||||
: "${PERDB_BASE:?}"
|
||||
# Set but allowed to be empty: empty disables the engine gate entirely.
|
||||
: "${ENGINE_GROUP_DN?}"
|
||||
|
||||
# LDAP_MATCHING_RULE_IN_CHAIN: matches through nested groups, so the engine gate
|
||||
# can hold org groups (Daemons, DBAs) instead of only individual users.
|
||||
ENGINE_FILTER=""
|
||||
if [ -n "$ENGINE_GROUP_DN" ]; then
|
||||
ENGINE_FILTER="(memberOf:1.2.840.113556.1.4.1941:=${ENGINE_GROUP_DN})"
|
||||
fi
|
||||
|
||||
: "${MYSQL_HOST:?}"; : "${MYSQL_PORT:?}"; : "${MYSQL_USER:?}"
|
||||
MARIADB=(mariadb --host "${MYSQL_HOST}" --port "${MYSQL_PORT}"
|
||||
--user "${MYSQL_USER}" --batch --skip-column-names)
|
||||
sql() { "${MARIADB[@]}" -e "$1"; }
|
||||
|
||||
# Escaping is context-dependent: in a '...' literal a backslash escapes the next
|
||||
# character (default sql_mode), while in a `...` identifier it is literal.
|
||||
esc_str() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e "s/'/''/g"; }
|
||||
esc_id() { printf '%s' "$1" | sed 's/`/``/g'; }
|
||||
|
||||
# Emit the sAMAccountName of every direct user member of the named AD group.
|
||||
group_members() {
|
||||
local base="$1" group="$2" dns
|
||||
dns="$(ldapsearch -LLL -o ldif-wrap=no -H "$LDAPURI" \
|
||||
-D "$LDAPBINDDN" -w "$LDAPPASSWORD" \
|
||||
-b "$base" -s one "(&(objectClass=group)(cn=${group}))" member \
|
||||
| sed -n 's/^member: //p')"
|
||||
[ -z "$dns" ] && return 0
|
||||
while IFS= read -r dn; do
|
||||
[ -z "$dn" ] && continue
|
||||
ldapsearch -LLL -o ldif-wrap=no -H "$LDAPURI" -D "$LDAPBINDDN" -w "$LDAPPASSWORD" \
|
||||
-b "$dn" -s base \
|
||||
"(&(objectCategory=person)(objectClass=user)(!(objectClass=computer))${ENGINE_FILTER})" \
|
||||
sAMAccountName 2>/dev/null | sed -n 's/^sAMAccountName: //p'
|
||||
done <<< "$dns"
|
||||
}
|
||||
|
||||
ensure_role() {
|
||||
local role; role="$(esc_id "$1")"
|
||||
sql "CREATE ROLE IF NOT EXISTS \`${role}\`;"
|
||||
}
|
||||
|
||||
ensure_user_in_role() {
|
||||
local user role
|
||||
user="$(esc_str "$1")"; role="$(esc_id "$2")"
|
||||
# PAM login account: the password is verified against AD, nothing is stored.
|
||||
sql "CREATE USER IF NOT EXISTS '${user}'@'%' IDENTIFIED VIA pam USING 'mariadb';"
|
||||
sql "GRANT \`${role}\` TO '${user}'@'%';"
|
||||
# MariaDB roles are inactive until selected, and it allows only ONE default
|
||||
# role per user (no ALL, unlike MySQL) — make this role active at login.
|
||||
sql "SET DEFAULT ROLE \`${role}\` FOR '${user}'@'%';"
|
||||
}
|
||||
|
||||
echo "[*] Ensuring anchor roles"
|
||||
for r in owners readers writers ddl; do ensure_role "$r"; done
|
||||
|
||||
echo "[*] Syncing global AD groups -> roles"
|
||||
for grp in owners readers writers ddl; do
|
||||
while IFS= read -r sam; do
|
||||
[ -z "$sam" ] && continue
|
||||
sam="$(printf '%s' "$sam" | tr '[:upper:]' '[:lower:]')"
|
||||
echo " - ${sam} -> ${grp}"
|
||||
ensure_user_in_role "$sam" "$grp"
|
||||
done < <(group_members "$GLOBAL_BASE" "$grp")
|
||||
done
|
||||
|
||||
echo "[*] Syncing per-database AD groups -> scoped roles"
|
||||
perdb_groups="$(ldapsearch -LLL -o ldif-wrap=no -H "$LDAPURI" \
|
||||
-D "$LDAPBINDDN" -w "$LDAPPASSWORD" \
|
||||
-b "$PERDB_BASE" -s one \
|
||||
'(&(objectClass=group)(|(cn=*_readers)(cn=*_writers)(cn=*_ddl)))' cn \
|
||||
| sed -n 's/^cn: //p')"
|
||||
while IFS= read -r grp; do
|
||||
[ -z "$grp" ] && continue
|
||||
role="$(printf '%s' "$grp" | tr '[:upper:]' '[:lower:]')"
|
||||
ensure_role "$role"
|
||||
while IFS= read -r sam; do
|
||||
[ -z "$sam" ] && continue
|
||||
sam="$(printf '%s' "$sam" | tr '[:upper:]' '[:lower:]')"
|
||||
echo " - ${sam} -> ${role}"
|
||||
ensure_user_in_role "$sam" "$role"
|
||||
done < <(group_members "$PERDB_BASE" "$grp")
|
||||
done <<< "$perdb_groups"
|
||||
|
||||
echo "[*] Role sync complete."
|
||||
Reference in new issue
Block a user