Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,36 @@ jobs:
IMAGE: ${{ env.TEST_IMAGE }}
run: bash tests/smoke.sh

- name: Test authentication and configuration security
env:
CONTAINER_RUNTIME: docker
IMAGE: ${{ env.TEST_IMAGE }}
run: bash tests/runtime-security.sh

- name: Test durable storage and lifecycle
env:
CONTAINER_RUNTIME: docker
IMAGE: ${{ env.TEST_IMAGE }}
run: bash tests/storage-lifecycle.sh

- name: Test resource exhaustion and recovery
env:
CONTAINER_RUNTIME: docker
IMAGE: ${{ env.TEST_IMAGE }}
run: bash tests/resource-limits.sh

- name: Test a preserved-data PostgreSQL 18 minor update
env:
CONTAINER_RUNTIME: docker
IMAGE: ${{ env.TEST_IMAGE }}
run: bash tests/minor-update.sh

- name: Test the TLS server profile
env:
CONTAINER_RUNTIME: docker
IMAGE: ${{ env.TEST_IMAGE }}
run: bash tests/tls.sh

- name: Scan image with Trivy
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,9 @@ but container releases use the upstream-derived format documented in
- Added schema-defined AMD64 and ARM64 artifact locks for the complete runtime
RPM and source closure, fail-closed acquisition and publisher verification,
a manual lock-update workflow, and network-disabled offline assembly.
- Added the PostgreSQL authentication, initialization, immutable configuration,
storage, crash-recovery, backup/restore, minor-update, TLS, resource-limit,
logging, and probe runtime contract with native architecture tests.
- Added executable deployment guidance for fixed and arbitrary identities,
Podman, Docker and Compose, named and bind-mounted storage, TLS and isolated
non-TLS networks, controlled transfer, operations, recovery, and teardown.
2 changes: 1 addition & 1 deletion Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ WORKDIR /var/lib/pgsql
EXPOSE 5432

HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=5 \
CMD ["/usr/pgsql-18/bin/psql", "--quiet", "--host=/tmp", "--username=postgres", "--dbname=postgres", "--command=SELECT 1"]
CMD ["/usr/pgsql-18/bin/pg_isready", "--quiet", "--host=/tmp", "--timeout=3"]

STOPSIGNAL SIGINT

Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,22 @@ The current development image provides:
authentication;
- a persistent data volume with read-only-root compatibility;
- dropped-capability and `no-new-privileges` operation; and
- stateful smoke tests for initialization, authentication, persistence,
- native stateful tests for secure initialization and configuration, TLS,
resource exhaustion/recovery, persistence, backup/restore, minor updates,
shutdown, arbitrary UIDs, and incompatible data directories.

Architecture-specific locks now cover the complete binary dependency closure,
publisher keys, source RPMs, and digest-pinned UBI inputs. Artifact acquisition
is separate from a network-disabled, pull-disabled container build; see the
[artifact acquisition contract](docs/ARTIFACT-ACQUISITION.md).

Detailed procedures for choosing, deploying, verifying, operating, updating,
recovering, and safely removing each intended profile are in the
[deployment and operations guide](docs/DEPLOYMENT.md). The
[runtime security contract](docs/RUNTIME-SECURITY.md) and
[storage, backup, and upgrade guide](docs/STORAGE.md) define the associated
security and data-lifecycle boundaries.

## Approved first-release boundary

The first release is scoped to native AMD64 and ARM64, an exact rootless
Expand Down
4 changes: 3 additions & 1 deletion compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ services:
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/postgres-password
secrets:
- postgres-password
- source: postgres-password
target: postgres-password
mode: 0440
ports:
- "127.0.0.1:5432:5432"
read_only: true
Expand Down
172 changes: 160 additions & 12 deletions container/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,74 @@
set -eu

postgres_major=18
initialization_lock=
password_file=

fatal() {
printf 'postgresql-entrypoint: %s\n' "$*" >&2
exit 1
}

cleanup_initialization() {
test -z "${password_file}" || rm -f -- "${password_file}"
test -z "${initialization_lock}" || rmdir -- "${initialization_lock}" 2>/dev/null || true
}

on_initialization_signal() {
cleanup_initialization
trap - EXIT HUP INT TERM
exit 1
}

reject_line_breaks() {
value=$1
label=$2
newline='
'
carriage_return=$(printf '\r')
case "${value}" in
*"${newline}"*|*"${carriage_return}"*) fatal "${label} must not contain line breaks" ;;
esac
}

require_regular_file() {
path=$1
label=$2
test ! -L "${path}" || fatal "${label} must not be a symbolic link"
test -f "${path}" || fatal "${label} must be a regular file"
test -r "${path}" || fatal "${label} is not readable"
}

