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
72 changes: 72 additions & 0 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: PR Validation

on:
pull_request:
branches: [main, master]
push:
branches: [main, master]

env:
CARGO_TERM_COLOR: always

jobs:
test:
name: Run Tests
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo registry
uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}

- name: Cache cargo index
uses: actions/cache@v4
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}

- name: Cache cargo build
uses: actions/cache@v4
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libclang-dev \
build-essential \
libreadline-dev \
zlib1g-dev \
flex \
bison \
libxml2-dev \
libxslt-dev \
libssl-dev \
libxml2-utils \
xsltproc \
pkg-config

- name: Install pgrx
run: cargo install --locked cargo-pgrx

- name: Initialize pgrx (download PostgreSQL binaries)
run: cargo pgrx init --pg18 download

- name: Run tests with mock Vault
run: cargo pgrx test pg18

- name: Check formatting
run: cargo fmt -- --check

- name: Run clippy
run: cargo clippy --no-default-features --features pg18 -- -D warnings
113 changes: 113 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
name: Release Docker Image

on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Docker image tag'
required: true
default: 'latest'

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push:
name: Build and Push Multi-Arch Docker Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=ref,event=branch
type=sha

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
INCLUDE_DEMO_INIT=false
cache-from: type=gha
cache-to: type=gha,mode=max

build-demo-image:
name: Build Demo Docker Image with Init Script
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}-demo
type=semver,pattern={{major}}.{{minor}}-demo
type=ref,event=branch,suffix=-demo
type=raw,value=demo

- name: Build and push Demo Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
INCLUDE_DEMO_INIT=true
cache-from: type=gha
cache-to: type=gha,mode=max
13 changes: 11 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ RUN cargo pgrx package --pg-config /usr/local/pgsql/bin/pg_config --features pg1
# Runtime stage
FROM postgres:18.1

# Build argument to include demo init script
ARG INCLUDE_DEMO_INIT=false

