✨ 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,10 @@
|
||||
ALPINE_VERSION=3.24
|
||||
GO_VERSION=1.26.3
|
||||
|
||||
PG_MAJOR=18
|
||||
POSTGIS_MAJOR=3
|
||||
VCHORD_VERSION=1.1.1
|
||||
PGPOOL_VERSION=4.7.2
|
||||
LDAP2PG_VERSION=6.6.0
|
||||
|
||||
MARIADB_VERSION=12
|
||||
@@ -0,0 +1,50 @@
|
||||
name: image-builder
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: containers.kritikos.io/system/postgres-extended:${PG_MAJOR:-18}
|
||||
build:
|
||||
context: postgres
|
||||
dockerfile: Containerfile
|
||||
args:
|
||||
- PG_MAJOR=${PG_MAJOR:-18}
|
||||
- POSTGIS_MAJOR=${POSTGIS_MAJOR:-3}
|
||||
- VCHORD_VERSION=${VCHORD_VERSION:-1.1.1}
|
||||
|
||||
ldap2pg:
|
||||
image: containers.kritikos.io/system/ldap2pg:${LDAP2PG_VERSION:-6.6.0}
|
||||
build:
|
||||
context: ldap2pg
|
||||
dockerfile: Containerfile
|
||||
args:
|
||||
- ALPINE=${ALPINE_VERSION:-latest}
|
||||
- GO_VERSION=${GO_VERSION:-1.26.3}
|
||||
- LDAP2PG_VERSION=${LDAP2PG_VERSION:-6.6.0}
|
||||
|
||||
pgpool:
|
||||
container_name: pgpool
|
||||
image: containers.kritikos.io/system/pgpool:${PGPOOL_VERSION:-4.7.2}
|
||||
build:
|
||||
context: pgpool
|
||||
dockerfile: Containerfile
|
||||
args:
|
||||
- ALPINE=${ALPINE_VERSION:-latest}
|
||||
- PG_MAJOR=${PG_MAJOR:-18}
|
||||
- PGPOOL_VERSION=${PGPOOL_VERSION:-4.7.2}
|
||||
|
||||
mariadb:
|
||||
image: containers.kritikos.io/system/mariadb-extended:${MARIADB_VERSION:-12}
|
||||
build:
|
||||
context: mariadb
|
||||
dockerfile: Containerfile
|
||||
args:
|
||||
- MARIADB_VERSION=${MARIADB_VERSION:-12}
|
||||
|
||||
ldap2mariadb:
|
||||
container_name: ldap2mariadb
|
||||
image: containers.kritikos.io/system/ldap2mariadb:latest
|
||||
build:
|
||||
context: ldap2mariadb
|
||||
dockerfile: Containerfile
|
||||
args:
|
||||
- ALPINE=${ALPINE_VERSION:-latest}
|
||||
@@ -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."
|
||||
@@ -0,0 +1,61 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
ARG ALPINE=latest
|
||||
ARG GO_VERSION=1.26.3
|
||||
ARG LDAP2PG_VERSION=6.6.0
|
||||
|
||||
# ─── Build stage ───────────────────────────────────────────────────────
|
||||
# ldap2pg 6.6.0 introduced strict YAML decoding but left the `joins`
|
||||
# subsearch key half-renamed: the normalizer rewrites it to `subsearches`
|
||||
# while the mapstructure tag on the target struct still says `joins`, so
|
||||
# any config using the (documented) `joins:` key is rejected as
|
||||
# "invalid keys: subsearches".
|
||||
#
|
||||
# The bug lives in a single struct tag; we grab the upstream source
|
||||
# tarball for the pinned tag, patch that one line, and rebuild.
|
||||
# Remove the whole builder stage and switch back to the released
|
||||
# .apk once upstream ships the fix.
|
||||
FROM golang:${GO_VERSION}-alpine AS builder
|
||||
ARG LDAP2PG_VERSION
|
||||
|
||||
RUN apk add --no-cache curl tar
|
||||
|
||||
WORKDIR /src
|
||||
RUN curl -fsSL "https://github.com/dalibo/ldap2pg/archive/refs/tags/v${LDAP2PG_VERSION}.tar.gz" \
|
||||
| tar -xz --strip-components=1
|
||||
|
||||
# Re-align the struct tag with the normalizer's canonical key name.
|
||||
RUN grep -q 'mapstructure:"joins"' internal/ldap/search.go \
|
||||
&& sed -i 's|mapstructure:"joins"|mapstructure:"subsearches"|' internal/ldap/search.go
|
||||
|
||||
RUN CGO_ENABLED=0 go build -trimpath \
|
||||
-ldflags "-s -w -X github.com/dalibo/ldap2pg/v6/internal/cmd.Version=${LDAP2PG_VERSION}" \
|
||||
-o /out/ldap2pg .
|
||||
|
||||
# ─── Runtime stage ─────────────────────────────────────────────────────
|
||||
FROM alpine:${ALPINE}
|
||||
|
||||
ARG LDAP2PG_VERSION
|
||||
|
||||
ENV LANG=en_US.utf8
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
RUN mkdir -p /docker-entrypoint.d \
|
||||
&& apk add --no-cache \
|
||||
bash \
|
||||
ca-certificates \
|
||||
openldap-clients \
|
||||
postgresql-client \
|
||||
&& update-ca-certificates
|
||||
|
||||
COPY --from=builder /out/ldap2pg /usr/bin/ldap2pg
|
||||
|
||||
# Fetch the upstream docker entrypoint pinned to the same release tag.
|
||||
ADD --chmod=0755 https://raw.githubusercontent.com/dalibo/ldap2pg/v${LDAP2PG_VERSION}/docker/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
COPY --chmod=0755 provision-user-db.sh /workspace/provision-user-db.sh
|
||||
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
|
||||
RUN /usr/local/bin/docker-entrypoint.sh --version
|
||||
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
: "${BOOTSTRAP_DB:?}"
|
||||
: "${ADMIN_DB:?}"
|
||||
: "${USERS_ROLE:?}" # group whose members each get a personal DB
|
||||
: "${READERS_ROLE:?}"
|
||||
: "${WRITERS_ROLE:?}"
|
||||
: "${DDL_ROLE:?}"
|
||||
: "${HARDEN_DB_PUBLIC:?}" # 1=yes: revoke PUBLIC connect on each user DB
|
||||
|
||||
psql_bootstrap() { psql -v ON_ERROR_STOP=1 -d "$BOOTSTRAP_DB" "$@"; }
|
||||
psql_userdb() { psql -v ON_ERROR_STOP=1 -d "$1" "${@:2}"; }
|
||||
|
||||
qi() {
|
||||
local s="$1"
|
||||
s="${s//\"/\"\"}"
|
||||
printf '"%s"' "$s"
|
||||
}
|
||||
|
||||
# ── 1. Ensure the admin (ldap2pg) database exists ──────────────────────────
|
||||
echo "[*] Ensuring admin database exists: ${ADMIN_DB}"
|
||||
psql_bootstrap -Atc "SELECT 1 FROM pg_database WHERE datname='${ADMIN_DB}';" | grep -qx 1 || \
|
||||
psql_bootstrap -c "CREATE DATABASE $(qi "$ADMIN_DB") OWNER postgres;"
|
||||
|
||||
# ── 2. Discover LDAP-synced login roles ────────────────────────────────────
|
||||
echo "[*] Discovering user roles from PG group: ${USERS_ROLE}"
|
||||
users="$(psql_bootstrap -Atc "
|
||||
SELECT r.rolname
|
||||
FROM pg_roles r
|
||||
JOIN pg_auth_members m ON m.member = r.oid
|
||||
JOIN pg_roles p ON p.oid = m.roleid
|
||||
WHERE p.rolname = '${USERS_ROLE}'
|
||||
AND r.rolcanlogin
|
||||
AND r.rolname <> 'postgres'
|
||||
AND r.rolname !~ '^pg_'
|
||||
ORDER BY 1;
|
||||
")"
|
||||
|
||||
if [[ -z "${users}" ]]; then
|
||||
echo "[*] No members in '${USERS_ROLE}'. Nothing to provision."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── 3. Per-user database provisioning ──────────────────────────────────────
|
||||
while IFS= read -r u; do
|
||||
[[ -z "$u" ]] && continue
|
||||
echo
|
||||
echo "[*] Provisioning DB for user: ${u}"
|
||||
|
||||
# ── 3a. Create the database if missing ───────────────────────────────────
|
||||
if ! psql_bootstrap -Atc "SELECT 1 FROM pg_database WHERE datname='${u}';" | grep -qx 1; then
|
||||
echo " - creating database $(qi "$u") owned by $(qi "$u")"
|
||||
psql_bootstrap -c "CREATE DATABASE $(qi "$u") OWNER $(qi "$u");"
|
||||
else
|
||||
echo " - database exists"
|
||||
fi
|
||||
|
||||
# ── 3b. Fix ownership if needed ──────────────────────────────────────────
|
||||
current_owner="$(psql_bootstrap -Atc "
|
||||
SELECT pg_get_userbyid(datdba)
|
||||
FROM pg_database
|
||||
WHERE datname='${u}';
|
||||
")"
|
||||
if [[ "${current_owner}" != "${u}" ]]; then
|
||||
echo " - fixing db owner: ${current_owner} -> ${u}"
|
||||
psql_bootstrap -c "ALTER DATABASE $(qi "$u") OWNER TO $(qi "$u");"
|
||||
else
|
||||
echo " - db owner OK"
|
||||
fi
|
||||
|
||||
# ── 3c. Harden: revoke PUBLIC connect ────────────────────────────────────
|
||||
if [[ "${HARDEN_DB_PUBLIC}" == "1" ]]; then
|
||||
psql_bootstrap -c "REVOKE ALL ON DATABASE $(qi "$u") FROM PUBLIC;"
|
||||
fi
|
||||
|
||||
# ── 3d. Owner DB-level grants ────────────────────────────────────────────
|
||||
psql_bootstrap -c "GRANT CONNECT, TEMPORARY, CREATE ON DATABASE $(qi "$u") TO $(qi "$u");"
|
||||
|
||||
# ── 3e. Per-owner schema + public schema grants ──────────────────────────
|
||||
psql_userdb "$u" -c "GRANT USAGE, CREATE ON SCHEMA public TO $(qi "$u");" >/dev/null
|
||||
|
||||
# ── 3f. Fine-grained role grants (readers / writers / ddl) ───────────────
|
||||
#
|
||||
# DB-level CONNECT so the marker roles can reach the database at all.
|
||||
for role in "$READERS_ROLE" "$WRITERS_ROLE" "$DDL_ROLE"; do
|
||||
psql_bootstrap -c "GRANT CONNECT ON DATABASE $(qi "$u") TO $(qi "$role");" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# DB-level CREATE for the DDL role: required to install trusted extensions
|
||||
# (e.g. CREATE EXTENSION vector) and to create schemas.
|
||||
psql_bootstrap -c "GRANT CREATE ON DATABASE $(qi "$u") TO $(qi "$DDL_ROLE");" 2>/dev/null || true
|
||||
|
||||
# Schema-level grants inside the user's database
|
||||
for schema in public; do
|
||||
# readers → read-only
|
||||
psql_userdb "$u" -c "GRANT USAGE ON SCHEMA $(qi "$schema") TO $(qi "$READERS_ROLE");" >/dev/null
|
||||
psql_userdb "$u" -c "GRANT SELECT ON ALL TABLES IN SCHEMA $(qi "$schema") TO $(qi "$READERS_ROLE");" >/dev/null
|
||||
psql_userdb "$u" -c "GRANT SELECT ON ALL SEQUENCES IN SCHEMA $(qi "$schema") TO $(qi "$READERS_ROLE");" >/dev/null
|
||||
|
||||
# writers → read + DML
|
||||
psql_userdb "$u" -c "GRANT USAGE ON SCHEMA $(qi "$schema") TO $(qi "$WRITERS_ROLE");" >/dev/null
|
||||
psql_userdb "$u" -c "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA $(qi "$schema") TO $(qi "$WRITERS_ROLE");" >/dev/null
|
||||
psql_userdb "$u" -c "GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA $(qi "$schema") TO $(qi "$WRITERS_ROLE");" >/dev/null
|
||||
|
||||
# ddl → full DDL (create/alter/drop via ownership proxy)
|
||||
psql_userdb "$u" -c "GRANT USAGE, CREATE ON SCHEMA $(qi "$schema") TO $(qi "$DDL_ROLE");" >/dev/null
|
||||
psql_userdb "$u" -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA $(qi "$schema") TO $(qi "$DDL_ROLE");" >/dev/null
|
||||
psql_userdb "$u" -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA $(qi "$schema") TO $(qi "$DDL_ROLE");" >/dev/null
|
||||
psql_userdb "$u" -c "GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA $(qi "$schema") TO $(qi "$DDL_ROLE");" >/dev/null
|
||||
|
||||
# ── Default privileges: apply to objects created in the future ────────
|
||||
psql_userdb "$u" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA $(qi "$schema") GRANT SELECT ON TABLES TO $(qi "$READERS_ROLE");" >/dev/null
|
||||
psql_userdb "$u" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA $(qi "$schema") GRANT SELECT ON SEQUENCES TO $(qi "$READERS_ROLE");" >/dev/null
|
||||
|
||||
psql_userdb "$u" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA $(qi "$schema") GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $(qi "$WRITERS_ROLE");" >/dev/null
|
||||
psql_userdb "$u" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA $(qi "$schema") GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO $(qi "$WRITERS_ROLE");" >/dev/null
|
||||
|
||||
psql_userdb "$u" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA $(qi "$schema") GRANT ALL PRIVILEGES ON TABLES TO $(qi "$DDL_ROLE");" >/dev/null
|
||||
psql_userdb "$u" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA $(qi "$schema") GRANT ALL PRIVILEGES ON SEQUENCES TO $(qi "$DDL_ROLE");" >/dev/null
|
||||
psql_userdb "$u" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA $(qi "$schema") GRANT ALL PRIVILEGES ON FUNCTIONS TO $(qi "$DDL_ROLE");" >/dev/null
|
||||
done
|
||||
|
||||
echo " - DB + schema grants OK (owner, readers, writers, ddl)"
|
||||
done <<< "${users}"
|
||||
|
||||
# ── 4. Per-database scoped role grants ─────────────────────────────────────
|
||||
# Discover PG roles matching <dbname>_readers / <dbname>_writers / <dbname>_ddl
|
||||
# and grant them ONLY on the corresponding database.
|
||||
echo
|
||||
echo "[*] Applying per-database scoped grants …"
|
||||
|
||||
apply_perdb_grants() {
|
||||
local db="$1" role="$2" kind="$3"
|
||||
|
||||
# DB-level CONNECT
|
||||
psql_bootstrap -c "GRANT CONNECT ON DATABASE $(qi "$db") TO $(qi "$role");" 2>/dev/null || true
|
||||
|
||||
# DDL roles also need DB-level CREATE to install trusted extensions
|
||||
# (e.g. CREATE EXTENSION vector) and to create schemas.
|
||||
if [[ "$kind" == "ddl" ]]; then
|
||||
psql_bootstrap -c "GRANT CREATE ON DATABASE $(qi "$db") TO $(qi "$role");" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
for schema in public "$db"; do
|
||||
case "$kind" in
|
||||
readers)
|
||||
psql_userdb "$db" -c "GRANT USAGE ON SCHEMA $(qi "$schema") TO $(qi "$role");" >/dev/null
|
||||
psql_userdb "$db" -c "GRANT SELECT ON ALL TABLES IN SCHEMA $(qi "$schema") TO $(qi "$role");" >/dev/null
|
||||
psql_userdb "$db" -c "GRANT SELECT ON ALL SEQUENCES IN SCHEMA $(qi "$schema") TO $(qi "$role");" >/dev/null
|
||||
psql_userdb "$db" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA $(qi "$schema") GRANT SELECT ON TABLES TO $(qi "$role");" >/dev/null
|
||||
psql_userdb "$db" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA $(qi "$schema") GRANT SELECT ON SEQUENCES TO $(qi "$role");" >/dev/null
|
||||
;;
|
||||
writers)
|
||||
psql_userdb "$db" -c "GRANT USAGE ON SCHEMA $(qi "$schema") TO $(qi "$role");" >/dev/null
|
||||
psql_userdb "$db" -c "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA $(qi "$schema") TO $(qi "$role");" >/dev/null
|
||||
psql_userdb "$db" -c "GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA $(qi "$schema") TO $(qi "$role");" >/dev/null
|
||||
psql_userdb "$db" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA $(qi "$schema") GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $(qi "$role");" >/dev/null
|
||||
psql_userdb "$db" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA $(qi "$schema") GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO $(qi "$role");" >/dev/null
|
||||
;;
|
||||
ddl)
|
||||
psql_userdb "$db" -c "GRANT USAGE, CREATE ON SCHEMA $(qi "$schema") TO $(qi "$role");" >/dev/null
|
||||
psql_userdb "$db" -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA $(qi "$schema") TO $(qi "$role");" >/dev/null
|
||||
psql_userdb "$db" -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA $(qi "$schema") TO $(qi "$role");" >/dev/null
|
||||
psql_userdb "$db" -c "GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA $(qi "$schema") TO $(qi "$role");" >/dev/null
|
||||
psql_userdb "$db" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA $(qi "$schema") GRANT ALL PRIVILEGES ON TABLES TO $(qi "$role");" >/dev/null
|
||||
psql_userdb "$db" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA $(qi "$schema") GRANT ALL PRIVILEGES ON SEQUENCES TO $(qi "$role");" >/dev/null
|
||||
psql_userdb "$db" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA $(qi "$schema") GRANT ALL PRIVILEGES ON FUNCTIONS TO $(qi "$role");" >/dev/null
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Query PG for roles whose name matches <something>_readers|_writers|_ddl
|
||||
# and where the <something> part matches an existing database.
|
||||
perdb_roles="$(psql_bootstrap -Atc "
|
||||
SELECT r.rolname, d.datname,
|
||||
CASE
|
||||
WHEN r.rolname LIKE '%\_readers' THEN 'readers'
|
||||
WHEN r.rolname LIKE '%\_writers' THEN 'writers'
|
||||
WHEN r.rolname LIKE '%\_ddl' THEN 'ddl'
|
||||
END AS kind
|
||||
FROM pg_roles r
|
||||
JOIN pg_auth_members m ON m.member = r.oid
|
||||
JOIN pg_roles p ON p.oid = m.roleid AND p.rolname = 'ldap_roles'
|
||||
JOIN pg_database d ON d.datname = regexp_replace(r.rolname, '_(readers|writers|ddl)$', '')
|
||||
WHERE r.rolname ~ '_(readers|writers|ddl)$'
|
||||
AND NOT r.rolcanlogin
|
||||
ORDER BY 1;
|
||||
")" || true
|
||||
|
||||
if [[ -n "${perdb_roles}" ]]; then
|
||||
while IFS='|' read -r rolname dbname kind; do
|
||||
[[ -z "$rolname" ]] && continue
|
||||
echo " - granting ${kind} on DB '${dbname}' to role '${rolname}'"
|
||||
apply_perdb_grants "$dbname" "$rolname" "$kind"
|
||||
done <<< "${perdb_roles}"
|
||||
else
|
||||
echo " - no per-database scoped roles found"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "[*] Done."
|
||||
@@ -0,0 +1,56 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
ARG MARIADB_VERSION=latest
|
||||
|
||||
FROM mariadb:${MARIADB_VERSION}
|
||||
|
||||
# ── AD/LDAP authentication via PAM ──────────────────────────────────────────
|
||||
# MariaDB Community has no native LDAP auth plugin, so AD logins go through the
|
||||
# auth_pam plugin -> a PAM "mariadb" service -> daemonless pam_ldap doing an
|
||||
# LDAP search+bind against Active Directory. This is the analogue of the pgpool
|
||||
# `ldap` method in pool_hba.conf; deliberately NO SSSD/nslcd daemon is added.
|
||||
#
|
||||
# libpam-ldap is the daemonless PAM/LDAP module. The auth_pam plugin ships with
|
||||
# recent MariaDB; the separate plugin package (older layouts) is installed
|
||||
# best-effort and also fixes the setuid auth_pam_tool helper permissions.
|
||||
# Either source must leave auth_pam.so in place: 10-plugins.cnf loads it
|
||||
# unconditionally, so a missing .so would only surface as a server that refuses
|
||||
# to start.
|
||||
RUN set -eu; \
|
||||
apt-get update; \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
libpam-ldap \
|
||||
ca-certificates \
|
||||
gettext-base; \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
mariadb-plugin-auth-pam || true; \
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
plugin=/usr/lib/mysql/plugin/auth_pam.so; \
|
||||
[ -e "$plugin" ] || { echo "auth_pam: missing $plugin" >&2; exit 1; }
|
||||
|
||||
# ── Layered configuration ───────────────────────────────────────────────────
|
||||
# Mirror the postgres image: image-owned defaults load first, user overrides
|
||||
# last (last-wins). The stock image already reads /etc/mysql/conf.d and
|
||||
# /etc/mysql/mariadb.conf.d; we append two include dirs AFTER those so:
|
||||
# image.conf.d -> our defaults (override the base image's 50-server.cnf)
|
||||
# site.conf.d -> a clean "mount your *.cnf here" area (wins over everything)
|
||||
RUN set -eu; \
|
||||
mkdir -p /etc/mysql/image.conf.d /etc/mysql/site.conf.d; \
|
||||
{ \
|
||||
echo '!includedir /etc/mysql/image.conf.d/'; \
|
||||
echo '!includedir /etc/mysql/site.conf.d/'; \
|
||||
} >> /etc/mysql/mariadb.cnf
|
||||
COPY conf.d/ /etc/mysql/image.conf.d/
|
||||
|
||||
# PAM service definition for MariaDB logins (auth_pam calls service "mariadb").
|
||||
COPY pam.d/mariadb /etc/pam.d/mariadb
|
||||
|
||||
# Entrypoint wrapper: renders /etc/pam_ldap.conf from env (keeps the AD bind
|
||||
# secret out of the image), then hands off to the stock mariadb entrypoint.
|
||||
COPY --chmod=0755 docker-entrypoint.sh /usr/local/bin/ldap-entrypoint.sh
|
||||
|
||||
# Report health via the image's bundled healthcheck helper.
|
||||
HEALTHCHECK --interval=10s --timeout=5s --retries=5 --start-period=15s \
|
||||
CMD healthcheck.sh --connect --innodb_initialized || exit 1
|
||||
|
||||
ENTRYPOINT ["ldap-entrypoint.sh"]
|
||||
CMD ["mariadbd"]
|
||||
@@ -0,0 +1,11 @@
|
||||
# Image-owned plugin preload (analogue of the postgres image's
|
||||
# shared_preload_libraries in 10-extensions.conf). auth_pam provides Active
|
||||
# Directory logins via PAM -> pam_ldap.
|
||||
[mariadb]
|
||||
plugin_load_add = auth_pam
|
||||
|
||||
# Make auth_pam use the mysql_clear_password protocol instead of the interactive
|
||||
# "dialog" one. Required for MaxScale: its clear_pw_passthrough only speaks
|
||||
# mysql_native_password / mysql_clear_password to backends, never dialog. The
|
||||
# password is sent in cleartext, so TLS must be on (99-tls.cnf) — it is.
|
||||
pam_use_cleartext_plugin = 1
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Render pam_ldap's config from the environment so the AD bind credentials are
|
||||
# never baked into the image. pam_ldap performs a search+bind against AD: bind
|
||||
# as the service account, find the user by ${LDAP_LOGIN_ATTR}, then re-bind as
|
||||
# that DN with the supplied password to verify it — the same shape as pgpool's
|
||||
# `ldap` method (see pool_hba.conf).
|
||||
: "${LDAPURI:?LDAPURI is required (e.g. ldaps://hecate.ad.kritikos.io)}"
|
||||
: "${LDAP_BASE:?LDAP_BASE is required (e.g. DC=ad,DC=kritikos,DC=io)}"
|
||||
: "${LDAPBINDDN:?LDAPBINDDN is required (AD service/bind account)}"
|
||||
: "${LDAPPASSWORD:?LDAPPASSWORD is required}"
|
||||
: "${LDAP_LOGIN_ATTR:?LDAP_LOGIN_ATTR is required (e.g. sAMAccountName)}"
|
||||
# demand = validate the AD cert against the trust store; relax to 'allow' only
|
||||
# for testing, or add your internal CA to the image and keep 'demand'.
|
||||
: "${LDAP_TLS_REQCERT:?LDAP_TLS_REQCERT is required (demand|allow|never)}"
|
||||
|
||||
umask 077
|
||||
# pam_ldap.so on this image reads /etc/ldap.conf (the PADL default path), and
|
||||
# libpam-ldap ships a placeholder there (base dc=example,dc=net) that otherwise
|
||||
# shadows our config and makes every login fail. Render both paths.
|
||||
for conf in /etc/ldap.conf /etc/pam_ldap.conf; do
|
||||
cat > "$conf" <<EOF
|
||||
uri ${LDAPURI}
|
||||
base ${LDAP_BASE}
|
||||
binddn ${LDAPBINDDN}
|
||||
bindpw ${LDAPPASSWORD}
|
||||
scope sub
|
||||
referrals no
|
||||
pam_login_attribute ${LDAP_LOGIN_ATTR}
|
||||
pam_filter objectclass=User
|
||||
tls_cacertdir /etc/ssl/certs
|
||||
tls_reqcert ${LDAP_TLS_REQCERT}
|
||||
EOF
|
||||
chmod 600 "$conf"
|
||||
done
|
||||
|
||||
# Grant mysql read access to the host TLS key (mode 0640, owned by GID
|
||||
# ${CERTS_GID}). compose exposes that GID via `group_add`, but the stock
|
||||
# entrypoint drops root -> mysql with gosu, which rebuilds mysql's groups from
|
||||
# /etc/group and discards the compose-added GID — mysql then can't read ssl_key
|
||||
# and MariaDB aborts with "Unable to get private key". While still root, put
|
||||
# mysql in a group with that GID so it survives the privilege drop.
|
||||
: "${CERTS_GID:?CERTS_GID is required (GID owning the host TLS key)}"
|
||||
if [ "$(id -u)" = '0' ]; then
|
||||
if getent group "${CERTS_GID}" >/dev/null; then
|
||||
certgroup="$(getent group "${CERTS_GID}" | cut -d: -f1)"
|
||||
else
|
||||
groupadd -g "${CERTS_GID}" hostcerts
|
||||
certgroup=hostcerts
|
||||
fi
|
||||
usermod -aG "$certgroup" mysql
|
||||
else
|
||||
echo "ldap-entrypoint: not root (uid $(id -u)); left group membership unchanged (CERTS_GID=${CERTS_GID})" >&2
|
||||
fi
|
||||
|
||||
exec docker-entrypoint.sh "$@"
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Runs once on first cluster init (empty datadir), the analogue of
|
||||
# init_pgpool_user.sql. Creates the infrastructure accounts (MaxScale monitor +
|
||||
# service, and the replication user) on the backend. All use native password
|
||||
# auth (NOT pam) — they are service accounts, not human AD logins. Passwords
|
||||
# come from the environment so nothing secret is committed to a .sql file, and
|
||||
# the root password goes via MYSQL_PWD (read by the client) rather than -p so it
|
||||
# never appears in the process list.
|
||||
|
||||
: "${MARIADB_ROOT_PASSWORD:?}"
|
||||
: "${MAXSCALE_MONITOR_PASSWORD:?}"
|
||||
: "${MAXSCALE_SERVICE_PASSWORD:?}"
|
||||
: "${MARIADB_REPL_PASSWORD:?}"
|
||||
|
||||
MYSQL_PWD="${MARIADB_ROOT_PASSWORD}" mariadb --protocol=socket -uroot <<SQL
|
||||
-- Monitor account: mariadbmon health checks, replication state, and failover.
|
||||
CREATE USER IF NOT EXISTS 'maxscale_monitor'@'%' IDENTIFIED BY '${MAXSCALE_MONITOR_PASSWORD}';
|
||||
GRANT BINLOG MONITOR, SLAVE MONITOR, REPLICATION SLAVE, REPLICATION MASTER ADMIN,
|
||||
REPLICATION SLAVE ADMIN, READ_ONLY ADMIN, RELOAD, PROCESS,
|
||||
SHOW DATABASES, EVENT
|
||||
ON *.* TO 'maxscale_monitor'@'%';
|
||||
|
||||
-- Service account: reads the user tables so MaxScale can authenticate clients
|
||||
-- and route them (pass-through auth defers the real check to the backend PAM).
|
||||
CREATE USER IF NOT EXISTS 'maxscale_service'@'%' IDENTIFIED BY '${MAXSCALE_SERVICE_PASSWORD}';
|
||||
GRANT SELECT ON mysql.* TO 'maxscale_service'@'%';
|
||||
GRANT SHOW DATABASES ON *.* TO 'maxscale_service'@'%';
|
||||
|
||||
-- Replication account: the replica's IO thread uses this to stream the binlog.
|
||||
CREATE USER IF NOT EXISTS 'repl'@'%' IDENTIFIED BY '${MARIADB_REPL_PASSWORD}';
|
||||
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
|
||||
SQL
|
||||
@@ -0,0 +1,6 @@
|
||||
# PAM service for MariaDB auth_pam logins (accounts created IDENTIFIED VIA pam
|
||||
# USING 'mariadb'). Authenticate + authorize against Active Directory through
|
||||
# the daemonless pam_ldap module; its connection settings live in
|
||||
# /etc/pam_ldap.conf, rendered at container start from the environment.
|
||||
auth required pam_ldap.so
|
||||
account required pam_ldap.so
|
||||
@@ -0,0 +1,79 @@
|
||||
ARG ALPINE=latest
|
||||
ARG PG_MAJOR=18
|
||||
ARG PGPOOL_VERSION=4.7.2
|
||||
|
||||
FROM alpine:${ALPINE} AS base
|
||||
|
||||
FROM base AS builder
|
||||
ARG PG_MAJOR
|
||||
ARG PGPOOL_VERSION
|
||||
|
||||
RUN apk add --no-cache \
|
||||
build-base \
|
||||
linux-headers \
|
||||
postgresql${PG_MAJOR}-dev \
|
||||
openssl-dev \
|
||||
openldap-dev \
|
||||
libmemcached-dev \
|
||||
wget
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
RUN wget -qO- https://www.pgpool.net/source/pgpool-II-${PGPOOL_VERSION}.tar.gz | tar xz
|
||||
|
||||
WORKDIR /tmp/pgpool-II-${PGPOOL_VERSION}
|
||||
RUN ./configure \
|
||||
--prefix=/usr \
|
||||
--sysconfdir=/etc/pgpool-II \
|
||||
--with-openssl \
|
||||
--with-ldap \
|
||||
--with-memcached=/usr \
|
||||
&& make -j"$(nproc)" \
|
||||
&& make install DESTDIR=/build
|
||||
|
||||
WORKDIR /tmp/pgpool-II-${PGPOOL_VERSION}/src/sql
|
||||
RUN make -j"$(nproc)" && make install DESTDIR=/build
|
||||
|
||||
# ── Runtime stage ──────────────────────────────────────────────────────────
|
||||
FROM base
|
||||
ARG PG_MAJOR
|
||||
|
||||
RUN apk add --no-cache \
|
||||
bash \
|
||||
postgresql${PG_MAJOR}-client \
|
||||
libldap \
|
||||
openssl \
|
||||
libmemcached-libs \
|
||||
ca-certificates \
|
||||
&& update-ca-certificates
|
||||
|
||||
# Copy built binaries, libs and default configs
|
||||
COPY --from=builder /build/usr/ /usr/
|
||||
COPY --from=builder /build/etc/ /etc/
|
||||
|
||||
# Runtime directories
|
||||
RUN mkdir -p /var/run/pgpool /var/log/pgpool /etc/pgpool-II \
|
||||
&& adduser -D -h /var/run/pgpool pgpool \
|
||||
&& chown pgpool:pgpool /var/run/pgpool /var/log/pgpool
|
||||
|
||||
# Ship a minimal default config (overridden by bind-mount at runtime)
|
||||
RUN set -eu; \
|
||||
cp /etc/pgpool-II/pgpool.conf.sample /etc/pgpool-II/pgpool.conf; \
|
||||
cp /etc/pgpool-II/pool_hba.conf.sample /etc/pgpool-II/pool_hba.conf; \
|
||||
cp /etc/pgpool-II/pcp.conf.sample /etc/pgpool-II/pcp.conf
|
||||
|
||||
EXPOSE 9999 9898
|
||||
|
||||
# Green means pgpool can hand a client a backend connection, i.e. at least one
|
||||
# node is attached AND a primary is identifiable -- NOT that the cluster is
|
||||
# intact: a degenerated standby still reports healthy. Retries exist to ride out
|
||||
# the ~1 health_check_period window where failover makes connections fail.
|
||||
HEALTHCHECK --interval=10s --timeout=5s --retries=5 --start-period=10s \
|
||||
CMD pg_isready -h 127.0.0.1 -p 9999 || exit 1
|
||||
|
||||
# AES passwords in pool_passwd: mount the key anywhere and set PGPOOLKEYFILE to
|
||||
# it; the entrypoint re-stages it at the permissions pgpool insists on.
|
||||
COPY --chmod=0755 docker-entrypoint.sh /usr/local/bin/
|
||||
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
CMD ["pgpool", "-n", "-f", "/etc/pgpool-II/pgpool.conf", "-F", "/etc/pgpool-II/pcp.conf"]
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ── Clean up stale PID files from previous runs ───────────────────────────
|
||||
rm -f /tmp/pgpool.pid /var/run/pgpool/pgpool.pid
|
||||
|
||||
exec "$@"
|
||||
@@ -0,0 +1,115 @@
|
||||
ARG PG_MAJOR=18
|
||||
ARG POSTGIS_MAJOR=3
|
||||
ARG VCHORD_VERSION=1.1.1
|
||||
|
||||
FROM postgres:${PG_MAJOR:-18} AS base-image
|
||||
WORKDIR /tmp
|
||||
|
||||
FROM base-image AS build-deps
|
||||
ARG PG_MAJOR
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
wget \
|
||||
curl \
|
||||
build-essential \
|
||||
cmake \
|
||||
libkrb5-dev \
|
||||
libicu-dev \
|
||||
postgresql-server-dev-$PG_MAJOR \
|
||||
&& apt-get install --reinstall -y ca-certificates \
|
||||
&& update-ca-certificates
|
||||
|
||||
FROM build-deps AS downloads
|
||||
WORKDIR /tmp
|
||||
ARG PG_MAJOR
|
||||
ARG VCHORD_VERSION
|
||||
RUN wget https://github.com/tensorchord/VectorChord/releases/download/${VCHORD_VERSION}/postgresql-${PG_MAJOR}-vchord_${VCHORD_VERSION}-1_amd64.deb
|
||||
|
||||
FROM downloads AS timescaledb
|
||||
ARG PG_MAJOR
|
||||
WORKDIR /tmp/timescaledb
|
||||
RUN URL_END=$(case "$PG_MAJOR" in ("12") echo "tag/2.11.2";; ("13") echo "tag/2.15.3";; (*) echo "latest";; esac) && \
|
||||
ASSET_NAME=$(basename $(curl -LIs -o /dev/null -w %{url_effective} https://github.com/timescale/timescaledb/releases/${URL_END})) && \
|
||||
curl --fail -L "https://github.com/timescale/timescaledb/archive/${ASSET_NAME}.tar.gz" | tar -zx --strip-components=1 -C . && ./bootstrap
|
||||
WORKDIR /tmp/timescaledb/build
|
||||
RUN make -j$(nproc) && \
|
||||
make install
|
||||
|
||||
FROM base-image AS final
|
||||
WORKDIR /tmp
|
||||
ARG PG_MAJOR
|
||||
ARG POSTGIS_MAJOR
|
||||
ARG VCHORD_VERSION
|
||||
|
||||
## Install postgis, pg_hint_plan and pgvector
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
libldap-common \
|
||||
postgresql-${PG_MAJOR}-postgis-${POSTGIS_MAJOR} \
|
||||
postgresql-${PG_MAJOR}-postgis-${POSTGIS_MAJOR}-scripts \
|
||||
postgis \
|
||||
postgresql-${PG_MAJOR}-pgrouting \
|
||||
postgresql-${PG_MAJOR}-pg-hint-plan \
|
||||
postgresql-${PG_MAJOR}-pgvector \
|
||||
&& apt-get install --reinstall -y ca-certificates
|
||||
|
||||
## Copy TimescaleDB from build image
|
||||
COPY --from=timescaledb \
|
||||
/usr/share/postgresql/$PG_MAJOR/extension/timescaledb* \
|
||||
/usr/share/postgresql/$PG_MAJOR/extension/
|
||||
COPY --from=timescaledb \
|
||||
/usr/lib/postgresql/$PG_MAJOR/lib/timescaledb* \
|
||||
/usr/lib/postgresql/$PG_MAJOR/lib/
|
||||
|
||||
## Install VectorChord
|
||||
COPY --from=downloads /tmp/postgresql-${PG_MAJOR}-vchord_${VCHORD_VERSION}-1_amd64.deb /tmp/
|
||||
RUN apt-get install -y --no-install-recommends ./postgresql-${PG_MAJOR}-vchord_${VCHORD_VERSION}-1_amd64.deb
|
||||
|
||||
## Mark curated extensions as trusted so non-superusers with CREATE on their
|
||||
## database (e.g. app migration roles) can install them without superuser.
|
||||
## These ship untrusted, otherwise yielding "permission denied to create
|
||||
## extension". Safe because each only adds data types / index access methods /
|
||||
## planner hints -- no filesystem, network, raw-storage, or SECURITY DEFINER
|
||||
## access. Do NOT add postgis*, *_fdw, dblink, or storage-introspection modules.
|
||||
RUN set -eu; \
|
||||
for ext in vector vchord pg_hint_plan; do \
|
||||
ctl="/usr/share/postgresql/${PG_MAJOR}/extension/${ext}.control"; \
|
||||
[ -f "$ctl" ] || { echo "trusted-marking: missing $ctl" >&2; exit 1; }; \
|
||||
grep -qE '^[[:space:]]*trusted[[:space:]]*=' "$ctl" \
|
||||
|| printf 'trusted = true\n' >> "$ctl"; \
|
||||
done
|
||||
|
||||
## Layered configuration (applied to every newly initialized cluster).
|
||||
## initdb seeds postgresql.conf from postgresql.conf.sample, so appending
|
||||
## include_dir there pulls in our config regardless of the container command.
|
||||
##
|
||||
## Image-owned config lives under /usr/local/etc/postgresql (conf.d + pg_hba.conf);
|
||||
## /etc/postgresql holds ONLY the user override dirs (conf.d, hba.d) so that
|
||||
## path is a clean "mount your files here" area.
|
||||
##
|
||||
## postgresql.conf: our /usr/local/etc/postgresql/conf.d loaded first, user
|
||||
## /etc/postgresql/conf.d last (last-wins => users override us, but a stray
|
||||
## mount can't wipe our defaults e.g. shared_preload_libraries).
|
||||
##
|
||||
## pg_hba.conf: conf.d/20-hba.conf points hba_file at our authoritative
|
||||
## /usr/local/etc/postgresql/pg_hba.conf, which fully replaces the initdb-
|
||||
## generated file (and sidesteps the entrypoint's appended "host all all all
|
||||
## <method>" line, written to the now-unused PGDATA copy). Tier order in that
|
||||
## file: local allows -> include_dir /etc/postgresql/hba.d -> default-deny.
|
||||
COPY conf.d/ /usr/local/etc/postgresql/conf.d/
|
||||
COPY pg_hba.conf /usr/local/etc/postgresql/pg_hba.conf
|
||||
RUN mkdir -p /etc/postgresql/conf.d \
|
||||
/etc/postgresql/hba.d \
|
||||
&& { \
|
||||
echo "include_dir = '/usr/local/etc/postgresql/conf.d'"; \
|
||||
echo "include_dir = '/etc/postgresql/conf.d'"; \
|
||||
} >> /usr/share/postgresql/${PG_MAJOR}/postgresql.conf.sample
|
||||
|
||||
## Clean image
|
||||
RUN rm -rf /var/lib/apt/lists/* \
|
||||
&& rm -rf /tmp/*
|
||||
|
||||
## Report health via pg_isready (env vars expand at runtime via /bin/sh -c).
|
||||
HEALTHCHECK --interval=10s --timeout=5s --retries=5 --start-period=2s \
|
||||
CMD pg_isready --dbname="${POSTGRES_DB:-postgres}" --username="${POSTGRES_USER:-postgres}" || exit 1
|
||||
|
||||
WORKDIR /var/lib/postgresql/data
|
||||
@@ -0,0 +1,4 @@
|
||||
# Preloaded extension libraries for this image.
|
||||
# Applied automatically to every newly initialized cluster via
|
||||
# postgresql.conf.sample (include_dir), regardless of container command.
|
||||
shared_preload_libraries = 'timescaledb,vchord,pg_hint_plan,pg_stat_statements'
|
||||
@@ -0,0 +1,3 @@
|
||||
# Use the image's authoritative pg_hba.conf (which includes hba.user.d and a
|
||||
# default-deny) instead of the initdb-generated one in PGDATA.
|
||||
hba_file = '/usr/local/etc/postgresql/pg_hba.conf'
|
||||
@@ -0,0 +1,24 @@
|
||||
# Authoritative pg_hba.conf for this image.
|
||||
#
|
||||
# Activated via the hba_file GUC (see conf.d/20-hba.conf), so it fully replaces
|
||||
# the initdb-generated $PGDATA/pg_hba.conf. This also means the docker-entrypoint's
|
||||
# appended "host all all all ${POSTGRES_HOST_AUTH_METHOD}" line (written to the
|
||||
# PGDATA copy) is never consulted -- ordering here is authoritative.
|
||||
#
|
||||
# pg_hba.conf is FIRST-MATCH-WINS, so the tiers are ordered deliberately:
|
||||
# 1. local/loopback allows (first: nothing can lock out local admin)
|
||||
# 2. site rules (include_dir /etc/postgresql/hba.d: mount files here)
|
||||
# 3. default-deny (last: fail closed)
|
||||
#
|
||||
# TYPE DATABASE USER ADDRESS METHOD
|
||||
|
||||
# 1. Local socket + loopback
|
||||
# local all all trust
|
||||
host all all 127.0.0.1/32 scram-sha-256
|
||||
host all all ::1/128 scram-sha-256
|
||||
|
||||
# 2. Site-specific rules (user-provided; mount *.conf files into this dir)
|
||||
include_dir "/etc/postgresql/hba.d"
|
||||
|
||||
# 3. Default-deny catch-all
|
||||
host all all all reject
|
||||
Reference in new issue
Block a user