Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a new example setup using native TLS #66

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
22 changes: 22 additions & 0 deletions examples/native-tls/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Native TLS Setup

In general, the recommended way of running the platform services is
behind a reverse proxy running on the same host. This way the TLS
termination can be handled by the reverse proxy, and the plain HTTP
communication between proxy and backend containers is never exposed
to the network.

However, in some environments the reverse proxy cannot be guaranteed
to run on the same host machine as the other platform containers, or
the containers themselves are deployed to different machines.

In these scenarios, it becomes necessary to enable native TLS support
for the individual platform containers. This example setup shows how
to configure the platform for this scenario.

## Private Certificate Authority

This example assumes that certificates from a private CA and
server certificates from a private certificate authority.

An alternative
47 changes: 47 additions & 0 deletions examples/native-tls/create-certificates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env python3

# To install the requirements run
#
# python3 -m venv venv/
# . venv/bin/activate
# python3 -m pip install trustme python-dotenv

import os
import trustme
from dotenv import load_dotenv
from urllib.parse import urlparse

# Load environment variables from `.env`
load_dotenv()

# Create a private certificate authority
ca = trustme.CA()
ca.cert_pem.write_to_path("ssl/ca.pem")

# Get the DNS names for the certificates.
app_endpoint = urlparse(os.getenv("TENZIR_PLATFORM_DOMAIN")).hostname
platform_endpoint = urlparse(os.getenv("TENZIR_PLATFORM_API_ENDPOINT")).hostname
control_endpoint = urlparse(os.getenv("TENZIR_PLATFORM_CONTROL_ENDPOINT")).hostname
blobs_endpoint = urlparse(os.getenv("TENZIR_PLATFORM_BLOBS_ENDPOINT")).hostname
login_endpoint = urlparse(os.getenv("TENZIR_PLATFORM_LOGIN_ENDPOINT")).hostname


# Create certificates. Note that these combine both key and certificate,
# so the `TLS_CERTFILE` and `TLS_KEYFILE` options will point to
# the same file.
def write_certificate(server_names: list[str], filename: str) -> None:
cert = ca.issue_cert(*server_names)
cert.private_key_and_cert_chain_pem.write_to_path(filename)


# We're creating certificates that also have the service name
# as SAN, so that the containers can also use TSL for connections
# inside the container network. For a production setup, this
# might not be possible, in this case all traffic must
# be routed through the external network.
write_certificate([app_endpoint, "app", "localhost"], filename="ssl/app-cert.pem")
write_certificate([platform_endpoint, "platform"], filename="ssl/platform-cert.pem")
write_certificate([control_endpoint, "websocket-gateway"], filename="ssl/control-cert.pem")
write_certificate([blobs_endpoint, "seaweed"], filename="ssl/blobs-cert.pem")
write_certificate([login_endpoint, "keycloak"], filename="ssl/login-cert.pem")
write_certificate(["postgres"], filename="ssl/db-cert.pem")
190 changes: 190 additions & 0 deletions examples/native-tls/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# A completely self-contained configuration using a bundled
# postgres database and a bundled keycloak identity provider.
#
# Useful for test deployments with multiple users, or for
# wrapping non-OIDC authentication backends like LDAP via
# Keycloak.
#
# Container ports are mapped to the range 3000-3004.

