Skip to content

Commit cd446ec

Browse files
y-tabatagbartolini
authored andcommitted
feat: add PostgreSQL OAuth Validator for Keycloak (kc_validator)
Initial implementation of the `kc_validator` extension, providing OAuth-based authentication integration between PostgreSQL and Keycloak. Includes: - Core C extension - Dockerfile for building and testing - Example manifests for CloudNativePG and Keycloak - Keycloak demo realm export - Updated `.gitignore` - Refreshed `README.md` with usage, configuration, and quick start instructions Signed-off-by: Yoshiyuki Tabata <[email protected]>
1 parent bb54dd6 commit cd446ec

File tree

9 files changed

+3433
-25
lines changed

9 files changed

+3433
-25
lines changed

.gitignore

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,24 @@
1-
# If you prefer the allow list template instead of the deny list, see community template:
2-
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3-
#
4-
# Binaries for programs and plugins
5-
*.exe
6-
*.exe~
7-
*.dll
8-
*.so
9-
*.dylib
1+
# PostgreSQL extension build artifacts
2+
src/*.o
3+
src/*.so
4+
src/*.bc
5+
src/.deps/
6+
src/*.log
107

11-
# Mac
12-
.DS_Store
13-
14-
# Test binary, built with `go test -c`
15-
*.test
16-
17-
# Output of the go coverage tool, specifically when used with LiteIDE
18-
*.out
8+
# Docker build cache / certs
9+
docker/certs/*.key
10+
docker/certs/*.csr
11+
docker/certs/*.crt
1912

20-
# editor and IDE paraphernalia
21-
.idea
13+
# Editor/IDE files
14+
.DS_Store
15+
.idea/
2216
*.swp
2317
*.swo
2418
*~
19+
.vscode/
20+
*.bak
2521

26-
# Dependency directories (remove the comment below to include it)
27-
# vendor/
28-
29-
# Go workspace file
30-
go.work
22+
# Misc
23+
*.test
24+
*.out

README.md

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,138 @@
22

33
# PostgreSQL OAuth Validator for Keycloak
44

5-
TODO
5+
**Requires**: PostgreSQL 18+
66

7+
This module enables PostgreSQL 18 to delegate authorization decisions to Keycloak using OAuth tokens, leveraging Keycloak Authorization Services for fine-grained, token-based access control.
8+
It sends a permission request to Keycloak's token endpoint using `grant_type=urn:ietf:params:oauth:grant-type:uma-ticket` and expects a decision response (`response_mode=decision`), which is a Keycloak-specific extension.
9+
It is designed for use with CloudNativePG, allowing database role elevation to be controlled by Keycloak policies.
10+
11+
---
12+
13+
## Features
14+
15+
- **Keycloak-based authorization for PostgreSQL roles**
16+
- Delegates database role elevation decisions to Keycloak Authorization Services using OAuth tokens.
17+
- **Permission string construction**
18+
- Builds permission strings as `<resource_name>#<scope>` and sends permission requests to Keycloak's token endpoint (`grant_type=urn:ietf:params:oauth:grant-type:uma-ticket`, `response_mode=decision`).
19+
- **Configurable via PostgreSQL GUC parameters**
20+
- All integration settings (endpoints, resource names, timeouts, debug, etc.) are controlled via GUCs.
21+
- **Secure HTTP communication**
22+
- Uses libcurl for HTTP requests with configurable timeouts and safe logging.
23+
- **Optional JWT issuer verification**
24+
- Can verify the `iss` claim in JWT tokens for additional security.
25+
26+
---
27+
28+
## Example: CloudNativePG Configuration
29+
30+
```yaml
31+
apiVersion: postgresql.cnpg.io/v1
32+
kind: Cluster
33+
metadata:
34+
name: pg-oauth
35+
spec:
36+
imageName: pg18-kc-validator:18.0 # Image containing kc_validator.so
37+
instances: 1
38+
39+
postgresql:
40+
parameters:
41+
oauth_validator_libraries: "kc_validator"
42+
kc.token_endpoint: "https://<keycloak>/realms/<realm>/protocol/openid-connect/token"
43+
kc.audience: "postgres-resource"
44+
kc.resource_name: "appdb" # Resource name in Keycloak
45+
kc.client_id: "postgres-resource"
46+
kc.http_timeout_ms: "2000"
47+
kc.expected_issuer: "https://<keycloak>/realms/<realm>"
48+
kc.debug: "on"
49+
kc.log_body: "on"
50+
log_min_messages: "debug1"
51+
pg_hba:
52+
- host all all 0.0.0.0/0 oauth issuer="https://<keycloak>/realms/<realm>" scope=db_access validator="kc_validator" delegate_ident_mapping=1
53+
```
54+
55+
For a full example, see `examples/cnpg/cluster.yaml`.
56+
57+
---
58+
59+
## Keycloak Configuration Steps
60+
61+
1. **Realm**
62+
Create or use an existing realm (e.g., `demo`).
63+
64+
2. **Resource Server Client** (`kc.audience`)
65+
Create a client for Authorization Services (e.g., `postgres-resource`).
66+
Enable Authorization Services and add scopes as needed (e.g., `app_readonly`, `app_readwrite`).
67+
68+
3. **Validator Client** (`kc.client_id`)
69+
A client allowed to call the token endpoint for permission decisions.
70+
71+
4. **Resource & Permission**
72+
Resource name: `<kc.resource_name>` (e.g., `appdb`).
73+
Scope name: `<scope>` (e.g., `app_readonly`, `app_readwrite`).
74+
Permission name: `<resource_name>#<scope>` (e.g., `appdb#app_readonly`).
75+
Create a permission for each database role you want to allow (e.g., DB role `app_readonly` maps to Keycloak scope `app_readonly`, permission name `appdb#app_readonly`).
76+
77+
5. **Policies**
78+
Attach policies to permissions so that only intended users can access specific scopes.
79+
80+
6. **Issuer Verification (optional)**
81+
Set `kc.expected_issuer` to your realm's issuer URL (e.g., `https://<keycloak>/realms/<realm>`).
82+
83+
---
84+
85+
## Quick Start with psql and Device Flow
86+
87+
You can quickly test the validator using Keycloak's Device Flow and psql:
88+
89+
1. **Connect to PostgreSQL using psql with OAuth parameters:**
90+
91+
```bash
92+
psql "host=<keycloak> \
93+
user=app_readonly \
94+
dbname=appdb \
95+
oauth_issuer=https://<keycloak>/realms/demo \
96+
oauth_client_id=appA \
97+
oauth_client_secret=<client secret> \
98+
oauth_scope='db_access'"
99+
```
100+
101+
When you run this command, psql will display a Device Authorization URL and a device code.
102+
103+
2. **Authenticate via browser:**
104+
105+
- Open the displayed URL in your browser.
106+
- Enter the device code shown by psql.
107+
- Log in with your Keycloak username and password.
108+
109+
Once authentication is complete, psql will automatically obtain an access token and connect to the database.
110+
111+
> Note:
112+
The DB role (`app_readonly`) should match the Keycloak scope name.
113+
The validator will request permission `<resource_name>#<scope>` (e.g., `appdb#app_readonly`) from Keycloak Authorization Services.
114+
115+
---
116+
117+
## Build Instructions
118+
119+
### Docker
120+
121+
```bash
122+
docker build -t pg-kc-validator -f docker/Dockerfile .
123+
```
124+
125+
---
126+
127+
## Security Notes
128+
129+
- Do not use self-signed certificates (server.crt) in production; always use a trusted CA.
130+
- Enable `kc.log_body` only for debugging; keep it `off` in production.
131+
- Place CA certificates in `/usr/local/share/ca-certificates/` and run `update-ca-certificates` in your Docker image.
132+
133+
---
134+
135+
## License
136+
137+
Apache-2.0. See `LICENSE`.
138+
139+
---

docker/Dockerfile

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# syntax=docker/dockerfile:1.7
2+
FROM postgres:18 AS builder
3+
ARG DEBIAN_FRONTEND=noninteractive
4+
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
5+
set -eux; \
6+
apt-get update; \
7+
apt-get install -y --no-install-recommends \
8+
build-essential libcurl4-openssl-dev postgresql-server-dev-18; \
9+
apt-get clean; \
10+
rm -rf /var/lib/apt/lists/*
11+
WORKDIR /work
12+
COPY src/ ./src/
13+
RUN make -C src
14+
15+
FROM ghcr.io/cloudnative-pg/postgresql:18-standard-trixie
16+
ARG DEBIAN_FRONTEND=noninteractive
17+
USER root
18+
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
19+
set -eux; \
20+
apt-get update; \
21+
apt-get install -y --no-install-recommends libcurl4 ca-certificates; \
22+
apt-get clean; \
23+
rm -rf /var/lib/apt/lists/*
24+
COPY --chmod=0644 docker/certs/server.crt /usr/local/share/ca-certificates/kc-root.crt
25+
RUN update-ca-certificates
26+
ENV CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
27+
USER postgres
28+
COPY --from=builder /work/src/kc_validator.so /usr/lib/postgresql/18/lib/

examples/cnpg/cluster.yaml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
apiVersion: postgresql.cnpg.io/v1
2+
kind: Cluster
3+
metadata:
4+
name: pg-oauth
5+
spec:
6+
imageName: pg18-kc-validator:18.0
7+
instances: 1
8+
9+
# Bootstrap from scratch and run our init SQL from a ConfigMap
10+
bootstrap:
11+
initdb:
12+
database: appdb
13+
owner: app
14+
postInitApplicationSQLRefs:
15+
configMapRefs:
16+
- name: pg-init-sql
17+
key: init.sql
18+
19+
storage:
20+
size: 1Gi
21+
postgresql:
22+
parameters:
23+
oauth_validator_libraries: "kc_validator"
24+
25+
kc.token_endpoint: "https://<keycloak>/realms/<realm>/protocol/openid-connect/token"
26+
kc.audience: "postgres-resource"
27+
kc.resource_name: "appdb"
28+
kc.client_id: "postgres-resource"
29+
kc.http_timeout_ms: "2000"
30+
kc.expected_issuer: "https://<keycloak>/realms/<realm>"
31+
kc.debug: "on"
32+
kc.log_body: "on"
33+
log_min_messages: "debug1"
34+
35+
pg_hba:
36+
- host all all 0.0.0.0/0 oauth issuer="https://<keycloak>/realms/<realm>" scope=db_access validator="kc_validator" delegate_ident_mapping=1

examples/cnpg/pg-init-sql.yaml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
apiVersion: v1
2+
kind: ConfigMap
3+
metadata:
4+
name: pg-init-sql
5+
data:
6+
init.sql: |
7+
-- Create the 'users' table
8+
CREATE TABLE IF NOT EXISTS users (
9+
id SERIAL PRIMARY KEY,
10+
username VARCHAR(50) NOT NULL UNIQUE,
11+
email VARCHAR(100) NOT NULL,
12+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
13+
);
14+
15+
-- Create the 'orders' table
16+
CREATE TABLE IF NOT EXISTS orders (
17+
id SERIAL PRIMARY KEY,
18+
user_id INTEGER REFERENCES users(id),
19+
product VARCHAR(100),
20+
amount INTEGER,
21+
ordered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
22+
);
23+
24+
-- Insert sample users
25+
INSERT INTO users (username, email) VALUES
26+
('alice', '[email protected]'),
27+
('bob', '[email protected]');
28+
29+
-- Insert sample orders
30+
INSERT INTO orders (user_id, product, amount) VALUES
31+
(1, 'Widget', 3),
32+
(2, 'Gadget', 5);
33+
34+
DO $$
35+
BEGIN
36+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_readonly') THEN
37+
CREATE ROLE app_readonly LOGIN;
38+
END IF;
39+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_readwrite') THEN
40+
CREATE ROLE app_readwrite LOGIN;
41+
END IF;
42+
END$$;
43+
44+
REVOKE CONNECT ON DATABASE appdb FROM PUBLIC;
45+
GRANT CONNECT ON DATABASE appdb TO app_readonly, app_readwrite;
46+
47+
REVOKE ALL ON SCHEMA public FROM PUBLIC;
48+
GRANT USAGE ON SCHEMA public TO app_readonly, app_readwrite;
49+
GRANT CREATE ON SCHEMA public TO app_readwrite;
50+
51+
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_readonly;
52+
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_readwrite;
53+
GRANT INSERT ON ALL TABLES IN SCHEMA public TO app_readwrite;
54+
55+
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_readonly;
56+
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO app_readwrite;
57+
58+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
59+
GRANT SELECT ON TABLES TO app_readonly;
60+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
61+
GRANT USAGE, SELECT ON SEQUENCES TO app_readonly;
62+
63+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
64+
GRANT SELECT, INSERT ON TABLES TO app_readwrite;
65+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
66+
GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO app_readwrite;

0 commit comments

Comments
 (0)