diff --git a/.github/workflows/check-compose-sync.yml b/.github/workflows/check-compose-sync.yml index 78a51b6..8fdc551 100644 --- a/.github/workflows/check-compose-sync.yml +++ b/.github/workflows/check-compose-sync.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install yq uses: mikefarah/yq@v4 diff --git a/.github/workflows/smoke-test-services.yml b/.github/workflows/smoke-test-services.yml index 70c4db9..d9cdad4 100644 --- a/.github/workflows/smoke-test-services.yml +++ b/.github/workflows/smoke-test-services.yml @@ -26,10 +26,10 @@ jobs: contents: read # Required for checkout steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: arn:aws:iam::761136292957:role/GitHubActions-CurrentsDevDocker-ECRPull aws-region: us-east-1 diff --git a/.github/workflows/validate-compose.yml b/.github/workflows/validate-compose.yml index e9e82d9..f25cd14 100644 --- a/.github/workflows/validate-compose.yml +++ b/.github/workflows/validate-compose.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Print versions run: | @@ -64,6 +64,7 @@ jobs: options: --privileged steps: - name: Install dependencies + timeout-minutes: 5 run: | dnf install -y podman git curl # Download pre-built docker-compose binary (avoids pip compilation issues) @@ -71,7 +72,7 @@ jobs: chmod +x /usr/local/bin/docker-compose - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Print versions run: | diff --git a/docs/README.md b/docs/README.md index b099fdb..95349c7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,6 +13,7 @@ The Docker Compose configuration is modular, allowing you to choose which data s - [🚀 Quickstart Guide](./quickstart.md) - [Container Image Access](./container-images.md) - [Configuration Reference](./configuration.md) +- [Enable SAML SSO](./sso-saml.md) - [Logging Configuration](./logging.md) - [Backup and Restore](./backup-restore.md) - [Upgrading Currents On-Prem](./upgrading.md) diff --git a/docs/backup-restore.md b/docs/backup-restore.md index fda316c..e36d499 100644 --- a/docs/backup-restore.md +++ b/docs/backup-restore.md @@ -12,6 +12,7 @@ Currents stores data in several locations: | ClickHouse | `data/clickhouse` | Analytics and reporting data | | Redis | `data/redis` | Cache and session data (optional to backup) | | RustFS | `data/rustfs` | Artifacts, screenshots, videos (if using the provided rustfs) | +| SAML SSO | `data/sso` | IdP metadata XML + optional SP cert/key (only if SSO is enabled) | ## Before You Begin @@ -124,6 +125,16 @@ tar -czvf redis-backup-$(date +%Y%m%d).tar.gz data/redis/ docker compose start ``` +### SAML SSO Files (Optional) + +If SAML SSO is enabled, `data/sso` holds your IdP metadata XML (and, if you sign AuthnRequests, the SP certificate/key PEMs). It is included in the full `data/` backup above; to back up just this folder: + +```bash +tar -czvf sso-backup-$(date +%Y%m%d).tar.gz data/sso/ +``` + +> **Important:** If you configured an SP private key (`sp-key.pem`), this folder contains a secret — store the archive securely. The IdP metadata itself can always be re-downloaded from your identity provider, so this backup is a convenience rather than a hard requirement. + ## Restore Procedures ### Prerequisites @@ -220,3 +231,14 @@ tar -xzvf rustfs-backup-YYYYMMDD.tar.gz docker compose up -d ``` +### SAML SSO Restore + +```bash +docker compose down +rm -rf data/sso/ +tar -xzvf sso-backup-YYYYMMDD.tar.gz +docker compose up -d +``` + +> **Podman users:** the API reads this folder as uid 1000 — after extracting, run `podman unshare chown -R 1000:1000 data/sso` (rootless) or `sudo chown -R 1000:1000 data/sso` (rootful). See the [quickstart troubleshooting](./quickstart.md#podman-permission-denied-errors) section. + diff --git a/docs/configuration.md b/docs/configuration.md index 4fda044..7ad0a5b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -69,6 +69,21 @@ Less commonly changed settings with sensible defaults. | `INVITE_EXPIRATION_DAYS` | int | `14` | Number of days before invitations expire | | `EMAIL_LINKS_BASE_URL` | string | `${APP_BASE_URL}` | Base URL for links in emails (derived from APP_BASE_URL) | +### SAML SSO (Optional) + +Enable SAML single sign-on by setting the two required variables and mounting your IdP metadata file into `./data/sso`. See [Enable SAML SSO](sso-saml.md) for the full walkthrough. SSO turns on automatically once both required variables are set — there is no separate enable flag. + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `SSO_SAML_IDP_METADATA_FILE` | string | _(empty)_ | Path to the mounted IdP metadata XML (e.g. `/etc/currents/sso/idp-metadata.xml`). **Required to enable SSO.** | +| `SSO_SAML_ISSUER` | string | _(empty)_ | SP entityID / audience — a stable opaque identifier (e.g. `currents-onprem:your-org`). Must match the Audience / SP Entity ID configured in your IdP. **Required to enable SSO.** | +| `SSO_SAML_PROVIDER_ID` | string | `onprem-saml` | Provider id; recommend your IdP name (e.g. `okta`). Becomes the last path segment of the ACS callback URL. | +| `SSO_SAML_DEFAULT_ROLE` | string | `member` | Role granted to auto-provisioned SSO users | +| `SSO_ALLOWED_DOMAINS` | string | _(empty)_ | Comma-separated email domains allowed to sign in via SSO | +| `SSO_SAML_AUTHN_REQUESTS_SIGNED` | bool | `false` | Sign outbound AuthnRequests (requires the SP key/cert below) | +| `SSO_SAML_SP_CERT_FILE` | string | _(empty)_ | Path to the mounted SP certificate PEM (only when signing requests) | +| `SSO_SAML_SP_KEY_FILE` | string | _(empty)_ | Path to the mounted SP private key PEM (only when signing requests) | + ### Docker Compose Configuration These variables configure Docker Compose behavior only (not passed to containers). All are optional with sensible defaults. @@ -107,6 +122,7 @@ These variables configure Docker Compose behavior only (not passed to containers | `DC_CLICKHOUSE_VOLUME` | string | `./data/clickhouse` | ClickHouse data volume path | | `DC_RUSTFS_VOLUME` | string | `./data/rustfs` | RustFS data volume path | | `DC_SCHEDULER_STARTUP_VOLUME` | string | `./data/startup` | Scheduler startup data volume | +| `DC_SSO_VOLUME` | string | `./data/sso` | SAML SSO files directory (IdP metadata + optional SP PEMs), mounted read-only to `/etc/currents/sso` | #### Traefik Configuration diff --git a/docs/index.md b/docs/index.md index d437aa7..504b2a8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,6 +12,7 @@ Docker Compose configuration for self-hosted Currents deployment. - [Quickstart Guide](quickstart.md) — Get up and running with Docker Compose - [Configuration Reference](configuration.md) — All environment variables and settings +- [Enable SAML SSO](sso-saml.md) — Delegate sign-in to your SAML identity provider - [Logging Configuration](logging.md) — Configure container logging for production - [Backup and Restore](backup-restore.md) — Backup and restore procedures - [Upgrading Currents On-Prem](upgrading.md) — Upgrade workflows and version management diff --git a/docs/quickstart.md b/docs/quickstart.md index 7ac8861..2463c9c 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -153,6 +153,17 @@ Common SMTP configurations: | Mailgun | `smtp.mailgun.org` | 587 | false | | Gmail | `smtp.gmail.com` | 587 | false | +### SAML SSO (optional) + +To let users sign in through your SAML identity provider (Okta, Entra ID, etc.), create a SAML app in your IdP, mount its metadata XML into `./data/sso`, and set two variables: + +```bash +SSO_SAML_IDP_METADATA_FILE=/etc/currents/sso/idp-metadata.xml +SSO_SAML_ISSUER=currents-onprem:your-org +``` + +See [Enable SAML SSO](./sso-saml.md) for the full walkthrough. + See [Configuration Reference](./configuration.md) for all available options. ## Step 4: Start Services @@ -322,7 +333,7 @@ This is due to Podman's rootless mode and UID mapping. Follow these steps: Create the data directories manually before starting services: ```bash -mkdir -p data/mongodb data/redis data/clickhouse data/rustfs data/startup data/traefik/certs data/traefik/config +mkdir -p data/mongodb data/redis data/clickhouse data/rustfs data/startup data/sso data/traefik/certs data/traefik/config ``` #### Step 2: Set Permissions @@ -346,6 +357,9 @@ podman unshare chown -R 10001:10001 data/rustfs # Scheduler runs as uid 1000 podman unshare chown -R 1000:1000 data/startup +# API runs as uid 1000 (only needed if using SAML SSO) +podman unshare chown -R 1000:1000 data/sso + # Traefik runs as root (uid 0) - no chown needed, just create dirs ``` @@ -366,6 +380,9 @@ sudo chown -R 10001:10001 data/rustfs # Scheduler runs as uid 1000 sudo chown -R 1000:1000 data/startup +# API runs as uid 1000 (only needed if using SAML SSO) +sudo chown -R 1000:1000 data/sso + # Traefik runs as root (uid 0) - no chown needed ``` @@ -387,6 +404,7 @@ sudo chcon -Rt svirt_sandbox_file_t data/redis sudo chcon -Rt svirt_sandbox_file_t data/clickhouse sudo chcon -Rt svirt_sandbox_file_t data/rustfs sudo chcon -Rt svirt_sandbox_file_t data/startup +sudo chcon -Rt svirt_sandbox_file_t data/sso sudo chcon -Rt svirt_sandbox_file_t data/traefik ``` @@ -515,6 +533,7 @@ DC_CLICKHOUSE_VOLUME=clickhouse-data | `DC_CLICKHOUSE_VOLUME` | `./data/clickhouse` | ClickHouse data storage | | `DC_RUSTFS_VOLUME` | `./data/rustfs` | RustFS object storage | | `DC_SCHEDULER_STARTUP_VOLUME` | `./data/startup` | Scheduler startup state | +| `DC_SSO_VOLUME` | `./data/sso` | SAML SSO files, mounted read-only | > **Tip:** Named Docker volumes are useful when you need encryption, network-attached storage, or custom volume drivers that aren't possible with bind mounts. diff --git a/docs/sso-saml.md b/docs/sso-saml.md new file mode 100644 index 0000000..eaf15e7 --- /dev/null +++ b/docs/sso-saml.md @@ -0,0 +1,106 @@ +# Enable SAML SSO + +Currents on-prem can delegate sign-in to your SAML 2.0 identity provider (Okta, Microsoft Entra ID, Google Workspace, etc.). Once enabled, users sign in **email-first**: they enter their email, are redirected to your IdP to authenticate, and on return are automatically provisioned into your organization. + +SSO turns on automatically once the two required variables are set and the IdP metadata file is mounted. + +## Prerequisites + +- A running on-prem deployment (see the [Quickstart Guide](./quickstart.md)). +- **SMTP configured** — email is used for account provisioning and invitations. See the [SMTP Configuration](./quickstart.md#smtp-configuration) section. +- Admin access to your identity provider to create a SAML application. + +## Step 1 — Choose two identifiers + +You'll reuse these in both your IdP and your `.env`: + +- **Provider id** (`SSO_SAML_PROVIDER_ID`) — set it to your IdP's service name, e.g. `okta`. It becomes the last path segment of the callback URL. +- **Issuer** (`SSO_SAML_ISSUER`) — a **stable, opaque** identifier that names this deployment as a SAML Service Provider. Use the form `currents-onprem:`, e.g. `currents-onprem:acme`. + +> ⚠️ **The issuer is not a URL.** Use a stable string like `currents-onprem:acme`. It only has to match the "Audience / SP Entity ID" you enter in your IdP — it does not need to resolve, and using your deployment URL is discouraged because that URL can change. + +## Step 2 — Create the SAML app in your IdP + +Create a new **SAML 2.0** application. Using Okta as the example, set: + +| IdP field | Value | +|-----------|-------| +| **Single Sign-On URL** (a.k.a. ACS / Assertion Consumer Service URL) | `/api/auth/sso/saml2/callback/okta` | +| **Audience URI** (a.k.a. SP Entity ID) | `currents-onprem:acme` | +| **Name ID format** | `Persistent` | +| **Application username** | Email | + +Replace `` with your deployment's base URL (the value of `APP_BASE_URL` in your `.env`, e.g. `https://currents.acme.com`), `okta` with your `SSO_SAML_PROVIDER_ID`, and `currents-onprem:acme` with your `SSO_SAML_ISSUER`. + +Then **download the application's IdP metadata XML** (in Okta: the app's *Metadata URL*, or *SAML Setup Instructions → Identity Provider metadata*). This one file contains the IdP sign-on URL, entity ID, and signing certificate — Currents reads all of them from it. + +## Step 3 — Provide the metadata file + +Drop the metadata XML into `./data/sso` (bind-mounted read-only into the API container at `/etc/currents/sso`): + +```bash +mkdir -p ./data/sso +cp ~/Downloads/okta-metadata.xml ./data/sso/idp-metadata.xml +``` + +## Step 4 — Configure and restart + +Add the following to your `.env`: + +```bash +# Required +SSO_SAML_IDP_METADATA_FILE=/etc/currents/sso/idp-metadata.xml +SSO_SAML_ISSUER=currents-onprem:acme + +# Recommended +SSO_SAML_PROVIDER_ID=okta +``` + +Restart the stack to apply: + +```bash +docker compose up -d +``` + +## Step 5 — Sign in + +Go to your dashboard and enter your email on the sign-in screen. If your email is not the root admin's local password account, you'll be redirected to your IdP; after authenticating you'll be returned and provisioned into the organization. + +> New SSO users are added to your single on-prem organization automatically, with the role set by `SSO_SAML_DEFAULT_ROLE` (default `member`). The root admin account continues to sign in with its email + password. + +## Optional configuration + +| Variable | Purpose | +|----------|---------| +| `SSO_SAML_DEFAULT_ROLE` | Role for auto-provisioned SSO users. Default `member`. | +| `SSO_ALLOWED_DOMAINS` | Comma-separated allow-list of email domains, e.g. `acme.com,acme.io`. Defense-in-depth on top of your IdP. | +| `SSO_SAML_AUTHN_REQUESTS_SIGNED` | Set `true` to sign outbound AuthnRequests. Requires an SP key/cert (below). | +| `SSO_SAML_SP_CERT_FILE` / `SSO_SAML_SP_KEY_FILE` | Paths to your SP certificate/private key PEMs (place them in `./data/sso` too). Only needed for signed requests. | + +### Signing AuthnRequests (advanced) + +If your IdP requires signed requests, generate a self-signed SP keypair, drop it in `./data/sso`, and point the vars at it: + +```bash +openssl req -x509 -newkey rsa:2048 -nodes \ + -keyout ./data/sso/sp-key.pem -out ./data/sso/sp-cert.pem -days 825 \ + -subj "/CN=currents-onprem" +``` + +```bash +SSO_SAML_AUTHN_REQUESTS_SIGNED=true +SSO_SAML_SP_CERT_FILE=/etc/currents/sso/sp-cert.pem +SSO_SAML_SP_KEY_FILE=/etc/currents/sso/sp-key.pem +``` + +## Rotating the IdP certificate + +The metadata file is the single source for the IdP sign-on URL, entity ID, and signing certificate. To rotate the IdP's signing certificate, download the refreshed metadata XML, replace `./data/sso/idp-metadata.xml`, and restart the API — no other configuration changes are needed. + +## Troubleshooting + +- **Sign-in isn't redirecting to the IdP** — confirm both `SSO_SAML_IDP_METADATA_FILE` and `SSO_SAML_ISSUER` are set and the file exists at the mounted path; SSO is off until both are present. Check the API logs at startup for SSO config validation errors. +- **IdP rejects the request / "audience mismatch"** — the **Audience / SP Entity ID** in your IdP must exactly equal `SSO_SAML_ISSUER`. +- **Assertion signature invalid** — the metadata XML is stale or from the wrong app; re-download it from the IdP and replace the file. + +See the [Configuration Reference](./configuration.md#saml-sso-optional) for the full list of SSO variables. diff --git a/on-prem/.env.example b/on-prem/.env.example index e9b3fc0..82419ba 100644 --- a/on-prem/.env.example +++ b/on-prem/.env.example @@ -126,6 +126,36 @@ INVITE_EXPIRATION_DAYS= # CORALOGIX_API_KEY= # CORALOGIX_APP_NAME="customer-name" +# ============================================================================= +# SAML SSO (Optional) +# ============================================================================= +# Let users sign in through your SAML identity provider (Okta, Entra ID, etc.). +# SSO turns on automatically once the two required vars below are set and the IdP +# metadata file is mounted — there is no separate enable flag. Full walkthrough: +# docs/sso-saml.md. +# +# Setup summary: +# 1. Create a SAML app in your IdP with: +# ACS / Single Sign-On URL: /api/auth/sso/saml2/callback/okta +# Audience / SP Entity ID: currents-onprem:your-org +# 2. mkdir -p ./data/sso, then download the app's metadata XML to +# ./data/sso/idp-metadata.xml (mounted read-only to /etc/currents/sso). +# 3. Set the required vars below and restart. +# +# Required to enable SSO: +# SSO_SAML_IDP_METADATA_FILE=/etc/currents/sso/idp-metadata.xml +# SSO_SAML_ISSUER=currents-onprem:your-org # stable opaque ID (NOT a URL); must match the IdP Audience +# +# Recommended: +# SSO_SAML_PROVIDER_ID=okta # your IdP name; last segment of the ACS URL (default: onprem-saml) +# +# Optional: +# SSO_SAML_DEFAULT_ROLE=member # role for auto-provisioned SSO users (default: member) +# SSO_ALLOWED_DOMAINS=acme.com,acme.io # restrict SSO to these email domains +# SSO_SAML_AUTHN_REQUESTS_SIGNED=false # sign AuthnRequests (requires the SP key/cert below) +# SSO_SAML_SP_CERT_FILE=/etc/currents/sso/sp-cert.pem +# SSO_SAML_SP_KEY_FILE=/etc/currents/sso/sp-key.pem + # ============================================================================= # Docker Compose Configuration # ============================================================================= @@ -156,6 +186,7 @@ INVITE_EXPIRATION_DAYS= # DC_CLICKHOUSE_VOLUME=./data/clickhouse # DC_RUSTFS_VOLUME=./data/rustfs # DC_SCHEDULER_STARTUP_VOLUME=./data/startup +# DC_SSO_VOLUME=./data/sso # SAML SSO files (IdP metadata + optional SP PEMs) # ============================================================================= # Traefik TLS Termination (Optional Profile) diff --git a/on-prem/docker-compose.cache.yml b/on-prem/docker-compose.cache.yml index 2d96e4f..3d8a28f 100644 --- a/on-prem/docker-compose.cache.yml +++ b/on-prem/docker-compose.cache.yml @@ -1,5 +1,5 @@ # Currents On-Prem Docker Compose -# Version: staging +# Version: 2026-01-26-001 # Profile: cache - Cache (redis) # # Generated by: ./scripts/generate-compose.sh @@ -47,6 +47,8 @@ services: BETTER_AUTH_ENABLED: "true" CLICKHOUSE_PASSWORD: ${CLICKHOUSE_CURRENTS_PASSWORD} EMAIL_TRANSPORTER: smtp + volumes: + - ${DC_SSO_VOLUME:-./data/sso}:/etc/currents/sso:ro depends_on: redis: condition: service_started diff --git a/on-prem/docker-compose.database.yml b/on-prem/docker-compose.database.yml index 2326524..0a9b822 100644 --- a/on-prem/docker-compose.database.yml +++ b/on-prem/docker-compose.database.yml @@ -1,5 +1,5 @@ # Currents On-Prem Docker Compose -# Version: staging +# Version: 2026-01-26-001 # Profile: database - Database services (redis, mongodb, clickhouse) # # Generated by: ./scripts/generate-compose.sh @@ -50,6 +50,8 @@ services: BETTER_AUTH_ENABLED: "true" CLICKHOUSE_PASSWORD: ${CLICKHOUSE_CURRENTS_PASSWORD} EMAIL_TRANSPORTER: smtp + volumes: + - ${DC_SSO_VOLUME:-./data/sso}:/etc/currents/sso:ro depends_on: mongodb: condition: service_healthy diff --git a/on-prem/docker-compose.full.yml b/on-prem/docker-compose.full.yml index 892855c..4f78573 100644 --- a/on-prem/docker-compose.full.yml +++ b/on-prem/docker-compose.full.yml @@ -1,5 +1,5 @@ # Currents On-Prem Docker Compose -# Version: staging +# Version: 2026-01-26-001 # Profile: full - All services (redis, mongodb, clickhouse, rustfs) # # Generated by: ./scripts/generate-compose.sh @@ -52,6 +52,8 @@ services: CLICKHOUSE_PASSWORD: ${CLICKHOUSE_CURRENTS_PASSWORD} EMAIL_TRANSPORTER: smtp FILE_STORAGE_FORCE_PATH_STYLE: "true" + volumes: + - ${DC_SSO_VOLUME:-./data/sso}:/etc/currents/sso:ro depends_on: mongodb: condition: service_healthy diff --git a/on-prem/templates/compose.currents.yml b/on-prem/templates/compose.currents.yml index 1b9a8e6..4fae7a7 100644 --- a/on-prem/templates/compose.currents.yml +++ b/on-prem/templates/compose.currents.yml @@ -31,6 +31,8 @@ services: BETTER_AUTH_ENABLED: "true" CLICKHOUSE_PASSWORD: ${CLICKHOUSE_CURRENTS_PASSWORD} EMAIL_TRANSPORTER: smtp + volumes: + - ${DC_SSO_VOLUME:-./data/sso}:/etc/currents/sso:ro changestreams-worker: image: ${DC_CURRENTS_IMAGE_REPOSITORY:-currents-}change-streams:${DC_CURRENTS_IMAGE_TAG:-staging}