services:
app:
# image: ghcr.io/tenzir/app:${TENZIR_PLATFORM_VERSION:-latest}
build: ../../components/app
environment:
- AUTH_TRUST_HOST=true
- PUBLIC_ENABLE_HIGHLIGHT=false
- ORIGIN=${TENZIR_PLATFORM_DOMAIN}
- PRIVATE_OIDC_PROVIDER_CLIENT_ID=${TENZIR_PLATFORM_OIDC_APP_CLIENT_ID}
- PRIVATE_OIDC_PROVIDER_CLIENT_SECRET=${TENZIR_PLATFORM_OIDC_APP_CLIENT_SECRET}
- PRIVATE_OIDC_PROVIDER_ISSUER_URL=${TENZIR_PLATFORM_OIDC_PROVIDER_ISSUER_URL}
- PRIVATE_OIDC_PROVIDER_NAME=${TENZIR_PLATFORM_OIDC_PROVIDER_NAME}
- PUBLIC_OIDC_PROVIDER_ID=${TENZIR_PLATFORM_OIDC_PROVIDER_NAME}
- PUBLIC_OIDC_SCOPES=profile email oidc
- PUBLIC_WEBSOCKET_GATEWAY_ENDPOINT=${TENZIR_PLATFORM_CONTROL_ENDPOINT}
- PRIVATE_USER_ENDPOINT=${TENZIR_PLATFORM_API_ENDPOINT}/user
- PRIVATE_WEBAPP_ENDPOINT=${TENZIR_PLATFORM_API_ENDPOINT}/webapp
- PRIVATE_WEBAPP_KEY=${TENZIR_PLATFORM_INTERNAL_APP_API_KEY}
- AUTH_SECRET=${TENZIR_PLATFORM_INTERNAL_AUTH_SECRET}
- PUBLIC_DISABLE_DEMO_NODE_AND_TOUR=${TENZIR_PLATFORM_DISABLE_LOCAL_DEMO_NODES}
- PRIVATE_DRIZZLE_DATABASE_URL=postgres://${TENZIR_PLATFORM_POSTGRES_USER}:${TENZIR_PLATFORM_POSTGRES_PASSWORD}@${TENZIR_PLATFORM_POSTGRES_HOSTNAME}/${TENZIR_PLATFORM_POSTGRES_DB}
- TLS_CERTFILE=/ssl/app-cert.pem
- TLS_KEYFILE=/ssl/app-cert.pem
# This should work according to the documentation, but in practice I had
# to mount the certificate into the system default location to have any
# effect:
- NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
volumes:
- ./ssl/app-cert.pem:/ssl/app-cert.pem
- ./ssl/ca.pem:/etc/ssl/certs/ca-certificates.crt
depends_on:
postgres:
condition: service_healthy
keycloak:
condition: service_started
ports:
- "3000:3000"

platform:
image: ghcr.io/tenzir/platform:${TENZIR_PLATFORM_VERSION}
build: ../../components/tenant-manager/platform/tenant_manager
command: ["tenant_manager/rest/server/local.py"]
environment:
- BASE_PATH=
- GATEWAY_WS_ENDPOINT=${TENZIR_PLATFORM_CONTROL_ENDPOINT}
- GATEWAY_HTTP_ENDPOINT=http://websocket-gateway:5000
- TENANT_MANAGER_DISABLE_LOCAL_DEMO_NODES=${TENZIR_PLATFORM_DISABLE_LOCAL_DEMO_NODES}
- TENZIR_DEMO_NODE_IMAGE=${TENZIR_PLATFORM_DEMO_NODE_IMAGE:-tenzir/tenzir-demo:latest}
- TENANT_MANAGER_APP_API_KEY=${TENZIR_PLATFORM_INTERNAL_APP_API_KEY}
- TENANT_MANAGER_TENANT_TOKEN_ENCRYPTION_KEY=${TENZIR_PLATFORM_INTERNAL_TENANT_TOKEN_ENCRYPTION_KEY}
- TENANT_MANAGER_AUTH__TRUSTED_AUDIENCES=${TENZIR_PLATFORM_OIDC_TRUSTED_AUDIENCES}
- TENANT_MANAGER_AUTH__ADMIN_FUNCTIONS=${TENZIR_PLATFORM_OIDC_ADMIN_RULES}
- TLS_CERTFILE=/ssl/platform-cert.pem
- TLS_KEYFILE=/ssl/platform-cert.pem
# 'requests' is using a baked-in CA bundle, so we need to point it to our CA explicitly.
- REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
- SSL_CERT_FILE=/ssl/ca.pem
- STORE__TYPE=${TENZIR_PLATFORM_STORE_TYPE}
- STORE__POSTGRES_URI=postgresql://${TENZIR_PLATFORM_POSTGRES_USER}:${TENZIR_PLATFORM_POSTGRES_PASSWORD}@${TENZIR_PLATFORM_POSTGRES_HOSTNAME}/${TENZIR_PLATFORM_POSTGRES_DB}
- TENANT_MANAGER_SIDEPATH_BUCKET_NAME=${TENZIR_PLATFORM_INTERNAL_BUCKET_NAME}
- BLOB_STORAGE__ENDPOINT_URL=http://seaweed:8333
- BLOB_STORAGE__PUBLIC_ENDPOINT_URL=${TENZIR_PLATFORM_BLOBS_ENDPOINT}
- BLOB_STORAGE__ACCESS_KEY_ID=${TENZIR_PLATFORM_INTERNAL_ACCESS_KEY_ID}
- BLOB_STORAGE__SECRET_ACCESS_KEY=${TENZIR_PLATFORM_INTERNAL_SECRET_ACCESS_KEY}
volumes:
- ./ssl/platform-cert.pem:/ssl/platform-cert.pem
- ./ssl/ca.pem:/etc/ssl/certs/ca-certificates.crt
ports:
- 3001:5000
depends_on:
websocket-gateway:
condition: service_started
postgres:
condition: service_healthy
keycloak:
condition: service_started