require_secret_permissions() {
path=$1
label=$2
permissions=$(stat -c '%a' "${path}")
owner=$(stat -c '%u' "${path}")
group=$(stat -c '%g' "${path}")

case "${permissions}" in
400|440|600|640) ;;
*) fatal "${label} permissions must be 0400, 0440, 0600, or 0640" ;;
esac
current_uid=$(id -u)
test "${owner}" = 0 || test "${owner}" = "${current_uid}" || \
fatal "${label} must be owned by UID 0 or the runtime UID"
case "${permissions}" in
440|640)
test "${group}" = 0 || \
fatal "${label} group-readable files must be owned by GID 0"
;;
esac
}

require_public_file_permissions() {
path=$1
label=$2
permissions=$(stat -c '%a' "${path}")
test $((0${permissions} & 07022)) -eq 0 || \
fatal "${label} must not be group/other writable or have special permission bits"
}

configure_arbitrary_uid() {
if id -un >/dev/null 2>&1; then
return
Expand All @@ -31,64 +93,150 @@ read_initial_password() {
fi

if test -n "${POSTGRES_PASSWORD_FILE:-}"; then
test -r "${POSTGRES_PASSWORD_FILE}" || \
fatal "POSTGRES_PASSWORD_FILE is not readable"
POSTGRES_PASSWORD=$(cat "${POSTGRES_PASSWORD_FILE}")
require_regular_file "${POSTGRES_PASSWORD_FILE}" POSTGRES_PASSWORD_FILE
require_secret_permissions "${POSTGRES_PASSWORD_FILE}" POSTGRES_PASSWORD_FILE
password_lines=$(wc -l <"${POSTGRES_PASSWORD_FILE}")
test "${password_lines}" -eq 0 || \
fatal "POSTGRES_PASSWORD_FILE must contain exactly one value without a line break"
POSTGRES_PASSWORD=$(cat -- "${POSTGRES_PASSWORD_FILE}")
fi

test -n "${POSTGRES_PASSWORD:-}" || \
fatal "initialization requires POSTGRES_PASSWORD or POSTGRES_PASSWORD_FILE"
reject_line_breaks "${POSTGRES_PASSWORD}" POSTGRES_PASSWORD
}

write_host_authentication() {
umask 077
{
printf '# Managed by postgresql-ubi; replaced on every start.\n'
printf 'local all all trust\n'
if test -n "${POSTGRESQL_TLS_CERT_FILE:-}"; then
printf 'hostssl all all 0.0.0.0/0 scram-sha-256\n'
printf 'hostssl all all ::/0 scram-sha-256\n'
else
printf 'host all all 0.0.0.0/0 scram-sha-256\n'
printf 'host all all ::/0 scram-sha-256\n'
fi
} >"${PGDATA}/pg_hba.conf"
}

initialize_database() {
read_initial_password
initialization_lock="${PGDATA}.postgresql-ubi.init.lock"
mkdir -- "${initialization_lock}" 2>/dev/null || \
fatal "initialization is already running or a stale initialization lock exists: ${initialization_lock}"
umask 077
password_file="/tmp/postgresql-password.$$"
printf '%s' "${POSTGRES_PASSWORD}" >"${password_file}"
trap 'rm -f "${password_file}"' EXIT HUP INT TERM
trap cleanup_initialization EXIT
trap on_initialization_signal HUP INT TERM

initdb \
--pgdata="${PGDATA}" \
--username="${POSTGRES_USER}" \
--pwfile="${password_file}" \
--auth-host=scram-sha-256 \
--auth-local=trust \
--data-checksums \
--encoding=UTF8

printf "\nlisten_addresses = '*'\nunix_socket_directories = '/tmp'\nlogging_collector = off\npassword_encryption = 'scram-sha-256'\n" \
>>"${PGDATA}/postgresql.conf"
printf '\nhost all all all scram-sha-256\n' >>"${PGDATA}/pg_hba.conf"
write_host_authentication

pg_ctl --pgdata="${PGDATA}" \
--options="-c listen_addresses='' -c unix_socket_directories=/tmp" \
--options="-c listen_addresses='' -c unix_socket_directories=/tmp -c password_encryption=scram-sha-256 -c hba_file=${PGDATA}/pg_hba.conf -c logging_collector=off" \
--wait start

database=${POSTGRES_DB:-${POSTGRES_USER}}
reject_line_breaks "${database}" POSTGRES_DB
if test "${database}" != "${POSTGRES_USER}"; then
createdb --host=/tmp --username="${POSTGRES_USER}" -- "${database}"
fi

pg_ctl --pgdata="${PGDATA}" --mode=fast --wait stop
rm -f "${password_file}"
cleanup_initialization
initialization_lock=
password_file=
trap - EXIT HUP INT TERM
unset POSTGRES_PASSWORD
}

if test "${1:-}" = "postgres"; then
validate_runtime_files() {
if test -n "${POSTGRESQL_CONFIG_FILE:-}"; then
require_regular_file "${POSTGRESQL_CONFIG_FILE}" POSTGRESQL_CONFIG_FILE
require_public_file_permissions "${POSTGRESQL_CONFIG_FILE}" POSTGRESQL_CONFIG_FILE
fi

if test -n "${POSTGRESQL_TLS_CERT_FILE:-}" || test -n "${POSTGRESQL_TLS_KEY_FILE:-}"; then
test -n "${POSTGRESQL_TLS_CERT_FILE:-}" && test -n "${POSTGRESQL_TLS_KEY_FILE:-}" || \
fatal "set both POSTGRESQL_TLS_CERT_FILE and POSTGRESQL_TLS_KEY_FILE"
require_regular_file "${POSTGRESQL_TLS_CERT_FILE}" POSTGRESQL_TLS_CERT_FILE
require_public_file_permissions "${POSTGRESQL_TLS_CERT_FILE}" POSTGRESQL_TLS_CERT_FILE
require_regular_file "${POSTGRESQL_TLS_KEY_FILE}" POSTGRESQL_TLS_KEY_FILE
require_secret_permissions "${POSTGRESQL_TLS_KEY_FILE}" POSTGRESQL_TLS_KEY_FILE
fi
}

if test "${1:-}" = postgres; then
# Supported by the UBI /bin/sh; POSIX leaves ulimit options unspecified.
# shellcheck disable=SC3045
ulimit -c 0 || fatal "cannot disable core dumps"
reject_line_breaks "${POSTGRES_USER}" POSTGRES_USER
configure_arbitrary_uid
mkdir -p "${PGDATA}"
test ! -L "${PGDATA}" || fatal "PGDATA must not be a symbolic link"
mkdir -p -- "${PGDATA}" 2>/dev/null || fatal "cannot create PGDATA: ${PGDATA}"
test -d "${PGDATA}" || fatal "PGDATA is not a directory: ${PGDATA}"
test -w "${PGDATA}" || fatal "PGDATA is not writable: ${PGDATA}"
writability_probe="${PGDATA}/.postgresql-ubi-write-test.$$"
(umask 077 && : >"${writability_probe}") 2>/dev/null || \
fatal "PGDATA cannot accept a validation write: ${PGDATA}"
rm -f -- "${writability_probe}" || \
fatal "PGDATA validation file cannot be removed: ${PGDATA}"

if test ! -s "${PGDATA}/PG_VERSION"; then
if test -n "$(find "${PGDATA}" -mindepth 1 -maxdepth 1 -print -quit)"; then
fatal "PGDATA is non-empty but has no PG_VERSION file"
fatal "PGDATA is non-empty but has no PG_VERSION file; refusing automatic recovery or reinitialization"
fi
initialize_database
else
installed_major=$(cat "${PGDATA}/PG_VERSION")
test "${installed_major}" = "${postgres_major}" || \
fatal "PGDATA major ${installed_major} is incompatible with PostgreSQL ${postgres_major}"
write_host_authentication
fi

unset POSTGRES_PASSWORD POSTGRES_PASSWORD_FILE
validate_runtime_files

if test -n "${POSTGRESQL_CONFIG_FILE:-}"; then
set -- "$@" -c "config_file=${POSTGRESQL_CONFIG_FILE}"
fi
if test -n "${POSTGRESQL_TLS_CERT_FILE:-}"; then
set -- "$@" \
-c ssl=on \
-c "ssl_cert_file=${POSTGRESQL_TLS_CERT_FILE}" \
-c "ssl_key_file=${POSTGRESQL_TLS_KEY_FILE}" \
-c ssl_min_protocol_version=TLSv1.2 \
-c ssl_max_protocol_version=TLSv1.3
else
set -- "$@" -c ssl=off
fi
set -- "$@" \
-c "data_directory=${PGDATA}" \
-c listen_addresses='*' \
-c port=5432 \
-c password_encryption=scram-sha-256 \
-c "hba_file=${PGDATA}/pg_hba.conf" \
-c unix_socket_directories=/tmp \
-c logging_collector=off \
-c log_destination=stderr \
-c log_statement=none \
-c log_min_duration_statement=-1 \
-c log_parameter_max_length=0 \
-c log_parameter_max_length_on_error=0 \
-c log_connections=on \
-c log_disconnections=on \
-c log_checkpoints=on
fi

exec "$@"
8 changes: 7 additions & 1 deletion docs/CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

CI runs repository validation, configuration scanning, native AMD64 and ARM64
image builds, restricted-runtime smoke tests, Trivy and Grype vulnerability
gates, and Syft SPDX SBOM generation. The aggregate `image` job fails unless
gates, and Syft SPDX SBOM generation. Runtime tests cover authentication and
configuration precedence, initialization failure modes, durable-state
lifecycle and crash recovery, logical backup/restoration, and the TLS profile.
Resource tests exercise inode, shared-memory, connection, and cgroup-memory
exhaustion under explicit file-descriptor and PID ceilings, then validate
durable-data recovery.
The aggregate `image` job fails unless
every native image job succeeds. Each image job acquires the architecture's
committed lock, verifies the bundle and full key fingerprints, pre-pulls only
the digest-pinned bases, and performs a clean build with network access and
Expand Down
Loading
Loading