Skip to content
Open
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
79 changes: 0 additions & 79 deletions .github/workflows/docker.yaml

This file was deleted.

6 changes: 0 additions & 6 deletions charts/postgrest/Chart.lock

This file was deleted.

7 changes: 1 addition & 6 deletions charts/postgrest/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,4 @@ maintainers:
- name: apkatsikas
email: andrew.katsikas@pelo.tech
description: Helm chart for a PostgREST data api.

dependencies:
- name: cluster
version: 0.7.0
repository: https://cloudnative-pg.io/charts
condition: cluster.enabled
appVersion: "v14.16"
123 changes: 49 additions & 74 deletions charts/postgrest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@

### version: 0.7.0<!-- x-release-please-version -->

Helm chart for a PostgREST data api.

## Requirements
![AppVersion: v14.16](https://img.shields.io/badge/AppVersion-v14.16-informational?style=flat-square)

| Repository | Name | Version |
|------------|------|---------|
| https://cloudnative-pg.io/charts | cluster | 0.7.0 |
Helm chart for a PostgREST data api.

## Values

Expand All @@ -18,81 +14,60 @@ Helm chart for a PostgREST data api.
| application.jwt.claim.selector | string | `".postgrest"` | |
| application.preRequest | string | `""` | |
| application.schemas | string | `"public"` | |
| cluster.cluster.instances | int | `3` | |
| cluster.cluster.roles[0].ensure | string | `"present"` | |
| cluster.cluster.roles[0].login | bool | `true` | |
| cluster.cluster.roles[0].name | string | `"goose"` | |
| cluster.cluster.roles[0].passwordSecret.name | string | `"migrations"` | |
| cluster.cluster.roles[0].superuser | bool | `true` | |
| cluster.cluster.roles[1].ensure | string | `"present"` | |
| cluster.cluster.roles[1].login | bool | `false` | |
| cluster.cluster.roles[1].name | string | `"anon"` | |
| cluster.cluster.roles[2].ensure | string | `"present"` | |
| cluster.cluster.roles[2].login | bool | `false` | |
| cluster.cluster.roles[2].name | string | `"peek"` | |
| cluster.cluster.roles[3].ensure | string | `"present"` | |
| cluster.cluster.roles[3].login | bool | `false` | |
| cluster.cluster.roles[3].name | string | `"view"` | |
| cluster.cluster.roles[4].ensure | string | `"present"` | |
| cluster.cluster.roles[4].login | bool | `false` | |
| cluster.cluster.roles[4].name | string | `"edit"` | |
| cluster.cluster.roles[5].ensure | string | `"present"` | |
| cluster.cluster.roles[5].inRoles[0] | string | `"anon"` | |
| cluster.cluster.roles[5].inRoles[1] | string | `"peek"` | |
| cluster.cluster.roles[5].inRoles[2] | string | `"view"` | |
| cluster.cluster.roles[5].inRoles[3] | string | `"edit"` | |
| cluster.cluster.roles[5].login | bool | `true` | |
| cluster.cluster.roles[5].name | string | `"postgrest"` | |
| cluster.cluster.roles[5].passwordSecret.name | string | `"connection"` | |
| cluster.cluster.roles[5].superuser | bool | `false` | |
| cluster.cluster.storage.size | string | `"1Gi"` | |
| cluster.databases[0].name | string | `"postgrest"` | |
| cluster.databases[0].owner | string | `"goose"` | |
| cluster.databases[0].schemas[0].ensure | string | `"present"` | |
| cluster.databases[0].schemas[0].name | string | `"public"` | |
| cluster.databases[0].schemas[0].owner | string | `"goose"` | |
| cluster.enabled | bool | `true` | |
| cluster.mode | string | `"standalone"` | |
| cluster.type | string | `"postgresql"` | |
| containerPorts.admin | int | `3001` | |
| containerPorts.http | int | `3000` | |
| database.connection.database | string | `"postgrest"` | |
| database.connection.hostname | string | `"postgrest-cluster-rw"` | |
| database.connection.password | string | `"postgrest"` | |
| database.connection.username | string | `"postgrest"` | |
| database.migrations.command[0] | string | `"/bin/goose"` | |
| database.migrations.command[1] | string | `"up"` | |
| database.migrations.database | string | `"postgrest"` | |
| database.migrations.enabled | bool | `false` | |
| database.migrations.hostname | string | `"postgrest-cluster-rw"` | |
| database.migrations.image | string | `"ghcr.io/pelotech/images/goose@sha256:afee303f0172bf7ed4db97180ce1561d0f1db09222f8e87d3313b2cd1fb184c9"` | |
| database.migrations.loadFromConfigMap | bool | `false` | |
| database.migrations.password | string | `"goose"` | |
| database.migrations.tag | string | `"latest"` | |
| database.migrations.username | string | `"goose"` | |
| imagePullSecrets | string | `nil` | |
| image.digest | string | `"sha256:bea1c76a856fa39d1e542d25911cf95d02fe2bf971992d033044ff209f1504b8"` | |
| image.pullPolicy | string | `"IfNotPresent"` | |
| image.registry | string | `"docker.io"` | |
| image.repository | string | `"postgrest/postgrest"` | |
| image.tag | string | `""` | |
| imagePullSecrets | list | `[]` | |
| ingress.enabled | bool | `false` | |
| ingress.rules[0].host | string | `"auth.app.localhost"` | |
| ingress.rules[0].host | string | `"data.app.localhost"` | |
| ingress.rules[0].http.paths[0].backend.service.name | string | `"postgrest"` | |
| ingress.rules[0].http.paths[0].backend.service.port.name | string | `"keyserver"` | |
| ingress.rules[0].http.paths[0].backend.service.port.name | string | `"postgrest"` | |
| ingress.rules[0].http.paths[0].path | string | `"/"` | |
| ingress.rules[0].http.paths[0].pathType | string | `"Prefix"` | |
| ingress.rules[1].host | string | `"data.app.localhost"` | |
| ingress.rules[1].http.paths[0].backend.service.name | string | `"postgrest"` | |
| ingress.rules[1].http.paths[0].backend.service.port.name | string | `"postgrest"` | |
| ingress.rules[1].http.paths[0].path | string | `"/"` | |
| ingress.rules[1].http.paths[0].pathType | string | `"Prefix"` | |
| ingress.tls[0].hosts[0] | string | `"auth.app.localhost"` | |
| ingress.tls[0].hosts[1] | string | `"data.app.localhost"` | |
| ingress.tls[0].hosts[0] | string | `"data.app.localhost"` | |
| ingress.tls[0].secretName | string | `"app.localhost-tls"` | |
| keyserver.api.key | string | `"a-string-secret-at-least-256-bits-long"` | |
| keyserver.image | string | `"ghcr.io/pelotech/images/keyserver@sha256:a128a2b37ccf00ab6bbbcd9ed0321bc81b1875aff51d0eb3aa412f07e50804d8"` | |
| keyserver.jwt.alg | string | `"RS256"` | |
| keyserver.jwt.aud | string | `"postgrest"` | |
| keyserver.jwt.claims.postgrest | string | `"peek"` | |
| keyserver.jwt.exp | string | `"5 minutes"` | |
| keyserver.jwt.iss | string | `"https://auth.app.localhost"` | |
| keyserver.jwt.jwks_uri | string | `"https://auth.app.localhost/jwks"` | |
| keyserver.jwt.origin | string | `"https://jwt.io,https://app.localhost"` | |
| keyserver.jwt.sub | string | `""` | |
| keyserver.jwt.trust | string | `"https://sso.localhost/auth/realms/example/protocol/openid-connect/certs"` | |
| keyserver.tag | string | `"latest"` | |
| jwks.enabled | bool | `true` | |
| jwks.filename | string | `"jwks.json"` | |
| jwks.image.digest | string | `"sha256:e318141552c5e6923abff064bd2b8d338c2722bda46cf48d84404b9957bcd814"` | |
| jwks.image.pullPolicy | string | `"IfNotPresent"` | |
| jwks.image.registry | string | `"reg.mini.dev"` | |
| jwks.image.repository | string | `"curl-fips"` | |
| jwks.image.tag | string | `"8.21.0"` | |
| jwks.maxAttempts | int | `40` | |
| jwks.mountPath | string | `"/etc/opt/postgrest/certificates"` | |
| jwks.refresh.enabled | bool | `false` | |
| jwks.refresh.interval | int | `300` | |
| jwks.refresh.reload.enabled | bool | `true` | |
| jwks.refresh.reload.processName | string | `"postgrest"` | |
| jwks.refresh.reload.signal | string | `"USR2"` | |
| jwks.require.kid | string | `""` | |
| jwks.require.kty | string | `""` | |
| jwks.require.use | string | `""` | |
| jwks.resources | object | `{}` | |
| jwks.retryInterval | int | `3` | |
| jwks.securityContext.runAsUser | int | `1000` | |
| jwks.sizeLimit | string | `"1Mi"` | |
| jwks.timeout | int | `10` | |
| jwks.url | string | `"https://sso.localhost/auth/realms/example/protocol/openid-connect/certs"` | |
| podAnnotations | object | `{}` | |
| podSecurityContext.fsGroup | int | `1000` | |
| probes.liveness.enabled | bool | `true` | |
| probes.liveness.failureThreshold | int | `3` | |
| probes.liveness.periodSeconds | int | `10` | |
| probes.startup.enabled | bool | `true` | |
| probes.startup.failureThreshold | int | `60` | |
| probes.startup.periodSeconds | int | `2` | |
| replicaCount | int | `1` | |
| resources | object | `{}` | |
| securityContext | object | `{}` | |
| service.port | int | `3000` | |
| service.type | string | `"ClusterIP"` | |

139 changes: 139 additions & 0 deletions charts/postgrest/files/fetch-jwks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/bin/sh
# Fetch a JWKS to disk for PostgREST, which takes jwt-secret as a literal or an @file only.
#
# once (default) fetch and exit. 0 if a usable key set ends up on disk, 1 otherwise.
# refresh then re-check every JWKS_REFRESH_INTERVAL seconds. Failures are not fatal.
#
# Responses are validated in a temp file and renamed into place, so a bad fetch never
# replaces working keys.
set -eu

MODE="${1:-once}"
case "${MODE}" in
once | refresh) ;;
*) echo "jwks :: unknown role '${MODE}', expected 'once' or 'refresh'" >&2; exit 2 ;;
esac