websocket-gateway:
image: ghcr.io/tenzir/platform:${TENZIR_PLATFORM_VERSION:-latest}
environment:
- BASE_PATH=
- TENZIR_PROXY_TIMEOUT=60
- TENANT_MANAGER_APP_API_KEY=${TENZIR_PLATFORM_INTERNAL_APP_API_KEY}
- TENANT_MANAGER_TENANT_TOKEN_ENCRYPTION_KEY=${TENZIR_PLATFORM_INTERNAL_TENANT_TOKEN_ENCRYPTION_KEY}
- STORE__TYPE=${TENZIR_PLATFORM_STORE_TYPE}
- STORE__POSTGRES_URI=postgresql://${TENZIR_PLATFORM_POSTGRES_USER}:${TENZIR_PLATFORM_POSTGRES_PASSWORD}@${TENZIR_PLATFORM_POSTGRES_HOSTNAME}/${TENZIR_PLATFORM_POSTGRES_DB}
- TLS_CERTFILE=/ssl/platform-cert.pem
- TLS_KEYFILE=/ssl/platform-cert.pem
- TLS_CAFILE=/ssl/ca.pem
depends_on:
postgres:
condition: service_healthy
volumes:
- ./ssl:/ssl
command: ["tenant_manager/ws/server/local.py"]
ports:
- 3002:5000


postgres-cert-setup:
image: postgres:14.5
user: root
entrypoint: ["/bin/sh", "-c", "cp ssl/db-cert.pem /postgres-ssl; chmod 600 /postgres-ssl/db-cert.pem; chown postgres:postgres /postgres-ssl/db-cert.pem"]
volumes:
- ./ssl:/ssl
- postgres_ssl:/postgres-ssl

postgres:
image: postgres:14.5
restart: always
command: >
-c ssl=on
-c ssl_cert_file=/ssl/db-cert.pem
-c ssl_key_file=/ssl/db-cert.pem
environment:
- POSTGRES_USER=${TENZIR_PLATFORM_POSTGRES_USER}
- POSTGRES_PASSWORD=${TENZIR_PLATFORM_POSTGRES_PASSWORD}
- POSTGRES_DB=${TENZIR_PLATFORM_POSTGRES_DB}
depends_on:
postgres-cert-setup:
condition: service_completed_successfully
volumes:
- postgres_data:/var/lib/postgresql/data
- postgres_ssl:/ssl
healthcheck:
test:
- 'CMD-SHELL'
- 'pg_isready -U postgres'
interval: 10s
timeout: 5s
retries: 5

# Note that this service takes ~25 seconds to start completely.
seaweed:
image: ghcr.io/tenzir/tenzir-seaweed:${TENZIR_PLATFORM_VERSION:-latest}
environment:
- TENZIR_PLATFORM_INTERNAL_ACCESS_KEY_ID
- TENZIR_PLATFORM_INTERNAL_SECRET_ACCESS_KEY
- TENZIR_PLATFORM_INTERNAL_BUCKET_NAME
command: ["server", "-dir=/var/lib/seaweedfs", "-s3", "-s3.config=/config.json", "-s3.port.https=8333", "-s3.cert.file=/ssl/blobs-cert.pem", "-s3.key.file=/ssl/blobs-cert.pem"]
volumes:
- seaweed_data:/var/lib/seaweedfs
- ./ssl:/ssl
ports:
- '3003:8333'

keycloak:
image: keycloak/keycloak
environment:
- KC_BOOTSTRAP_ADMIN_USERNAME=admin
- KC_BOOTSTRAP_ADMIN_PASSWORD=changeme
- KC_DB=postgres
- KC_DB_URL=jdbc:postgresql://${TENZIR_PLATFORM_POSTGRES_HOSTNAME}/${TENZIR_PLATFORM_POSTGRES_DB}
- KC_DB_USERNAME=${TENZIR_PLATFORM_POSTGRES_USER}
- KC_DB_PASSWORD=${TENZIR_PLATFORM_POSTGRES_PASSWORD}
- KC_HOSTNAME=${TENZIR_PLATFORM_LOGIN_ENDPOINT}
- KC_HTTPS_CERTIFICATE_FILE=/ssl/login-cert.pem
- KC_HTTPS_CERTIFICATE_KEY_FILE=/ssl/login-cert.pem
# For a production setup, use an invocation like
# start --https-certificate-file=/path/to/certfile.pem --https-certificate-key-file=/path/to/keyfile.pem --hostname https://my.keycloak.org
# or, when running behind a reverse proxy
# start --hostname https://my.keycloak.org --http-enabled true
command: ["start"]
depends_on:
postgres:
condition: service_healthy
volumes:
- keycloak_data:/opt/keycloak/data
- ./ssl:/ssl
ports:
- '3004:8443'