# Install runtime dependencies
RUN apt-get update && apt-get install -y \
libssl3 \
Expand All @@ -66,8 +69,14 @@ RUN apt-get update && apt-get install -y \
COPY --from=builder /build/target/release/pg_pii_vault-pg18/usr/local/pgsql/share/extension/* /usr/share/postgresql/18/extension/
COPY --from=builder /build/target/release/pg_pii_vault-pg18/usr/local/pgsql/lib/* /usr/lib/postgresql/18/lib/

# Copy initialization script
COPY docker-init.sql /docker-entrypoint-initdb.d/
# Conditionally copy initialization script for demo purposes only
RUN if [ "$INCLUDE_DEMO_INIT" = "true" ]; then mkdir -p /docker-entrypoint-initdb.d/; fi
COPY --chmod=0755 docker-init.sql /tmp/docker-init.sql
RUN if [ "$INCLUDE_DEMO_INIT" = "true" ]; then \
mv /tmp/docker-init.sql /docker-entrypoint-initdb.d/; \
else \
rm /tmp/docker-init.sql; \
fi

# Environment variables for Vault connection
ENV PII_VAULT_URL=""
Expand Down
85 changes: 80 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,46 @@ PostgreSQL extension for GDPR-compliant column-level encryption using HashiCorp

## Quick Start

### Installation
### Docker Compose Demo

The fastest way to try `pg_pii_vault` is using Docker Compose:

```bash
# Start all services (PostgreSQL + Vault)
docker-compose up -d

# Wait for services to be ready
docker-compose ps

# Connect to PostgreSQL
psql -h localhost -U postgres -d testdb
```

The demo environment includes:
- PostgreSQL 18 with `pg_pii_vault` extension pre-installed
- HashiCorp Vault in dev mode with Transit engine enabled
- Pre-configured connection settings
- Sample database initialization

### Production Installation

#### Option 1: Docker Image

```bash
# Pull the production image (without demo init script)
docker pull ghcr.io/g0ddest/pg_pii_vault:latest

# Run with your Vault configuration
docker run -d \
-e POSTGRES_PASSWORD=yourpassword \
-e PII_VAULT_URL=http://vault:8200 \
-e PII_VAULT_TOKEN=your-vault-token \
-e PII_VAULT_MOUNT=transit \
-p 5432:5432 \
ghcr.io/g0ddest/pg_pii_vault:latest
```

#### Option 2: Build from Source

```bash
# Prerequisites: Rust, pgrx
Expand Down Expand Up @@ -132,17 +171,45 @@ SELECT piitext_out_text(secret) FROM users WHERE id = 456;
- **IV**: 12 bytes, generated via `pg_strong_random()`
- **AAD**: `col:piitext:id:<hex_key_id>` for protection against attacks

## Distribution

### Docker Images

Docker images are automatically built for multiple architectures on each release:

- **Production Image**: `ghcr.io/g0ddest/pg_pii_vault:latest`
- PostgreSQL 18 with `pg_pii_vault` extension
- No demo initialization scripts
- Multi-arch: `linux/amd64`, `linux/arm64`

- **Demo Image**: `ghcr.io/g0ddest/pg_pii_vault:demo`
- Includes sample database setup
- For testing and demonstration purposes only
- Multi-arch: `linux/amd64`, `linux/arm64`

### Building Custom Images

```bash
# Build production image
docker build --build-arg INCLUDE_DEMO_INIT=false -t pg_pii_vault:prod .

# Build demo image
docker build --build-arg INCLUDE_DEMO_INIT=true -t pg_pii_vault:demo .
```

## Testing

```bash
# Run all tests
cargo pgrx test pg16

# Expected output:
# test tests::pg_test_piitext_basic ... ok
# test tests::pg_test_encryption_with_uuid ... ok
# test tests::pg_test_encryption_with_int ... ok
# test tests::pg_test_debug_output ... ok
# test tests::test_piitext_basic ... ok
# test tests::test_encryption_with_uuid ... ok
# test tests::test_encryption_with_int ... ok
# test tests::test_debug_output ... ok
# test tests::test_crypto_shredding_workflow ... ok
# test tests::test_re_encryption_with_different_key ... ok
```

## Configuration
Expand Down Expand Up @@ -183,6 +250,14 @@ For GDPR "right to be forgotten":
2. SELECT without `piitext_out_text()` returns CBOR JSON
3. Triggers not implemented due to pgrx limitations

## CI/CD

The project uses GitHub Actions for continuous integration and delivery:

- **PR Validation**: Runs tests, formatting checks, and clippy on every pull request
- **Release Pipeline**: Builds multi-architecture Docker images on release creation
- **Automated Testing**: All tests run using mock Vault for faster execution

## Roadmap

- [ ] Automatic encryption triggers
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ services:
build:
context: .
dockerfile: Dockerfile
args:
INCLUDE_DEMO_INIT: "true"
container_name: postgres_pii_vault
ports:
- "5432:5432"
Expand Down
2 changes: 1 addition & 1 deletion src/bin/pgrx_embed.rs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
::pgrx::pgrx_embed!();
::pgrx::pgrx_embed!();
16 changes: 9 additions & 7 deletions src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ struct CacheEntry {
expires_at: Instant,
}

static KEY_CACHE: Lazy<RwLock<HashMap<Vec<u8>, CacheEntry>>> = Lazy::new(|| {
RwLock::new(HashMap::new())
});
static KEY_CACHE: Lazy<RwLock<HashMap<Vec<u8>, CacheEntry>>> =
Lazy::new(|| RwLock::new(HashMap::new()));

pub fn get_cached_key(key_id: &[u8]) -> Option<[u8; 32]> {
let cache = KEY_CACHE.read().ok()?;
Expand All @@ -24,9 +23,12 @@ pub fn get_cached_key(key_id: &[u8]) -> Option<[u8; 32]> {

pub fn insert_into_cache(key_id: Vec<u8>, key: [u8; 32], ttl_secs: u64) {
if let Ok(mut cache) = KEY_CACHE.write() {
cache.insert(key_id, CacheEntry {
key,
expires_at: Instant::now() + Duration::from_secs(ttl_secs),
});
cache.insert(
key_id,
CacheEntry {
key,
expires_at: Instant::now() + Duration::from_secs(ttl_secs),
},
);
}
}
8 changes: 6 additions & 2 deletions src/contents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ impl<'a> From<PiiTextContents<'a>> for Vec<u8> {
fn from(contents: PiiTextContents<'a>) -> Vec<u8> {
match contents {
PiiTextContents::Staging(s) => s.as_bytes().to_vec(),
PiiTextContents::Sealed(data) => serde_cbor::to_vec(&data).expect("CBOR serialization failed"),
PiiTextContents::Sealed(data) => {
serde_cbor::to_vec(&data).expect("CBOR serialization failed")
}
}
}
}
Expand All @@ -45,7 +47,9 @@ impl<'a> From<&PiiTextContents<'a>> for Vec<u8> {
fn from(contents: &PiiTextContents<'a>) -> Vec<u8> {
match contents {
PiiTextContents::Staging(s) => s.as_bytes().to_vec(),
PiiTextContents::Sealed(data) => serde_cbor::to_vec(data).expect("CBOR serialization failed"),
PiiTextContents::Sealed(data) => {
serde_cbor::to_vec(data).expect("CBOR serialization failed")
}
}
}
}
Loading