URL="${JWKS_URL:?JWKS_URL is required}"
TARGET="${JWKS_TARGET:-/etc/opt/postgrest/certificates/jwks.json}"
TIMEOUT="${JWKS_TIMEOUT:-10}"
RETRY_INTERVAL="${JWKS_RETRY_INTERVAL:-3}"
MAX_ATTEMPTS="${JWKS_MAX_ATTEMPTS:-40}"
REFRESH_INTERVAL="${JWKS_REFRESH_INTERVAL:-0}"
RELOAD_SIGNAL="${JWKS_RELOAD_SIGNAL:-}"
RELOAD_PROCESS="${JWKS_RELOAD_PROCESS:-postgrest}"
REQUIRE_KTY="${JWKS_REQUIRE_KTY:-}"
REQUIRE_USE="${JWKS_REQUIRE_USE:-}"
REQUIRE_KID="${JWKS_REQUIRE_KID:-}"

TMP="${TARGET}.tmp"

log() { echo "jwks :: $*"; }
err() { echo "jwks :: $*" >&2; }

# Tests each key on its own. A document can carry the required kty, use and kid across
# different keys and satisfy a naive substring check without holding a usable key.
valid_jwks() {
[ -s "$1" ] || return 1
JWKS_REQUIRE_KTY="${REQUIRE_KTY}" JWKS_REQUIRE_USE="${REQUIRE_USE}" \
JWKS_REQUIRE_KID="${REQUIRE_KID}" awk '
BEGIN {
want_kty = ENVIRON["JWKS_REQUIRE_KTY"]
want_use = ENVIRON["JWKS_REQUIRE_USE"]
want_kid = ENVIRON["JWKS_REQUIRE_KID"]
}
{ doc = doc $0 }
END {
gsub(/[ \t\r\n]/, "", doc)
if (doc !~ /^\{/ || doc !~ /\}$/) exit 1
if (doc !~ /"keys":\[/) exit 1

n = split(doc, key, /\},?\{/)
for (i = 1; i <= n; i++) {
if (key[i] !~ /"kty":"[^"]+"/) continue
if (want_kty != "" && key[i] !~ "\"kty\":\"" want_kty "\"") continue
if (want_use != "" && key[i] !~ "\"use\":\"" want_use "\"") continue
if (want_kid != "" && key[i] !~ /"kid":"[^"]+"/) continue
exit 0
}
exit 1
}
' "$1"
}

# Whitespace goes first: a pretty-printed document writes "kid": "x", not "kid":"x".
key_ids() {
tr -d '[:space:]' < "$1" \
| grep -o '"kid":"[^"]*"' \
| cut -d'"' -f4 \
| sort \
| paste -sd, - \
| sed 's/,/, /g'
}

# Key ids rather than bytes. Some providers reorder the keys per request, which would look
# like a rotation every refresh and flush PostgREST's JWT cache for nothing.
fingerprint() {
ids=$(key_ids "$1")
if [ -n "${ids}" ]; then
echo "${ids}"
else
cat "$1"
fi
}

# SIGUSR2 makes PostgREST re-read the @file jwt-secret. Needs a matching uid: a non-root
# sender has no effective CAP_KILL.
reload() {
[ "${MODE}" = refresh ] || return 0
[ -n "${RELOAD_SIGNAL}" ] || return 0

pid=$(pgrep -x "${RELOAD_PROCESS}" | head -1)
if [ -n "${pid}" ] && kill -"${RELOAD_SIGNAL}" "${pid}" 2>/dev/null; then
log "signalled ${RELOAD_PROCESS} (pid ${pid}) with SIG${RELOAD_SIGNAL}"
else
err "could not signal ${RELOAD_PROCESS}: not running, or not permitted"
fi
}

fetch_once() {
attempt=1
while [ "${attempt}" -le "${MAX_ATTEMPTS}" ]; do
if curl --fail --silent --show-error --location --max-time "${TIMEOUT}" \
--output "${TMP}" "${URL}" && valid_jwks "${TMP}"; then
if [ -s "${TARGET}" ] && [ "$(fingerprint "${TMP}")" = "$(fingerprint "${TARGET}")" ]; then
rm -f "${TMP}"
log "unchanged"
else
mv -f "${TMP}" "${TARGET}"
ids=$(key_ids "${TARGET}")
log "wrote ${TARGET} from ${URL}${ids:+ (key ids: ${ids})}"
reload
fi
return 0
fi

rm -f "${TMP}"
err "no usable jwks from ${URL} (attempt ${attempt}/${MAX_ATTEMPTS})"
attempt=$((attempt + 1))
[ "${attempt}" -le "${MAX_ATTEMPTS}" ] && sleep "${RETRY_INTERVAL}"
done

if valid_jwks "${TARGET}"; then
err "giving up on the fetch, keeping the jwks already at ${TARGET}"
return 0
fi

err "no usable jwks fetched and none on disk at ${TARGET}"
return 1
}

fetch_once || exit 1

[ "${MODE}" = refresh ] || exit 0

log "refreshing every ${REFRESH_INTERVAL}s"
while true; do
sleep "${REFRESH_INTERVAL}"
fetch_once || true
done
Loading
Loading