volumes:
postgres_data:
driver: local
postgres_ssl:
driver: local
seaweed_data:
driver: local
keycloak_data:
driver: local
70 changes: 70 additions & 0 deletions examples/native-tls/env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# The docker image tag that is used for platform deployment
# See https://ghcr.io/tenzir/platform
TENZIR_PLATFORM_VERSION=latest

# The `platform.local` placeholder domain needs to be
# replaced by an ip or hostname under which the host running
# the docker compose file is reachable.
#
# **NOTE**: The issuer URL must be a hostname or ip address
# that can be used to reach the container *both* from within
# the docker network and from a user's browser. In particular
# `localhost` will not work.
#
# Also, the user id referenced in `TENZIR_PLATFORM_OIDC_ADMIN_RULES`
# needs to be replaced by the user id of the intended admin user once
# that user is created in keycloak.

TENZIR_PLATFORM_DOMAIN=https://platform.local:3000
TENZIR_PLATFORM_API_ENDPOINT=https://platform.local:3001
TENZIR_PLATFORM_CONTROL_ENDPOINT=wss://platform.local:3002
TENZIR_PLATFORM_BLOBS_ENDPOINT=https://platform.local:3003
TENZIR_PLATFORM_LOGIN_ENDPOINT=https://platform.local:3004
TENZIR_PLATFORM_OIDC_PROVIDER_ISSUER_URL=https://platform.local:3004/realms/master

TENZIR_PLATFORM_OIDC_PROVIDER_NAME=keycloak
TENZIR_PLATFORM_OIDC_TRUSTED_AUDIENCES='{"issuer": "https://platform.local:3004/realms/master","audiences": ["tenzir-app"]}'
TENZIR_PLATFORM_OIDC_ADMIN_RULES='[{"auth_fn":"auth_user","user_id":"00000000-0000-0000-0000-000000000000"}]'

TENZIR_PLATFORM_OIDC_CLI_CLIENT_ID=tenzir-cli
TENZIR_PLATFORM_OIDC_APP_CLIENT_ID=tenzir-app
TENZIR_PLATFORM_OIDC_APP_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx

# If local demo nodes are enabled, the platform may spawn demo nodes
# as local docker containers. Requires `docker.sock` to be mounted
# in the container and a demo node image available locally.
TENZIR_PLATFORM_DISABLE_LOCAL_DEMO_NODES=false
TENZIR_PLATFORM_DEMO_NODE_IMAGE=tenzir/tenzir-node:latest

# Database connection
TENZIR_PLATFORM_STORE_TYPE=postgres
TENZIR_PLATFORM_POSTGRES_USER=postgres
TENZIR_PLATFORM_POSTGRES_PASSWORD=postgres
TENZIR_PLATFORM_POSTGRES_DB=platform
TENZIR_PLATFORM_POSTGRES_HOSTNAME=postgres:5432


# -------------------------------------------------------------------------------------------
# Variables that are not user-facing, these should move into a separate file later on.
# -------------------------------------------------------------------------------------------

# Secrets used by the platform
# - AUTH_SECRET:
# An arbitrary random string used as key to encrypt frontend cookies.
# Generate with `openssl rand -hex 32`.
# - TENANT_TOKEN_ENCRYPTION_KEY:
# Encryption key used to generate user keys
# Generate with `openssl rand 32 | base64`.
# - APP_API_KEY:
# An arbitrary random string used by the app to access the `/webapp` API.
# Generate with `openssl rand -hex 32`.
TENZIR_PLATFORM_INTERNAL_AUTH_SECRET=0000000000000000000000000000000000000000000000000000000000000000
TENZIR_PLATFORM_INTERNAL_TENANT_TOKEN_ENCRYPTION_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
TENZIR_PLATFORM_INTERNAL_APP_API_KEY=0000000000000000000000000000000000000000000000000000000000000000

# The access key must be valid and must have read and write permissions on the bucket.
# When using the bundled seaweed instance, these are also arbitrary strings that
# are automatically written into `/config.json` in the seaweed container.
TENZIR_PLATFORM_INTERNAL_BUCKET_NAME=platform-bucket
TENZIR_PLATFORM_INTERNAL_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TENZIR_PLATFORM_INTERNAL_SECRET_ACCESS_KEY=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
Empty file added examples/native-tls/ssl/.keepme
Empty file.