From 28ecd87c1edd7d07f8c4506b7d3a0547bc8c7b37 Mon Sep 17 00:00:00 2001 From: peb-adr Date: Thu, 6 Mar 2025 17:43:30 +0100 Subject: [PATCH 01/12] No longer set milestone (#19) --- .github/workflows/pick-to-staging.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pick-to-staging.yml b/.github/workflows/pick-to-staging.yml index 7191ed4..5cdab2b 100644 --- a/.github/workflows/pick-to-staging.yml +++ b/.github/workflows/pick-to-staging.yml @@ -63,4 +63,3 @@ jobs: reviewers: ${{ github.event.pull_request.user.login }} assignees: ${{ github.event.pull_request.user.login }} labels: picked-to-staging - milestone: 4 From 0e82813c3845fc4f2f5a44f6881eb61b0f9dcd64 Mon Sep 17 00:00:00 2001 From: Janmtbehrens Date: Tue, 22 Jul 2025 15:34:25 +0200 Subject: [PATCH 02/12] Dockerfile Rework (#20) --- Dockerfile | 50 +++++++++++++++++++++++++++++++++++++++++------- Dockerfile.dev | 12 ------------ Makefile | 14 ++++++++++++-- dev/command.sh | 4 ++++ dev/run-tests.sh | 5 +++++ 5 files changed, 64 insertions(+), 21 deletions(-) delete mode 100644 Dockerfile.dev create mode 100644 dev/command.sh create mode 100644 dev/run-tests.sh diff --git a/Dockerfile b/Dockerfile index 461a141..a70c9b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,51 @@ -FROM caddy:2.3.0-alpine +ARG CONTEXT=prod -LABEL org.opencontainers.image.title="OpenSlides Proxy" -LABEL org.opencontainers.image.description="The proxy is the entrypoint for traffic going into an OpenSlides instance." -LABEL org.opencontainers.image.licenses="MIT" -LABEL org.opencontainers.image.source="https://github.com/OpenSlides/OpenSlides/tree/main/proxy" +FROM caddy:2.3.0-alpine as base + +## Setup +ARG CONTEXT +WORKDIR /app +ENV APP_CONTEXT=${CONTEXT} -RUN apk update && apk add --no-cache jq gettext +## Installs +RUN apk update && apk add --no-cache \ + jq \ + gettext COPY caddy_base.json /caddy_base.json COPY entrypoint /entrypoint COPY certs /certs +## External Information +LABEL org.opencontainers.image.title="OpenSlides Proxy" +LABEL org.opencontainers.image.description="The proxy is the entrypoint for traffic going into an OpenSlides instance." +LABEL org.opencontainers.image.licenses="MIT" +LABEL org.opencontainers.image.source="https://github.com/OpenSlides/OpenSlides/tree/main/proxy" + +## Command ENTRYPOINT ["/entrypoint"] -CMD ["caddy", "run", "--config", "/etc/caddy/config.json"] +COPY ./dev/command.sh ./ +RUN chmod +x command.sh +CMD ["./command.sh"] + +# Development Image + +FROM base as dev + +ENV ENABLE_LOCAL_HTTPS=1 + +# Testing Image + +FROM base as tests + +# Production Image + +FROM base as prod + +# Add appuser +RUN adduser --system --no-create-home appuser && \ + chown appuser /app/ && \ + chown appuser /etc/caddy/ && \ + chown appuser /config/caddy/ + +USER appuser diff --git a/Dockerfile.dev b/Dockerfile.dev deleted file mode 100644 index c9ec9d7..0000000 --- a/Dockerfile.dev +++ /dev/null @@ -1,12 +0,0 @@ -FROM caddy:2.3.0-alpine - -RUN apk update && apk add --no-cache jq gettext - -COPY caddy_base.json /caddy_base.json -COPY entrypoint /entrypoint -COPY certs /certs - -ENV ENABLE_LOCAL_HTTPS=1 - -ENTRYPOINT ["/entrypoint"] -CMD ["caddy", "run", "--config", "/etc/caddy/config.json"] diff --git a/Makefile b/Makefile index 519433b..0a305aa 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,13 @@ +SERVICE=proxy + +build-prod: + docker build ./ --tag "openslides-$(SERVICE)" --build-arg CONTEXT="prod" --target "prod" + build-dev: - ./make-localhost-cert.sh - docker build -t openslides-proxy-dev -f Dockerfile.dev . + docker build ./ --tag "openslides-$(SERVICE)-dev" --build-arg CONTEXT="dev" --target "dev" + +build-test: + docker build ./ --tag "openslides-$(SERVICE)-tests" --build-arg CONTEXT="tests" --target "tests" + +run-tests: + echo "Proxy has no tests" \ No newline at end of file diff --git a/dev/command.sh b/dev/command.sh new file mode 100644 index 0000000..ec45872 --- /dev/null +++ b/dev/command.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +# Same command for all contexts +caddy run --config /etc/caddy/config.json \ No newline at end of file diff --git a/dev/run-tests.sh b/dev/run-tests.sh new file mode 100644 index 0000000..8c612be --- /dev/null +++ b/dev/run-tests.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +echo "########################################################################" +echo "###################### Proxy has no tests ##############################" +echo "########################################################################" From fc1fa2eb91252cfcb4cdffbf327e3ac27d6043bd Mon Sep 17 00:00:00 2001 From: Bastian Rihm Date: Mon, 18 Aug 2025 12:05:26 +0200 Subject: [PATCH 03/12] Fix ssl certificate not generated on `build-dev` (#22) --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0a305aa..28b2004 100644 --- a/Makefile +++ b/Makefile @@ -4,10 +4,11 @@ build-prod: docker build ./ --tag "openslides-$(SERVICE)" --build-arg CONTEXT="prod" --target "prod" build-dev: + ./make-localhost-cert.sh docker build ./ --tag "openslides-$(SERVICE)-dev" --build-arg CONTEXT="dev" --target "dev" build-test: docker build ./ --tag "openslides-$(SERVICE)-tests" --build-arg CONTEXT="tests" --target "tests" run-tests: - echo "Proxy has no tests" \ No newline at end of file + echo "Proxy has no tests" From 30eb99ca51ef3e7aab403d869d97def790e4ca55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20B=C3=B6hlke?= Date: Wed, 24 Sep 2025 14:29:40 +0200 Subject: [PATCH 04/12] Replace caddy with traefik --- Dockerfile | 68 +++++++++---- README.md | 57 ++++++----- caddy_base.json | 230 ------------------------------------------ dev/command.sh | 16 ++- entrypoint | 261 ++++++++++++++++++++++++++++++++++++++---------- traefik.yml | 23 +++++ 6 files changed, 322 insertions(+), 333 deletions(-) delete mode 100644 caddy_base.json create mode 100644 traefik.yml diff --git a/Dockerfile b/Dockerfile index a70c9b5..e479b40 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,51 +1,79 @@ ARG CONTEXT=prod -FROM caddy:2.3.0-alpine as base +FROM traefik:v3.5.0 as base ## Setup ARG CONTEXT WORKDIR /app ENV APP_CONTEXT=${CONTEXT} -## Installs -RUN apk update && apk add --no-cache \ - jq \ - gettext +# Install required packages based on context +RUN apk add --no-cache curl bash -COPY caddy_base.json /caddy_base.json +# Copy configuration files +COPY traefik.yml /etc/traefik/traefik.yml COPY entrypoint /entrypoint COPY certs /certs +COPY ./dev/command.sh ./ + +# Create dynamic config directory and make scripts executable +RUN mkdir -p /etc/traefik/dynamic && \ + chmod +x /entrypoint && \ + chmod +x ./command.sh ## External Information -LABEL org.opencontainers.image.title="OpenSlides Proxy" -LABEL org.opencontainers.image.description="The proxy is the entrypoint for traffic going into an OpenSlides instance." +LABEL org.opencontainers.image.title="OpenSlides Traefik Proxy" +LABEL org.opencontainers.image.description="The Traefik proxy is the entrypoint for traffic going into an OpenSlides instance." LABEL org.opencontainers.image.licenses="MIT" -LABEL org.opencontainers.image.source="https://github.com/OpenSlides/OpenSlides/tree/main/proxy" +LABEL org.opencontainers.image.source="https://github.com/OpenSlides/OpenSlides/tree/main/openslides-traefik-proxy" + +## Health check +HEALTHCHECK --interval=30s --timeout=3s \ + CMD curl -f http://localhost:8000/ping || exit 1 ## Command ENTRYPOINT ["/entrypoint"] -COPY ./dev/command.sh ./ -RUN chmod +x command.sh CMD ["./command.sh"] # Development Image - FROM base as dev ENV ENABLE_LOCAL_HTTPS=1 +ENV TRAEFIK_LOG_LEVEL=DEBUG -# Testing Image +# Default service hosts and ports for development +ENV ACTION_HOST=backend +ENV ACTION_PORT=9002 +ENV PRESENTER_HOST=backend +ENV PRESENTER_PORT=9003 +ENV AUTOUPDATE_HOST=autoupdate +ENV AUTOUPDATE_PORT=9012 +ENV SEARCH_HOST=search +ENV SEARCH_PORT=9050 +ENV AUTH_HOST=auth +ENV AUTH_PORT=9004 +ENV CLIENT_HOST=client +ENV CLIENT_PORT=9001 +ENV ICC_HOST=icc +ENV ICC_PORT=9007 +ENV MEDIA_HOST=media +ENV MEDIA_PORT=9006 +ENV MANAGE_HOST=manage +ENV MANAGE_PORT=9008 +ENV VOTE_HOST=vote +ENV VOTE_PORT=9013 +# Testing Image FROM base as tests # Production Image - FROM base as prod -# Add appuser -RUN adduser --system --no-create-home appuser && \ - chown appuser /app/ && \ - chown appuser /etc/caddy/ && \ - chown appuser /config/caddy/ +# Add appuser for security +RUN adduser -S -D -H appuser +RUN chown -R appuser /app/ && \ + chown -R appuser /etc/traefik/ && \ + chown appuser /entrypoint && \ + chown appuser ./command.sh -USER appuser +USER appuser \ No newline at end of file diff --git a/README.md b/README.md index 6ffb57d..0594b2b 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,30 @@ -# OpenSlides Proxy - -The proxy - based on [caddy](https://hub.docker.com/_/caddy) - is the entrypoint -for traffic going into an OpenSlides instance and hides all the services needed -for production behind a single port. On the docker container this will be port -8000 . An arbitrary port from the host can then be forwarded to that (e.g. -443->8000). - -## HTTPS - -It is possible to make use of caddy's automatic https feature in order to not -having to manually generate TLS certificates. -Set `ENABLE_AUTO_HTTPS=1` and `EXTERNAL_ADDRESS=openslides.example.com` to -activate it. Caddy will then retrieve a letsencrypt certificate for that -domain. -For testing a setup e.g. -`ACME_ENDPOINT=https://acme-staging-v02.api.letsencrypt.org/directory` can also -be set to avoid hitting rate limits. -Importantly, port 80 on the host must be forwarded to port 8001 on which caddy -will answer the ACME-challenge during certificate retrieval. - -Alternatively a locally generated certificate can be used by setting -`ENABLE_LOCAL_HTTPS=1 HTTPS_CERT_FILE=path/to/crt HTTPS_CERT_FILE=path/to/key` -and providing cert and key files at the specified location. This is mostly for -dev and testing setups and is not useful for a public domain as the cert is not -issued by a trusted CA and therefore not trusted by browsers. If set, this -overrules `ENABLE_AUTO_HTTPS`. +# OpenSlides Traefik Proxy Service + +The OpenSlides Traefik proxy service is a reverse proxy based on [Traefik](https://traefik.io/) that +routes all external traffic to the appropriate OpenSlides services. + +## Overview + +This service: + +- Provides HTTPS termination with self-signed certificates for development +- Routes requests to appropriate microservices based on URL paths +- Handles WebSocket connections for real-time features +- Supports gRPC communication for the manage service + +## Configuration + +The proxy service is configured through: + +- `traefik.yml` - Static configuration +- `entrypoint` - Dynamic configuration generation based on environment variables + +### Environment Variables + +- `ENABLE_LOCAL_HTTPS` - Enable HTTPS with local certificates (default: 1 for dev) +- `TRAEFIK_LOG_LEVEL` - Log level (default: INFO) +- Service locations can be configured via `*_HOST` and `*_PORT` variables + +## License + +This service is part of OpenSlides and licensed under the MIT license. \ No newline at end of file diff --git a/caddy_base.json b/caddy_base.json deleted file mode 100644 index 9c2c18c..0000000 --- a/caddy_base.json +++ /dev/null @@ -1,230 +0,0 @@ -{ - "apps": { - "tls": { - "automation": { - "policies": [ - { - "issuers": [ - { - "module": "acme", - "challenges": { - "tls-alpn": { - "disabled": true - } - } - } - ] - } - ] - } - }, - "http": { - "servers": { - "srv0": { - "listen": [":8000"], - "allow_h2c": true, - "routes": [ - { - "handle": [ - { - "flush_interval": -1, - "handler": "reverse_proxy", - "upstreams": [ - { - "dial": "$AUTOUPDATE_HOST:$AUTOUPDATE_PORT" - } - ] - } - ], - "match": [ - { - "path": ["/system/autoupdate*"] - } - ] - }, - { - "handle": [ - { - "handler": "reverse_proxy", - "upstreams": [ - { - "dial": "$PRESENTER_HOST:$PRESENTER_PORT" - } - ] - } - ], - "match": [ - { - "path": ["/system/presenter*"] - } - ] - }, - { - "handle": [ - { - "handler": "reverse_proxy", - "upstreams": [ - { - "dial": "$SEARCH_HOST:$SEARCH_PORT" - } - ] - } - ], - "match": [ - { - "path": ["/system/search*"] - } - ] - }, - { - "handle": [ - { - "handler": "reverse_proxy", - "upstreams": [ - { - "dial": "$ACTION_HOST:$ACTION_PORT" - } - ] - } - ], - "match": [ - { - "path": ["/system/action*"] - } - ] - }, - { - "handle": [ - { - "handler": "reverse_proxy", - "upstreams": [ - { - "dial": "$MEDIA_HOST:$MEDIA_PORT" - } - ] - } - ], - "match": [ - { - "path": ["/system/media*"] - } - ] - }, - { - "handle": [ - { - "handler": "reverse_proxy", - "upstreams": [ - { - "dial": "$AUTH_HOST:$AUTH_PORT" - } - ] - } - ], - "match": [ - { - "path": ["/system/auth*", "/system/saml*"] - } - ] - }, - { - "handle": [ - { - "flush_interval": -1, - "handler": "reverse_proxy", - "upstreams": [ - { - "dial": "$ICC_HOST:$ICC_PORT" - } - ] - } - ], - "match": [ - { - "path": ["/system/icc*"] - } - ] - }, - { - "handle": [ - { - "handler": "reverse_proxy", - "upstreams": [ - { - "dial": "$VOTE_HOST:$VOTE_PORT" - } - ] - } - ], - "match": [ - { - "path": ["/system/vote*"] - } - ] - }, - { - "handle": [ - { - "handler": "reverse_proxy", - "flush_interval": -1, - "transport": { - "protocol": "http", - "versions": ["2", "h2c"] - }, - "upstreams": [ - { - "dial": "$MANAGE_HOST:$MANAGE_PORT" - } - ] - } - ], - "match": [ - { - "header": { - "Content-Type": ["application/grpc"] - } - } - ] - }, - { - "handle": [ - { - "body": "Misdirected Request", - "close": true, - "handler": "static_response", - "status_code": 421 - } - ], - "match": [ - { - "not": [ - { - "header": { - "Host": [] - } - } - ] - } - ] - }, - { - "handle": [ - { - "handler": "reverse_proxy", - "upstreams": [ - { - "dial": "$CLIENT_HOST:$CLIENT_PORT" - } - ] - } - ] - } - ], - "automatic_https": { - "disable": true - } - } - } - } - } -} diff --git a/dev/command.sh b/dev/command.sh index ec45872..33d0162 100644 --- a/dev/command.sh +++ b/dev/command.sh @@ -1,4 +1,16 @@ #!/bin/sh -# Same command for all contexts -caddy run --config /etc/caddy/config.json \ No newline at end of file +if [ "$APP_CONTEXT" = "dev" ]; then + echo "Starting Traefik in development mode..." + exec traefik +fi + +if [ "$APP_CONTEXT" = "prod" ]; then + echo "Starting Traefik in production mode..." + exec traefik +fi + +if [ "$APP_CONTEXT" = "tests" ]; then + echo "Starting Traefik in test mode..." + exec traefik +fi diff --git a/entrypoint b/entrypoint index 32d9b25..699b499 100755 --- a/entrypoint +++ b/entrypoint @@ -1,67 +1,220 @@ -#!/bin/sh +#!/bin/bash set -e -config=/etc/caddy/config.json -base=/caddy_base.json -HTTPS_CERT_FILE="${HTTPS_CERT_FILE:-/certs/cert.pem}" -HTTPS_KEY_FILE="${HTTPS_KEY_FILE:-/certs/key.pem}" - -cp $base $config - -# set defaults in base -ACTION_HOST="${ACTION_HOST:-backend}" ACTION_PORT="${ACTION_PORT:-9002}" \ -PRESENTER_HOST="${PRESENTER_HOST:-backend}" PRESENTER_PORT="${PRESENTER_PORT:-9003}" \ -AUTOUPDATE_HOST="${AUTOUPDATE_HOST:-autoupdate}" AUTOUPDATE_PORT="${AUTOUPDATE_PORT:-9012}" \ -ICC_HOST="${ICC_HOST:-icc}" ICC_PORT="${ICC_PORT:-9007}" \ -AUTH_HOST="${AUTH_HOST:-auth}" AUTH_PORT="${AUTH_PORT:-9004}" \ -SEARCH_HOST="${SEARCH_HOST:-search}" SEARCH_PORT="${SEARCH_PORT:-9050}" \ -MEDIA_HOST="${MEDIA_HOST:-media}" MEDIA_PORT="${MEDIA_PORT:-9006}" \ -MANAGE_HOST="${MANAGE_HOST:-manage}" MANAGE_PORT="${MANAGE_PORT:-9008}" \ -CLIENT_HOST="${CLIENT_HOST:-client}" CLIENT_PORT="${CLIENT_PORT:-9001}" \ -VOTE_HOST="${VOTE_HOST:-vote}" VOTE_PORT="${VOTE_PORT:-9013}" \ -envsubst < "$config" > "$config.out" && mv -f "$config.out" "$config" - -jq_write() { - filter=$1 - jq "$filter" "$config" > "$config.out" && mv -f "$config.out" "$config" +# Dynamic configuration directory +DYNAMIC_DIR="${DYNAMIC_DIR:-/etc/traefik/dynamic}" +DYNAMIC_CONFIG="${DYNAMIC_DIR}/dynamic.yml" + +# Create dynamic directory if it doesn't exist +mkdir -p "$DYNAMIC_DIR" + +# Service definitions with their default hosts and ports +declare -A SERVICES=( + ["action"]="ACTION_HOST:backend ACTION_PORT:9002 PATH:/system/action" + ["presenter"]="PRESENTER_HOST:backend PRESENTER_PORT:9003 PATH:/system/presenter" + ["autoupdate"]="AUTOUPDATE_HOST:autoupdate AUTOUPDATE_PORT:9012 PATH:/system/autoupdate" + ["icc"]="ICC_HOST:icc ICC_PORT:9007 PATH:/system/icc" + ["auth"]="AUTH_HOST:auth AUTH_PORT:9004 PATH:/system/auth,/system/saml" + ["search"]="SEARCH_HOST:search SEARCH_PORT:9050 PATH:/system/search" + ["media"]="MEDIA_HOST:media MEDIA_PORT:9006 PATH:/system/media" + ["manage"]="MANAGE_HOST:manage MANAGE_PORT:9008 GRPC:true" + ["vote"]="VOTE_HOST:vote VOTE_PORT:9013 PATH:/system/vote" + ["client"]="CLIENT_HOST:client CLIENT_PORT:9001 PATH:/ PRIORITY:1" +) + +# Function to parse service configuration +parse_service_config() { + local config="$1" + local -A params + + for item in $config; do + local key="${item%%:*}" + local value="${item#*:}" + params["$key"]="$value" + done + + echo "$(declare -p params)" } -### HTTPS ### -if [ -n "$ENABLE_LOCAL_HTTPS" ]; then - [ -f "$HTTPS_CERT_FILE" ] && [ -f "$HTTPS_KEY_FILE" ] || { - echo "ERROR: no local cert-files provided. Did you run make-localhost-cert.sh?" - exit 1 - } - jq_write ".apps.http.servers.srv0.tls_connection_policies = [{ certificate_selection: { any_tag: [ \"cert0\" ] }}]" - jq_write ".apps.tls = { certificates: { load_files: [{ certificate: \"$HTTPS_CERT_FILE\", key: \"$HTTPS_KEY_FILE\", tags: [ \"cert0\" ] }] }}" -else - if [ -n "$ENABLE_AUTO_HTTPS" ]; then - if [ -n "$EXTERNAL_ADDRESS" ]; then - echo "INFO: For the automatic https to work ports 443 and 80 of the host must be - directed to 8000 and 8001 of this container" - jq_write "del(.apps.http.servers.srv0.automatic_https)" # disabled in base - jq_write ".apps.http.https_port = 8000" - jq_write ".apps.http.http_port = 8001" - jq_write ".apps.http.servers.srv0.routes[-1].match = [{ host: [\"$EXTERNAL_ADDRESS\"]}]" - - if [ -n "$ACME_ENDPOINT" ]; then - jq_write ".apps.tls.automation.policies[0].issuers[0].ca = \"${ACME_ENDPOINT}\"" +# Start building the dynamic configuration +cat > "$DYNAMIC_CONFIG" << 'EOF' +# Dynamic configuration for OpenSlides services +http: + routers: +EOF + +# Track which services are enabled +ENABLED_SERVICES=() + +# Generate routers for enabled services +for service_name in "${!SERVICES[@]}"; do + eval "$(parse_service_config "${SERVICES[$service_name]}")" + + # Get the host environment variable name and check if it's set + host_var_name="" + for key in "${!params[@]}"; do + if [[ "$key" =~ _HOST$ ]]; then + host_var_name="$key" + break + fi + done + + # Check if the HOST environment variable is set + if [ -n "$host_var_name" ] && [ -n "${!host_var_name}" ]; then + echo "Enabling service: $service_name (${!host_var_name})" >&2 + ENABLED_SERVICES+=("$service_name") + + # Export the host and port with their default values if not set + for key in "${!params[@]}"; do + if [[ "$key" =~ _HOST$ ]] || [[ "$key" =~ _PORT$ ]]; then + export "${key}=${!key:-${params[$key]}}" fi - else - echo "ERROR: EXTERNAL_ADDRESS needed for automatic HTTPS / cert generation" - exit 1 + done + + # Generate router configuration + cat >> "$DYNAMIC_CONFIG" << EOF + # ${service_name^} service + ${service_name}: + rule: "$( + if [ "${params[GRPC]}" = "true" ]; then + echo "Header(\`Content-Type\`, \`application/grpc\`)" + elif [ -n "${params[PATH]}" ]; then + paths="${params[PATH]}" + IFS=',' read -ra PATH_ARRAY <<< "$paths" + if [ ${#PATH_ARRAY[@]} -eq 1 ]; then + echo "PathPrefix(\`${PATH_ARRAY[0]}\`)" + else + rule_parts=() + for path in "${PATH_ARRAY[@]}"; do + rule_parts+=("PathPrefix(\`$path\`)") + done + # Join array elements with " || " + result="" + for i in "${!rule_parts[@]}"; do + if [ $i -eq 0 ]; then + result="${rule_parts[$i]}" + else + result="$result || ${rule_parts[$i]}" + fi + done + echo "$result" + fi + fi + )" + service: ${service_name} + entryPoints: + - websecure +EOF + + # Add priority if specified + if [ -n "${params[PRIORITY]}" ]; then + cat >> "$DYNAMIC_CONFIG" << EOF + priority: ${params[PRIORITY]} +EOF fi + + echo "" >> "$DYNAMIC_CONFIG" + else + echo "Skipping service: $service_name (HOST not set)" >&2 fi -fi +done + +# Generate services section +cat >> "$DYNAMIC_CONFIG" << 'EOF' + + services: +EOF -### ALLOWED HOSTS ### -if [ -n "$ALLOWED_HOSTS" ]; then - for host in $ALLOWED_HOSTS; do - jq_write ".apps.http.servers.srv0.routes[-2].match[0].not[0].header.Host += [\"$host\"]" +# Generate service backends for enabled services +for service_name in "${ENABLED_SERVICES[@]}"; do + eval "$(parse_service_config "${SERVICES[$service_name]}")" + + # Get host and port variable names + host_var="" + port_var="" + for key in "${!params[@]}"; do + if [[ "$key" =~ _HOST$ ]]; then + host_var="$key" + elif [[ "$key" =~ _PORT$ ]]; then + port_var="$key" + fi done -else - jq_write "del(.apps.http.servers.srv0.routes[-2])" + + host_value="${!host_var}" + port_value="${!port_var}" + + # Determine protocol + protocol="http" + if [ "${params[GRPC]}" = "true" ]; then + protocol="h2c" + fi + + cat >> "$DYNAMIC_CONFIG" << EOF + # ${service_name^} service + ${service_name}: + loadBalancer: + servers: + - url: "${protocol}://${host_value}:${port_value}" + passHostHeader: true + +EOF +done + +# Handle HTTPS configuration for local development +if [ -n "$ENABLE_LOCAL_HTTPS" ]; then + HTTPS_CERT_FILE="${HTTPS_CERT_FILE:-/certs/cert.pem}" + HTTPS_KEY_FILE="${HTTPS_KEY_FILE:-/certs/key.pem}" + + if [ -f "$HTTPS_CERT_FILE" ] && [ -f "$HTTPS_KEY_FILE" ]; then + cat >> "$DYNAMIC_CONFIG" << EOF + +# TLS configuration for local development +tls: + certificates: + - certFile: ${HTTPS_CERT_FILE} + keyFile: ${HTTPS_KEY_FILE} + stores: + - default + stores: + default: + defaultCertificate: + certFile: ${HTTPS_CERT_FILE} + keyFile: ${HTTPS_KEY_FILE} +EOF + + # Update traefik.yml to enable HTTPS + TRAEFIK_CONFIG="${TRAEFIK_CONFIG:-/etc/traefik/traefik.yml}" + cat > "$TRAEFIK_CONFIG" << EOF +# Traefik configuration for OpenSlides with HTTPS +api: + dashboard: true + debug: true + +entryPoints: + websecure: + address: ":8000" + http: + tls: true + +providers: + file: + directory: ${DYNAMIC_DIR} + watch: true + +log: + level: ${TRAEFIK_LOG_LEVEL:-INFO} + +accessLog: {} + +serversTransport: + insecureSkipVerify: true +EOF + else + echo "ERROR: no local cert-files provided. Did you run make-localhost-cert.sh?" + exit 1 + fi fi +# Execute traefik or passed command exec "$@" diff --git a/traefik.yml b/traefik.yml new file mode 100644 index 0000000..0c63ca9 --- /dev/null +++ b/traefik.yml @@ -0,0 +1,23 @@ +# Traefik configuration for OpenSlides with HTTPS +api: + dashboard: true + debug: true + +entryPoints: + websecure: + address: ":8000" + http: + tls: true + +providers: + file: + directory: /etc/traefik/dynamic + watch: true + +log: + level: INFO + +accessLog: {} + +serversTransport: + insecureSkipVerify: true From a94de3419afdd2c2fb80614ab411e13dccb7c143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20B=C3=B6hlke?= Date: Wed, 24 Sep 2025 14:30:20 +0200 Subject: [PATCH 05/12] Use mkcert if available --- make-localhost-cert.sh | 48 +++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/make-localhost-cert.sh b/make-localhost-cert.sh index a49735f..5271e23 100755 --- a/make-localhost-cert.sh +++ b/make-localhost-cert.sh @@ -3,20 +3,52 @@ set -e cd "$(dirname "$0")" +# Ensure certs directory exists +mkdir -p certs + if [[ -f "certs/key.pem" ]] || [[ -f "certs/cert.pem" ]]; then echo "Certificate already exists." exit 0 fi -if ! type 2>&1 >/dev/null openssl ; then - echo >&2 "Error: openssl not found!" +echo "Creating certificates..." + +# Check if mkcert is available +if type mkcert >/dev/null 2>&1; then + echo "Using mkcert to generate trusted certificates..." + + # Install local CA if not already installed + mkcert -install >/dev/null 2>&1 || true + + # Generate certificate for localhost and common local addresses + mkcert -cert-file certs/cert.pem -key-file certs/key.pem \ + localhost 127.0.0.1 ::1 \ + "*.localhost" \ + "localhost.localdomain" \ + "*.localhost.localdomain" + + echo "Certificates created with mkcert (automatically trusted in browsers)" + +elif type openssl >/dev/null 2>&1; then + echo "mkcert not found, falling back to openssl..." + echo "You will need to accept a security exception for the" + echo "generated certificate in your browser manually." + + openssl req -x509 -newkey rsa:4096 -nodes -days 3650 \ + -subj "/C=DE/O=Selfsigned Test/CN=localhost" \ + -keyout certs/key.pem -out certs/cert.pem + + echo "Self-signed certificate created with openssl" + +else + echo >&2 "Error: Neither mkcert nor openssl found!" + echo >&2 "Please install either mkcert (recommended) or openssl." + echo >&2 "" + echo >&2 "To install mkcert:" + echo >&2 " - macOS: brew install mkcert" + echo >&2 " - Linux: Check https://github.com/FiloSottile/mkcert#installation" + echo >&2 " - Windows: choco install mkcert or scoop install mkcert" exit 1 fi -echo "Creating certificates..." -echo "You will need to accept an security exception for the" -echo "generated certificate in your browser manually." -openssl req -x509 -newkey rsa:4096 -nodes -days 3650 \ - -subj "/C=DE/O=Selfsigned Test/CN=localhost" \ - -keyout certs/key.pem -out certs/cert.pem echo "done" From 28464724f175660ff732708668e6f14a48499e3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20B=C3=B6hlke?= Date: Wed, 15 Oct 2025 15:02:21 +0200 Subject: [PATCH 06/12] Simplify logic using templates with simple entrypoint --- Dockerfile | 18 ++- entrypoint | 239 ++++++++---------------------------- services/action.router | 5 + services/action.service | 6 + services/auth.router | 5 + services/auth.service | 6 + services/autoupdate.router | 5 + services/autoupdate.service | 6 + services/client.router | 7 ++ services/client.service | 6 + services/icc.router | 5 + services/icc.service | 6 + services/manage.router | 5 + services/manage.service | 7 ++ services/media.router | 5 + services/media.service | 6 + services/presenter.router | 5 + services/presenter.service | 6 + services/search.router | 5 + services/search.service | 6 + services/vote.router | 5 + services/vote.service | 6 + templates/tls.yml | 13 ++ templates/traefik.yml | 23 ++++ 24 files changed, 211 insertions(+), 195 deletions(-) create mode 100644 services/action.router create mode 100644 services/action.service create mode 100644 services/auth.router create mode 100644 services/auth.service create mode 100644 services/autoupdate.router create mode 100644 services/autoupdate.service create mode 100644 services/client.router create mode 100644 services/client.service create mode 100644 services/icc.router create mode 100644 services/icc.service create mode 100644 services/manage.router create mode 100644 services/manage.service create mode 100644 services/media.router create mode 100644 services/media.service create mode 100644 services/presenter.router create mode 100644 services/presenter.service create mode 100644 services/search.router create mode 100644 services/search.service create mode 100644 services/vote.router create mode 100644 services/vote.service create mode 100644 templates/tls.yml create mode 100644 templates/traefik.yml diff --git a/Dockerfile b/Dockerfile index e479b40..0602172 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,25 +7,25 @@ ARG CONTEXT WORKDIR /app ENV APP_CONTEXT=${CONTEXT} -# Install required packages based on context -RUN apk add --no-cache curl bash +# curl for healthcheck, gettext for templating (envsubst) +RUN apk add --no-cache curl gettext # Copy configuration files COPY traefik.yml /etc/traefik/traefik.yml COPY entrypoint /entrypoint COPY certs /certs -COPY ./dev/command.sh ./ +COPY services /services +COPY templates /templates -# Create dynamic config directory and make scripts executable +# Create dynamic config directory and make entrypoint executable RUN mkdir -p /etc/traefik/dynamic && \ - chmod +x /entrypoint && \ - chmod +x ./command.sh + chmod +x /entrypoint ## External Information LABEL org.opencontainers.image.title="OpenSlides Traefik Proxy" LABEL org.opencontainers.image.description="The Traefik proxy is the entrypoint for traffic going into an OpenSlides instance." LABEL org.opencontainers.image.licenses="MIT" -LABEL org.opencontainers.image.source="https://github.com/OpenSlides/OpenSlides/tree/main/openslides-traefik-proxy" +LABEL org.opencontainers.image.source="https://github.com/OpenSlides/OpenSlides/tree/main/openslides-proxy" ## Health check HEALTHCHECK --interval=30s --timeout=3s \ @@ -33,7 +33,6 @@ HEALTHCHECK --interval=30s --timeout=3s \ ## Command ENTRYPOINT ["/entrypoint"] -CMD ["./command.sh"] # Development Image FROM base as dev @@ -73,7 +72,6 @@ FROM base as prod RUN adduser -S -D -H appuser RUN chown -R appuser /app/ && \ chown -R appuser /etc/traefik/ && \ - chown appuser /entrypoint && \ - chown appuser ./command.sh + chown appuser /entrypoint USER appuser \ No newline at end of file diff --git a/entrypoint b/entrypoint index 699b499..cf277cb 100755 --- a/entrypoint +++ b/entrypoint @@ -1,220 +1,89 @@ -#!/bin/bash +#!/bin/sh +# Co-authored-by: Claude set -e -# Dynamic configuration directory +# Configuration DYNAMIC_DIR="${DYNAMIC_DIR:-/etc/traefik/dynamic}" DYNAMIC_CONFIG="${DYNAMIC_DIR}/dynamic.yml" +SERVICES_DIR="${SERVICES_DIR:-/services}" -# Create dynamic directory if it doesn't exist +# Create dynamic directory mkdir -p "$DYNAMIC_DIR" -# Service definitions with their default hosts and ports -declare -A SERVICES=( - ["action"]="ACTION_HOST:backend ACTION_PORT:9002 PATH:/system/action" - ["presenter"]="PRESENTER_HOST:backend PRESENTER_PORT:9003 PATH:/system/presenter" - ["autoupdate"]="AUTOUPDATE_HOST:autoupdate AUTOUPDATE_PORT:9012 PATH:/system/autoupdate" - ["icc"]="ICC_HOST:icc ICC_PORT:9007 PATH:/system/icc" - ["auth"]="AUTH_HOST:auth AUTH_PORT:9004 PATH:/system/auth,/system/saml" - ["search"]="SEARCH_HOST:search SEARCH_PORT:9050 PATH:/system/search" - ["media"]="MEDIA_HOST:media MEDIA_PORT:9006 PATH:/system/media" - ["manage"]="MANAGE_HOST:manage MANAGE_PORT:9008 GRPC:true" - ["vote"]="VOTE_HOST:vote VOTE_PORT:9013 PATH:/system/vote" - ["client"]="CLIENT_HOST:client CLIENT_PORT:9001 PATH:/ PRIORITY:1" -) - -# Function to parse service configuration -parse_service_config() { - local config="$1" - local -A params - - for item in $config; do - local key="${item%%:*}" - local value="${item#*:}" - params["$key"]="$value" - done - - echo "$(declare -p params)" -} - -# Start building the dynamic configuration +# Set default values and export for envsubst +export ACTION_HOST="${ACTION_HOST:-backend}" +export ACTION_PORT="${ACTION_PORT:-9002}" +export PRESENTER_HOST="${PRESENTER_HOST:-backend}" +export PRESENTER_PORT="${PRESENTER_PORT:-9003}" +export AUTOUPDATE_HOST="${AUTOUPDATE_HOST:-autoupdate}" +export AUTOUPDATE_PORT="${AUTOUPDATE_PORT:-9012}" +export ICC_HOST="${ICC_HOST:-icc}" +export ICC_PORT="${ICC_PORT:-9007}" +export AUTH_HOST="${AUTH_HOST:-auth}" +export AUTH_PORT="${AUTH_PORT:-9004}" +export SEARCH_HOST="${SEARCH_HOST:-search}" +export SEARCH_PORT="${SEARCH_PORT:-9050}" +export MEDIA_HOST="${MEDIA_HOST:-media}" +export MEDIA_PORT="${MEDIA_PORT:-9006}" +export MANAGE_HOST="${MANAGE_HOST:-manage}" +export MANAGE_PORT="${MANAGE_PORT:-9008}" +export VOTE_HOST="${VOTE_HOST:-vote}" +export VOTE_PORT="${VOTE_PORT:-9013}" +export CLIENT_HOST="${CLIENT_HOST:-client}" +export CLIENT_PORT="${CLIENT_PORT:-9001}" + +# Start building config cat > "$DYNAMIC_CONFIG" << 'EOF' -# Dynamic configuration for OpenSlides services http: routers: EOF -# Track which services are enabled -ENABLED_SERVICES=() +# Concatenate all enabled .router files +for service in action presenter autoupdate icc auth search media manage vote client; do + service_upper=$(echo "$service" | tr '[:lower:]' '[:upper:]') + host_var="${service_upper}_HOST" -# Generate routers for enabled services -for service_name in "${!SERVICES[@]}"; do - eval "$(parse_service_config "${SERVICES[$service_name]}")" - - # Get the host environment variable name and check if it's set - host_var_name="" - for key in "${!params[@]}"; do - if [[ "$key" =~ _HOST$ ]]; then - host_var_name="$key" - break - fi - done - - # Check if the HOST environment variable is set - if [ -n "$host_var_name" ] && [ -n "${!host_var_name}" ]; then - echo "Enabling service: $service_name (${!host_var_name})" >&2 - ENABLED_SERVICES+=("$service_name") - - # Export the host and port with their default values if not set - for key in "${!params[@]}"; do - if [[ "$key" =~ _HOST$ ]] || [[ "$key" =~ _PORT$ ]]; then - export "${key}=${!key:-${params[$key]}}" - fi - done - - # Generate router configuration - cat >> "$DYNAMIC_CONFIG" << EOF - # ${service_name^} service - ${service_name}: - rule: "$( - if [ "${params[GRPC]}" = "true" ]; then - echo "Header(\`Content-Type\`, \`application/grpc\`)" - elif [ -n "${params[PATH]}" ]; then - paths="${params[PATH]}" - IFS=',' read -ra PATH_ARRAY <<< "$paths" - if [ ${#PATH_ARRAY[@]} -eq 1 ]; then - echo "PathPrefix(\`${PATH_ARRAY[0]}\`)" - else - rule_parts=() - for path in "${PATH_ARRAY[@]}"; do - rule_parts+=("PathPrefix(\`$path\`)") - done - # Join array elements with " || " - result="" - for i in "${!rule_parts[@]}"; do - if [ $i -eq 0 ]; then - result="${rule_parts[$i]}" - else - result="$result || ${rule_parts[$i]}" - fi - done - echo "$result" - fi - fi - )" - service: ${service_name} - entryPoints: - - websecure -EOF - - # Add priority if specified - if [ -n "${params[PRIORITY]}" ]; then - cat >> "$DYNAMIC_CONFIG" << EOF - priority: ${params[PRIORITY]} -EOF - fi - - echo "" >> "$DYNAMIC_CONFIG" - else - echo "Skipping service: $service_name (HOST not set)" >&2 + if env | grep -q "^${host_var}="; then + eval "echo \"Enabling: $service (\$${host_var})\"" >&2 + envsubst < "$SERVICES_DIR/${service}.router" >> "$DYNAMIC_CONFIG" fi done -# Generate services section +# Add services section cat >> "$DYNAMIC_CONFIG" << 'EOF' - + services: EOF -# Generate service backends for enabled services -for service_name in "${ENABLED_SERVICES[@]}"; do - eval "$(parse_service_config "${SERVICES[$service_name]}")" - - # Get host and port variable names - host_var="" - port_var="" - for key in "${!params[@]}"; do - if [[ "$key" =~ _HOST$ ]]; then - host_var="$key" - elif [[ "$key" =~ _PORT$ ]]; then - port_var="$key" - fi - done - - host_value="${!host_var}" - port_value="${!port_var}" - - # Determine protocol - protocol="http" - if [ "${params[GRPC]}" = "true" ]; then - protocol="h2c" +# Concatenate all enabled .service files +for service in action presenter autoupdate icc auth search media manage vote client; do + service_upper=$(echo "$service" | tr '[:lower:]' '[:upper:]') + host_var="${service_upper}_HOST" + + if env | grep -q "^${host_var}="; then + envsubst < "$SERVICES_DIR/${service}.service" >> "$DYNAMIC_CONFIG" fi - - cat >> "$DYNAMIC_CONFIG" << EOF - # ${service_name^} service - ${service_name}: - loadBalancer: - servers: - - url: "${protocol}://${host_value}:${port_value}" - passHostHeader: true - -EOF done -# Handle HTTPS configuration for local development +# Generate traefik.yml from template +TRAEFIK_CONFIG="${TRAEFIK_CONFIG:-/etc/traefik/traefik.yml}" +export DYNAMIC_DIR +export TRAEFIK_LOG_LEVEL="${TRAEFIK_LOG_LEVEL:-INFO}" +envsubst < /templates/traefik.yml > "$TRAEFIK_CONFIG" + +# Handle HTTPS for local development if [ -n "$ENABLE_LOCAL_HTTPS" ]; then HTTPS_CERT_FILE="${HTTPS_CERT_FILE:-/certs/cert.pem}" HTTPS_KEY_FILE="${HTTPS_KEY_FILE:-/certs/key.pem}" - - if [ -f "$HTTPS_CERT_FILE" ] && [ -f "$HTTPS_KEY_FILE" ]; then - cat >> "$DYNAMIC_CONFIG" << EOF - -# TLS configuration for local development -tls: - certificates: - - certFile: ${HTTPS_CERT_FILE} - keyFile: ${HTTPS_KEY_FILE} - stores: - - default - stores: - default: - defaultCertificate: - certFile: ${HTTPS_CERT_FILE} - keyFile: ${HTTPS_KEY_FILE} -EOF - - # Update traefik.yml to enable HTTPS - TRAEFIK_CONFIG="${TRAEFIK_CONFIG:-/etc/traefik/traefik.yml}" - cat > "$TRAEFIK_CONFIG" << EOF -# Traefik configuration for OpenSlides with HTTPS -api: - dashboard: true - debug: true - -entryPoints: - websecure: - address: ":8000" - http: - tls: true - -providers: - file: - directory: ${DYNAMIC_DIR} - watch: true -log: - level: ${TRAEFIK_LOG_LEVEL:-INFO} - -accessLog: {} - -serversTransport: - insecureSkipVerify: true -EOF + if [ -f "$HTTPS_CERT_FILE" ] && [ -f "$HTTPS_KEY_FILE" ]; then + export HTTPS_CERT_FILE HTTPS_KEY_FILE + envsubst < /templates/tls.yml >> "$DYNAMIC_CONFIG" else echo "ERROR: no local cert-files provided. Did you run make-localhost-cert.sh?" exit 1 fi fi -# Execute traefik or passed command exec "$@" diff --git a/services/action.router b/services/action.router new file mode 100644 index 0000000..1b8e57c --- /dev/null +++ b/services/action.router @@ -0,0 +1,5 @@ + action: + rule: "PathPrefix(\`/system/action\`)" + service: action + entryPoints: + - websecure diff --git a/services/action.service b/services/action.service new file mode 100644 index 0000000..fc9bbeb --- /dev/null +++ b/services/action.service @@ -0,0 +1,6 @@ + action: + loadBalancer: + servers: + - url: "http://${ACTION_HOST}:${ACTION_PORT}" + # Forward the original Host header to the backend service + passHostHeader: true diff --git a/services/auth.router b/services/auth.router new file mode 100644 index 0000000..3464f33 --- /dev/null +++ b/services/auth.router @@ -0,0 +1,5 @@ + auth: + rule: "PathPrefix(\`/system/auth\`) || PathPrefix(\`/system/saml\`)" + service: auth + entryPoints: + - websecure diff --git a/services/auth.service b/services/auth.service new file mode 100644 index 0000000..6f9cda6 --- /dev/null +++ b/services/auth.service @@ -0,0 +1,6 @@ + auth: + loadBalancer: + servers: + - url: "http://${AUTH_HOST}:${AUTH_PORT}" + # Forward the original Host header to the backend service + passHostHeader: true diff --git a/services/autoupdate.router b/services/autoupdate.router new file mode 100644 index 0000000..9d7e8e4 --- /dev/null +++ b/services/autoupdate.router @@ -0,0 +1,5 @@ + autoupdate: + rule: "PathPrefix(\`/system/autoupdate\`)" + service: autoupdate + entryPoints: + - websecure diff --git a/services/autoupdate.service b/services/autoupdate.service new file mode 100644 index 0000000..f96f5cb --- /dev/null +++ b/services/autoupdate.service @@ -0,0 +1,6 @@ + autoupdate: + loadBalancer: + servers: + - url: "http://${AUTOUPDATE_HOST}:${AUTOUPDATE_PORT}" + # Forward the original Host header to the backend service + passHostHeader: true diff --git a/services/client.router b/services/client.router new file mode 100644 index 0000000..a96875b --- /dev/null +++ b/services/client.router @@ -0,0 +1,7 @@ + client: + rule: "PathPrefix(\`/\`)" + service: client + entryPoints: + - websecure + # Priority ensures this catch-all route is evaluated last + priority: 1 diff --git a/services/client.service b/services/client.service new file mode 100644 index 0000000..aa523a9 --- /dev/null +++ b/services/client.service @@ -0,0 +1,6 @@ + client: + loadBalancer: + servers: + - url: "http://${CLIENT_HOST}:${CLIENT_PORT}" + # Forward the original Host header to the backend service + passHostHeader: true diff --git a/services/icc.router b/services/icc.router new file mode 100644 index 0000000..b5b06c1 --- /dev/null +++ b/services/icc.router @@ -0,0 +1,5 @@ + icc: + rule: "PathPrefix(\`/system/icc\`)" + service: icc + entryPoints: + - websecure diff --git a/services/icc.service b/services/icc.service new file mode 100644 index 0000000..b6db89e --- /dev/null +++ b/services/icc.service @@ -0,0 +1,6 @@ + icc: + loadBalancer: + servers: + - url: "http://${ICC_HOST}:${ICC_PORT}" + # Forward the original Host header to the backend service + passHostHeader: true diff --git a/services/manage.router b/services/manage.router new file mode 100644 index 0000000..75f019b --- /dev/null +++ b/services/manage.router @@ -0,0 +1,5 @@ + manage: + rule: "Header(\`Content-Type\`, \`application/grpc\`)" + service: manage + entryPoints: + - websecure diff --git a/services/manage.service b/services/manage.service new file mode 100644 index 0000000..6d6104c --- /dev/null +++ b/services/manage.service @@ -0,0 +1,7 @@ + manage: + loadBalancer: + servers: + # h2c = HTTP/2 Cleartext (unencrypted) for gRPC + - url: "h2c://${MANAGE_HOST}:${MANAGE_PORT}" + # Forward the original Host header to the backend service + passHostHeader: true diff --git a/services/media.router b/services/media.router new file mode 100644 index 0000000..b774bbf --- /dev/null +++ b/services/media.router @@ -0,0 +1,5 @@ + media: + rule: "PathPrefix(\`/system/media\`)" + service: media + entryPoints: + - websecure diff --git a/services/media.service b/services/media.service new file mode 100644 index 0000000..378b83a --- /dev/null +++ b/services/media.service @@ -0,0 +1,6 @@ + media: + loadBalancer: + servers: + - url: "http://${MEDIA_HOST}:${MEDIA_PORT}" + # Forward the original Host header to the backend service + passHostHeader: true diff --git a/services/presenter.router b/services/presenter.router new file mode 100644 index 0000000..8c6e0af --- /dev/null +++ b/services/presenter.router @@ -0,0 +1,5 @@ + presenter: + rule: "PathPrefix(\`/system/presenter\`)" + service: presenter + entryPoints: + - websecure diff --git a/services/presenter.service b/services/presenter.service new file mode 100644 index 0000000..e7bc05d --- /dev/null +++ b/services/presenter.service @@ -0,0 +1,6 @@ + presenter: + loadBalancer: + servers: + - url: "http://${PRESENTER_HOST}:${PRESENTER_PORT}" + # Forward the original Host header to the backend service + passHostHeader: true diff --git a/services/search.router b/services/search.router new file mode 100644 index 0000000..fa8bb91 --- /dev/null +++ b/services/search.router @@ -0,0 +1,5 @@ + search: + rule: "PathPrefix(\`/system/search\`)" + service: search + entryPoints: + - websecure diff --git a/services/search.service b/services/search.service new file mode 100644 index 0000000..bd935e0 --- /dev/null +++ b/services/search.service @@ -0,0 +1,6 @@ + search: + loadBalancer: + servers: + - url: "http://${SEARCH_HOST}:${SEARCH_PORT}" + # Forward the original Host header to the backend service + passHostHeader: true diff --git a/services/vote.router b/services/vote.router new file mode 100644 index 0000000..97746cd --- /dev/null +++ b/services/vote.router @@ -0,0 +1,5 @@ + vote: + rule: "PathPrefix(\`/system/vote\`)" + service: vote + entryPoints: + - websecure diff --git a/services/vote.service b/services/vote.service new file mode 100644 index 0000000..96e4cbd --- /dev/null +++ b/services/vote.service @@ -0,0 +1,6 @@ + vote: + loadBalancer: + servers: + - url: "http://${VOTE_HOST}:${VOTE_PORT}" + # Forward the original Host header to the backend service + passHostHeader: true diff --git a/templates/tls.yml b/templates/tls.yml new file mode 100644 index 0000000..75561d3 --- /dev/null +++ b/templates/tls.yml @@ -0,0 +1,13 @@ + +# TLS configuration for local development +tls: + certificates: + - certFile: ${HTTPS_CERT_FILE} + keyFile: ${HTTPS_KEY_FILE} + stores: + - default + stores: + default: + defaultCertificate: + certFile: ${HTTPS_CERT_FILE} + keyFile: ${HTTPS_KEY_FILE} diff --git a/templates/traefik.yml b/templates/traefik.yml new file mode 100644 index 0000000..006cc8a --- /dev/null +++ b/templates/traefik.yml @@ -0,0 +1,23 @@ +# Traefik configuration for OpenSlides with HTTPS +api: + dashboard: true + debug: true + +entryPoints: + websecure: + address: ":8000" + http: + tls: true + +providers: + file: + directory: ${DYNAMIC_DIR} + watch: true + +log: + level: ${TRAEFIK_LOG_LEVEL} + +accessLog: {} + +serversTransport: + insecureSkipVerify: true From 08ddeaf71b12d438fad4ab2a12510aca44f35a8f Mon Sep 17 00:00:00 2001 From: Janmtbehrens Date: Thu, 16 Oct 2025 16:00:08 +0200 Subject: [PATCH 07/12] Makefile Rework (#21) --- Dockerfile | 8 ++++---- Makefile | 19 +++++++++++++------ dev/command.sh | 2 +- dev/run-lint.sh | 5 +++++ dev/run-tests.sh | 8 ++++++++ 5 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 dev/run-lint.sh diff --git a/Dockerfile b/Dockerfile index a70c9b5..8ebd9f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ ARG CONTEXT=prod -FROM caddy:2.3.0-alpine as base +FROM caddy:2.3.0-alpine AS base ## Setup ARG CONTEXT @@ -30,17 +30,17 @@ CMD ["./command.sh"] # Development Image -FROM base as dev +FROM base AS dev ENV ENABLE_LOCAL_HTTPS=1 # Testing Image -FROM base as tests +FROM base AS tests # Production Image -FROM base as prod +FROM base AS prod # Add appuser RUN adduser --system --no-create-home appuser && \ diff --git a/Makefile b/Makefile index 28b2004..0b7910e 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,21 @@ -SERVICE=proxy +override SERVICE=proxy + +# Build images for different contexts build-prod: - docker build ./ --tag "openslides-$(SERVICE)" --build-arg CONTEXT="prod" --target "prod" + docker build ./ $(ARGS) --tag "openslides-$(SERVICE)" --build-arg CONTEXT="prod" --target "prod" build-dev: ./make-localhost-cert.sh - docker build ./ --tag "openslides-$(SERVICE)-dev" --build-arg CONTEXT="dev" --target "dev" + docker build ./ $(ARGS) --tag "openslides-$(SERVICE)-dev" --build-arg CONTEXT="dev" --target "dev" + +build-tests: + docker build ./ $(ARGS) --tag "openslides-$(SERVICE)-tests" --build-arg CONTEXT="tests" --target "tests" -build-test: - docker build ./ --tag "openslides-$(SERVICE)-tests" --build-arg CONTEXT="tests" --target "tests" +# Tests run-tests: - echo "Proxy has no tests" + bash dev/run-tests.sh + +lint: + bash dev/run-lint.sh -l diff --git a/dev/command.sh b/dev/command.sh index ec45872..bcd5658 100644 --- a/dev/command.sh +++ b/dev/command.sh @@ -1,4 +1,4 @@ #!/bin/sh # Same command for all contexts -caddy run --config /etc/caddy/config.json \ No newline at end of file +caddy run --config /etc/caddy/config.json diff --git a/dev/run-lint.sh b/dev/run-lint.sh new file mode 100644 index 0000000..99dcf00 --- /dev/null +++ b/dev/run-lint.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +echo "########################################################################" +echo "####################### Proxy has no linters ###########################" +echo "########################################################################" diff --git a/dev/run-tests.sh b/dev/run-tests.sh index 8c612be..43060af 100644 --- a/dev/run-tests.sh +++ b/dev/run-tests.sh @@ -1,5 +1,13 @@ #!/bin/bash +set -e + echo "########################################################################" echo "###################### Proxy has no tests ##############################" echo "########################################################################" + +# Setup +LOCAL_PWD=$(dirname "$0") + +# Linters +bash "$LOCAL_PWD"/run-lint.sh From c64735927437740186a84bd3da5eb27ea09925b8 Mon Sep 17 00:00:00 2001 From: Adrian Richter Date: Tue, 25 Nov 2025 15:46:37 +0100 Subject: [PATCH 08/12] WIP: v1 - quite far in progress --- Dockerfile | 41 ++---- entrypoint | 236 +++++++++++++++++++++++++--------- make-localhost-cert.sh | 7 +- services/action.router | 4 +- services/auth.router | 4 +- services/autoupdate.router | 4 +- services/client.router | 4 +- services/icc.router | 4 +- services/manage.router | 4 +- services/media.router | 4 +- services/presenter.router | 4 +- services/search.router | 4 +- services/vote.router | 4 +- templates/entrypoint_acme.yml | 4 + templates/entrypoint_web.yml | 4 + templates/tls.yml | 6 +- templates/traefik.yml | 21 ++- traefik.yml | 23 ---- 18 files changed, 234 insertions(+), 148 deletions(-) create mode 100644 templates/entrypoint_acme.yml create mode 100644 templates/entrypoint_web.yml delete mode 100644 traefik.yml diff --git a/Dockerfile b/Dockerfile index 8587050..e7b4831 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ ARG CONTEXT=prod -FROM traefik:v3.5.0 as base +FROM traefik:3.6.0 as base ## Setup ARG CONTEXT @@ -11,56 +11,39 @@ ENV APP_CONTEXT=${CONTEXT} RUN apk add --no-cache curl gettext # Copy configuration files -COPY traefik.yml /etc/traefik/traefik.yml +#COPY traefik.yml /etc/traefik/traefik.yml COPY entrypoint /entrypoint COPY certs /certs COPY services /services COPY templates /templates # Create dynamic config directory and make entrypoint executable -RUN mkdir -p /etc/traefik/dynamic && \ - chmod +x /entrypoint +RUN mkdir -p /etc/traefik/dynamic +RUN chmod +x /entrypoint -## External Information +# External Information LABEL org.opencontainers.image.title="OpenSlides Traefik Proxy" LABEL org.opencontainers.image.description="The Traefik proxy is the entrypoint for traffic going into an OpenSlides instance." LABEL org.opencontainers.image.licenses="MIT" LABEL org.opencontainers.image.source="https://github.com/OpenSlides/OpenSlides/tree/main/openslides-proxy" -## Health check +# Health check HEALTHCHECK --interval=30s --timeout=3s \ - CMD curl -f http://localhost:8000/ping || exit 1 + CMD curl -f http://localhost:8080/ping -## Command +# Command ENTRYPOINT ["/entrypoint"] +COPY ./dev/command.sh ./ +RUN chmod +x command.sh +CMD ["./command.sh"] # Development Image FROM base AS dev ENV ENABLE_LOCAL_HTTPS=1 +ENV ENABLE_DASHBOARD=1 ENV TRAEFIK_LOG_LEVEL=DEBUG -# Default service hosts and ports for development -ENV ACTION_HOST=backend -ENV ACTION_PORT=9002 -ENV PRESENTER_HOST=backend -ENV PRESENTER_PORT=9003 -ENV AUTOUPDATE_HOST=autoupdate -ENV AUTOUPDATE_PORT=9012 -ENV SEARCH_HOST=search -ENV SEARCH_PORT=9050 -ENV AUTH_HOST=auth -ENV AUTH_PORT=9004 -ENV CLIENT_HOST=client -ENV CLIENT_PORT=9001 -ENV ICC_HOST=icc -ENV ICC_PORT=9007 -ENV MEDIA_HOST=media -ENV MEDIA_PORT=9006 -ENV MANAGE_HOST=manage -ENV MANAGE_PORT=9008 -ENV VOTE_HOST=vote -ENV VOTE_PORT=9013 # Testing Image FROM base AS tests diff --git a/entrypoint b/entrypoint index cf277cb..5f928e7 100755 --- a/entrypoint +++ b/entrypoint @@ -1,84 +1,152 @@ #!/bin/sh # Co-authored-by: Claude -set -e +set -ae -# Configuration +# Define variables and set defaults were applicable +TRAEFIK_CONFIG="${TRAEFIK_CONFIG:-/etc/traefik/traefik.yml}" +TRAEFIK_LOG_LEVEL="${TRAEFIK_LOG_LEVEL:-INFO}" +ENABLE_DASHBOARD="${ENABLE_DASHBOARD:-}" DYNAMIC_DIR="${DYNAMIC_DIR:-/etc/traefik/dynamic}" DYNAMIC_CONFIG="${DYNAMIC_DIR}/dynamic.yml" SERVICES_DIR="${SERVICES_DIR:-/services}" +ENABLE_LOCAL_HTTPS="${ENABLE_LOCAL_HTTPS:-}" +HTTPS_CERT_FILE="${HTTPS_CERT_FILE:-/certs/cert.pem}" +HTTPS_KEY_FILE="${HTTPS_KEY_FILE:-/certs/key.pem}" +ENABLE_AUTO_HTTPS="${ENABLE_AUTO_HTTPS:-}" +EXTERNAL_ADDRESS="${EXTERNAL_ADDRESS:-openslides.example.com}" +ACME_EMAIL="${ACME_EMAIL:-}" -# Create dynamic directory -mkdir -p "$DYNAMIC_DIR" - -# Set default values and export for envsubst -export ACTION_HOST="${ACTION_HOST:-backend}" -export ACTION_PORT="${ACTION_PORT:-9002}" -export PRESENTER_HOST="${PRESENTER_HOST:-backend}" -export PRESENTER_PORT="${PRESENTER_PORT:-9003}" -export AUTOUPDATE_HOST="${AUTOUPDATE_HOST:-autoupdate}" -export AUTOUPDATE_PORT="${AUTOUPDATE_PORT:-9012}" -export ICC_HOST="${ICC_HOST:-icc}" -export ICC_PORT="${ICC_PORT:-9007}" -export AUTH_HOST="${AUTH_HOST:-auth}" -export AUTH_PORT="${AUTH_PORT:-9004}" -export SEARCH_HOST="${SEARCH_HOST:-search}" -export SEARCH_PORT="${SEARCH_PORT:-9050}" -export MEDIA_HOST="${MEDIA_HOST:-media}" -export MEDIA_PORT="${MEDIA_PORT:-9006}" -export MANAGE_HOST="${MANAGE_HOST:-manage}" -export MANAGE_PORT="${MANAGE_PORT:-9008}" -export VOTE_HOST="${VOTE_HOST:-vote}" -export VOTE_PORT="${VOTE_PORT:-9013}" -export CLIENT_HOST="${CLIENT_HOST:-client}" -export CLIENT_PORT="${CLIENT_PORT:-9001}" - -# Start building config -cat > "$DYNAMIC_CONFIG" << 'EOF' -http: - routers: +# Set default values for service endpoints +ACTION_HOST="${ACTION_HOST:-backend}" +ACTION_PORT="${ACTION_PORT:-9002}" +PRESENTER_HOST="${PRESENTER_HOST:-backend}" +PRESENTER_PORT="${PRESENTER_PORT:-9003}" +AUTOUPDATE_HOST="${AUTOUPDATE_HOST:-autoupdate}" +AUTOUPDATE_PORT="${AUTOUPDATE_PORT:-9012}" +ICC_HOST="${ICC_HOST:-icc}" +ICC_PORT="${ICC_PORT:-9007}" +AUTH_HOST="${AUTH_HOST:-auth}" +AUTH_PORT="${AUTH_PORT:-9004}" +SEARCH_HOST="${SEARCH_HOST:-search}" +SEARCH_PORT="${SEARCH_PORT:-9050}" +MEDIA_HOST="${MEDIA_HOST:-media}" +MEDIA_PORT="${MEDIA_PORT:-9006}" +MANAGE_HOST="${MANAGE_HOST:-manage}" +MANAGE_PORT="${MANAGE_PORT:-9008}" +VOTE_HOST="${VOTE_HOST:-vote}" +VOTE_PORT="${VOTE_PORT:-9013}" +CLIENT_HOST="${CLIENT_HOST:-client}" +CLIENT_PORT="${CLIENT_PORT:-9001}" + + +# ================================= +# = Build static / install config = +# ================================= + +## Start with empty file +#echo "" > "$TRAEFIK_CONFIG" +# +## Add dashboard if enabled +#if [ -n "$ENABLE_DASHBOARD" ]; then +# echo "Enabling dashboard. 'debug: true' for now. NOT FOR PRODUCTION" +# cat >> "$TRAEFIK_CONFIG" << 'EOF' +#api: +# dashboard: true +# debug: true +#EOF +#fi +# +## Add provider to read routing config from file +#cat >> "$TRAEFIK_CONFIG" << 'EOF' +# +#providers: +# file: +# directory: ${DYNAMIC_DIR} +# watch: true +#EOF +# +## Enable /ping for HEALTHCHECK on default traefik endpoint (:8080) +#cat >> "$TRAEFIK_CONFIG" << 'EOF' +# +#ping: {} +#EOF +# +## Add logging config +#cat >> "$TRAEFIK_CONFIG" << 'EOF' +# +#log: +# level: ${TRAEFIK_LOG_LEVEL} +# +#accessLog: {} +#EOF + +# Generate base config from template +envsubst < /templates/traefik.yml > "$TRAEFIK_CONFIG" + +# Add dashboard if enabled +if [ -n "$ENABLE_DASHBOARD" ]; then + echo "Enabling dashboard. 'debug: true' for now. NOT FOR PRODUCTION" + cat >> "$TRAEFIK_CONFIG" << 'EOF' + +api: + dashboard: true + debug: true EOF +fi -# Concatenate all enabled .router files -for service in action presenter autoupdate icc auth search media manage vote client; do - service_upper=$(echo "$service" | tr '[:lower:]' '[:upper:]') - host_var="${service_upper}_HOST" +# Add entrypoint in accordance to HTTPS related variables +cat >> "$TRAEFIK_CONFIG" << 'EOF' +entryPoints: + main: + address: ":8000" + http: +EOF - if env | grep -q "^${host_var}="; then - eval "echo \"Enabling: $service (\$${host_var})\"" >&2 - envsubst < "$SERVICES_DIR/${service}.router" >> "$DYNAMIC_CONFIG" - fi -done +if [ -n "$ENABLE_LOCAL_HTTPS" ]; then + # Define tls property, which will cause all routers to terminate TLS and + # foward decrypted traffic. + cat >> "$TRAEFIK_CONFIG" << 'EOF' + tls: {} +EOF +elif [ -n "$ENABLE_AUTO_HTTPS" ]; then + # Also needs tls property, but with additional information for cert retrieval + cat >> "$TRAEFIK_CONFIG" << 'EOF' + tls: + domain: ${EXTERNAL_ADDRESS} +EOF + # Additionally a plain HTTP endpoint to answer ACME challenges on must be + # configured + echo "Configuring traefik to answer acme challenges on port 8001." + cat >> "$TRAEFIK_CONFIG" << 'EOF' -# Add services section -cat >> "$DYNAMIC_CONFIG" << 'EOF' + acme: + address: ":8001" +EOF + # Add the certificates resolver providing information for automatic ACME + # based cert retrieval. + cat >> "$TRAEFIK_CONFIG" << 'EOF' - services: +certificatesResolvers: + acmeResolver: + acme: + email: ${ACME_EMAIL} + storage: acme.json + httpChallenge: + entryPoint: acme EOF +fi -# Concatenate all enabled .service files -for service in action presenter autoupdate icc auth search media manage vote client; do - service_upper=$(echo "$service" | tr '[:lower:]' '[:upper:]') - host_var="${service_upper}_HOST" - if env | grep -q "^${host_var}="; then - envsubst < "$SERVICES_DIR/${service}.service" >> "$DYNAMIC_CONFIG" - fi -done +# ================================== +# = Build dynamic / routing config = +# ================================== -# Generate traefik.yml from template -TRAEFIK_CONFIG="${TRAEFIK_CONFIG:-/etc/traefik/traefik.yml}" -export DYNAMIC_DIR -export TRAEFIK_LOG_LEVEL="${TRAEFIK_LOG_LEVEL:-INFO}" -envsubst < /templates/traefik.yml > "$TRAEFIK_CONFIG" +# Start with empty file +echo "" > "$DYNAMIC_CONFIG" -# Handle HTTPS for local development if [ -n "$ENABLE_LOCAL_HTTPS" ]; then - HTTPS_CERT_FILE="${HTTPS_CERT_FILE:-/certs/cert.pem}" - HTTPS_KEY_FILE="${HTTPS_KEY_FILE:-/certs/key.pem}" - if [ -f "$HTTPS_CERT_FILE" ] && [ -f "$HTTPS_KEY_FILE" ]; then - export HTTPS_CERT_FILE HTTPS_KEY_FILE envsubst < /templates/tls.yml >> "$DYNAMIC_CONFIG" else echo "ERROR: no local cert-files provided. Did you run make-localhost-cert.sh?" @@ -86,4 +154,50 @@ if [ -n "$ENABLE_LOCAL_HTTPS" ]; then fi fi +# First build SERVICES list (space separated) based on files present in +# services directory +SERVICES= +for service_file in $SERVICES_DIR/*.service; do + service=$(basename $service_file .service) + service_upper=$(echo "$service" | tr '[:lower:]' '[:upper:]') + host_var="${service_upper}_HOST" + + if [[ ! -f "$SERVICES_DIR/$service.service" ]] || [[ ! -f "$SERVICES_DIR/$service.router" ]]; then + echo "Skipping, config incomplete: $service" + continue + fi + + if eval [[ -n "\$${host_var}" ]]; then + eval "echo \"Adding config: $service (host: \$${host_var})\"" >&2 + SERVICES="$SERVICES $service" + else + echo "Skipping, disabled in environment: $service" + fi +done + +# Write to config file +cat >> "$DYNAMIC_CONFIG" << 'EOF' + +http: + routers: +EOF + +# Concatenate all enabled .router files +for service in $SERVICES; do + envsubst < "$SERVICES_DIR/${service}.router" >> "$DYNAMIC_CONFIG" +done + +# Add services section +cat >> "$DYNAMIC_CONFIG" << 'EOF' + + services: +EOF + +# Concatenate all enabled .service files +for service in $SERVICES; do + envsubst < "$SERVICES_DIR/${service}.service" >> "$DYNAMIC_CONFIG" +done + + +# Finally start CMD exec "$@" diff --git a/make-localhost-cert.sh b/make-localhost-cert.sh index 5271e23..54a74e3 100755 --- a/make-localhost-cert.sh +++ b/make-localhost-cert.sh @@ -27,7 +27,10 @@ if type mkcert >/dev/null 2>&1; then "localhost.localdomain" \ "*.localhost.localdomain" - echo "Certificates created with mkcert (automatically trusted in browsers)" + echo "Certificates created with mkcert." + echo "If the certs were generated on a different machine than the one running the" + echo "browser to access the endpoint, you will first have to copy mkcerts local" + echo "rootCA there and add it to the trust store." elif type openssl >/dev/null 2>&1; then echo "mkcert not found, falling back to openssl..." @@ -45,8 +48,8 @@ else echo >&2 "Please install either mkcert (recommended) or openssl." echo >&2 "" echo >&2 "To install mkcert:" - echo >&2 " - macOS: brew install mkcert" echo >&2 " - Linux: Check https://github.com/FiloSottile/mkcert#installation" + echo >&2 " - macOS: brew install mkcert" echo >&2 " - Windows: choco install mkcert or scoop install mkcert" exit 1 fi diff --git a/services/action.router b/services/action.router index 1b8e57c..8cdc6b6 100644 --- a/services/action.router +++ b/services/action.router @@ -1,5 +1,5 @@ action: - rule: "PathPrefix(\`/system/action\`)" + rule: "PathPrefix(`/system/action`)" service: action entryPoints: - - websecure + - main diff --git a/services/auth.router b/services/auth.router index 3464f33..0282964 100644 --- a/services/auth.router +++ b/services/auth.router @@ -1,5 +1,5 @@ auth: - rule: "PathPrefix(\`/system/auth\`) || PathPrefix(\`/system/saml\`)" + rule: "PathPrefix(`/system/auth`) || PathPrefix(`/system/saml`)" service: auth entryPoints: - - websecure + - main diff --git a/services/autoupdate.router b/services/autoupdate.router index 9d7e8e4..19a3806 100644 --- a/services/autoupdate.router +++ b/services/autoupdate.router @@ -1,5 +1,5 @@ autoupdate: - rule: "PathPrefix(\`/system/autoupdate\`)" + rule: "PathPrefix(`/system/autoupdate`)" service: autoupdate entryPoints: - - websecure + - main diff --git a/services/client.router b/services/client.router index a96875b..5064e01 100644 --- a/services/client.router +++ b/services/client.router @@ -1,7 +1,7 @@ client: - rule: "PathPrefix(\`/\`)" + rule: "PathPrefix(`/`)" service: client entryPoints: - - websecure + - main # Priority ensures this catch-all route is evaluated last priority: 1 diff --git a/services/icc.router b/services/icc.router index b5b06c1..eb7ac0f 100644 --- a/services/icc.router +++ b/services/icc.router @@ -1,5 +1,5 @@ icc: - rule: "PathPrefix(\`/system/icc\`)" + rule: "PathPrefix(`/system/icc`)" service: icc entryPoints: - - websecure + - main diff --git a/services/manage.router b/services/manage.router index 75f019b..415208b 100644 --- a/services/manage.router +++ b/services/manage.router @@ -1,5 +1,5 @@ manage: - rule: "Header(\`Content-Type\`, \`application/grpc\`)" + rule: "Header(`Content-Type`, `application/grpc`)" service: manage entryPoints: - - websecure + - main diff --git a/services/media.router b/services/media.router index b774bbf..c428375 100644 --- a/services/media.router +++ b/services/media.router @@ -1,5 +1,5 @@ media: - rule: "PathPrefix(\`/system/media\`)" + rule: "PathPrefix(`/system/media`)" service: media entryPoints: - - websecure + - main diff --git a/services/presenter.router b/services/presenter.router index 8c6e0af..62fd449 100644 --- a/services/presenter.router +++ b/services/presenter.router @@ -1,5 +1,5 @@ presenter: - rule: "PathPrefix(\`/system/presenter\`)" + rule: "PathPrefix(`/system/presenter`)" service: presenter entryPoints: - - websecure + - main diff --git a/services/search.router b/services/search.router index fa8bb91..d6af74c 100644 --- a/services/search.router +++ b/services/search.router @@ -1,5 +1,5 @@ search: - rule: "PathPrefix(\`/system/search\`)" + rule: "PathPrefix(`/system/search`)" service: search entryPoints: - - websecure + - main diff --git a/services/vote.router b/services/vote.router index 97746cd..a357117 100644 --- a/services/vote.router +++ b/services/vote.router @@ -1,5 +1,5 @@ vote: - rule: "PathPrefix(\`/system/vote\`)" + rule: "PathPrefix(`/system/vote`)" service: vote entryPoints: - - websecure + - main diff --git a/templates/entrypoint_acme.yml b/templates/entrypoint_acme.yml new file mode 100644 index 0000000..6e0940c --- /dev/null +++ b/templates/entrypoint_acme.yml @@ -0,0 +1,4 @@ + +entryPoints: + amce: + address: ":8001" diff --git a/templates/entrypoint_web.yml b/templates/entrypoint_web.yml new file mode 100644 index 0000000..f3aae6e --- /dev/null +++ b/templates/entrypoint_web.yml @@ -0,0 +1,4 @@ + +entryPoints: + web: + address: ":8000" diff --git a/templates/tls.yml b/templates/tls.yml index 75561d3..024deba 100644 --- a/templates/tls.yml +++ b/templates/tls.yml @@ -1,5 +1,9 @@ -# TLS configuration for local development +# TLS configuration for using locally provided certs. +# Defining the cert files also as defaultCertificate in order for requests +# without an SNI to also be handled by it. Otherwise a self-signed cert +# generated by traefik would be used. +# See: https://doc.traefik.io/traefik/reference/routing-configuration/http/tls/tls-certificates/ tls: certificates: - certFile: ${HTTPS_CERT_FILE} diff --git a/templates/traefik.yml b/templates/traefik.yml index 006cc8a..0cc8671 100644 --- a/templates/traefik.yml +++ b/templates/traefik.yml @@ -1,23 +1,20 @@ -# Traefik configuration for OpenSlides with HTTPS -api: - dashboard: true - debug: true - -entryPoints: - websecure: - address: ":8000" - http: - tls: true +# Traefik configuration for OpenSlides +# Add provider to read routing config from file providers: file: directory: ${DYNAMIC_DIR} watch: true +# Enable /ping for HEALTHCHECK on default traefik endpoint (:8080) +ping: {} + +# Add logging config log: level: ${TRAEFIK_LOG_LEVEL} accessLog: {} -serversTransport: - insecureSkipVerify: true +# entryPoints are generated dynamically in entrypoint script as their +# definitions depend on TLS configuration + diff --git a/traefik.yml b/traefik.yml deleted file mode 100644 index 0c63ca9..0000000 --- a/traefik.yml +++ /dev/null @@ -1,23 +0,0 @@ -# Traefik configuration for OpenSlides with HTTPS -api: - dashboard: true - debug: true - -entryPoints: - websecure: - address: ":8000" - http: - tls: true - -providers: - file: - directory: /etc/traefik/dynamic - watch: true - -log: - level: INFO - -accessLog: {} - -serversTransport: - insecureSkipVerify: true From 56d4b63cc8227b3cddb859618282f796a32f06ce Mon Sep 17 00:00:00 2001 From: Adrian Richter Date: Tue, 25 Nov 2025 16:23:27 +0100 Subject: [PATCH 09/12] WIP: v2 - acme endpoint --- entrypoint | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/entrypoint b/entrypoint index 5f928e7..9e7872e 100755 --- a/entrypoint +++ b/entrypoint @@ -15,6 +15,7 @@ HTTPS_CERT_FILE="${HTTPS_CERT_FILE:-/certs/cert.pem}" HTTPS_KEY_FILE="${HTTPS_KEY_FILE:-/certs/key.pem}" ENABLE_AUTO_HTTPS="${ENABLE_AUTO_HTTPS:-}" EXTERNAL_ADDRESS="${EXTERNAL_ADDRESS:-openslides.example.com}" +ACME_ENDPOINT="${ACME_ENDPOINT:-}" ACME_EMAIL="${ACME_EMAIL:-}" # Set default values for service endpoints @@ -111,9 +112,10 @@ if [ -n "$ENABLE_LOCAL_HTTPS" ]; then EOF elif [ -n "$ENABLE_AUTO_HTTPS" ]; then # Also needs tls property, but with additional information for cert retrieval - cat >> "$TRAEFIK_CONFIG" << 'EOF' + cat >> "$TRAEFIK_CONFIG" << EOF tls: - domain: ${EXTERNAL_ADDRESS} + domains: + - main: ${EXTERNAL_ADDRESS} EOF # Additionally a plain HTTP endpoint to answer ACME challenges on must be # configured @@ -125,7 +127,7 @@ EOF EOF # Add the certificates resolver providing information for automatic ACME # based cert retrieval. - cat >> "$TRAEFIK_CONFIG" << 'EOF' + cat >> "$TRAEFIK_CONFIG" << EOF certificatesResolvers: acmeResolver: @@ -135,6 +137,11 @@ certificatesResolvers: httpChallenge: entryPoint: acme EOF + if [ -n "$ACME_ENDPOINT" ]; then + cat >> "$TRAEFIK_CONFIG" << EOF + caServer: ${ACME_ENDPOINT} +EOF + fi fi From cd0edb021f47c078ca5deda168c738707c46bdd9 Mon Sep 17 00:00:00 2001 From: Adrian Richter Date: Thu, 27 Nov 2025 14:28:04 +0100 Subject: [PATCH 10/12] Cleanup + expand readme --- Dockerfile | 11 +++++----- README.md | 23 ++++++++++++++------- entrypoint => entrypoint.sh | 41 ++----------------------------------- templates/traefik.yml | 2 +- 4 files changed, 25 insertions(+), 52 deletions(-) rename entrypoint => entrypoint.sh (85%) diff --git a/Dockerfile b/Dockerfile index e7b4831..09828fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,15 +11,14 @@ ENV APP_CONTEXT=${CONTEXT} RUN apk add --no-cache curl gettext # Copy configuration files -#COPY traefik.yml /etc/traefik/traefik.yml -COPY entrypoint /entrypoint +COPY entrypoint.sh /entrypoint.sh COPY certs /certs COPY services /services COPY templates /templates # Create dynamic config directory and make entrypoint executable RUN mkdir -p /etc/traefik/dynamic -RUN chmod +x /entrypoint +RUN chmod +x /entrypoint.sh # External Information LABEL org.opencontainers.image.title="OpenSlides Traefik Proxy" @@ -32,11 +31,12 @@ HEALTHCHECK --interval=30s --timeout=3s \ CMD curl -f http://localhost:8080/ping # Command -ENTRYPOINT ["/entrypoint"] +ENTRYPOINT ["/entrypoint.sh"] COPY ./dev/command.sh ./ RUN chmod +x command.sh CMD ["./command.sh"] + # Development Image FROM base AS dev @@ -48,6 +48,7 @@ ENV TRAEFIK_LOG_LEVEL=DEBUG # Testing Image FROM base AS tests + # Production Image FROM base AS prod @@ -55,6 +56,6 @@ FROM base AS prod RUN adduser -S -D -H appuser RUN chown -R appuser /app/ && \ chown -R appuser /etc/traefik/ && \ - chown appuser /entrypoint + chown appuser /entrypoint.sh USER appuser diff --git a/README.md b/README.md index 0594b2b..fdbe294 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ routes all external traffic to the appropriate OpenSlides services. This service: -- Provides HTTPS termination with self-signed certificates for development +- Can provide HTTPS termination with self-signed certificates for development + or with certs retrieved via ACME protocol (e.g. lets encrypt) for production - Routes requests to appropriate microservices based on URL paths - Handles WebSocket connections for real-time features - Supports gRPC communication for the manage service @@ -16,15 +17,23 @@ This service: The proxy service is configured through: -- `traefik.yml` - Static configuration -- `entrypoint` - Dynamic configuration generation based on environment variables +- `traefik.yml` - Static/install configuration +- `dynamic.yml` - Dynamic/routing configuration +- `entrypoint.sh` - Both yml config files are generated here during container startup. + - -> Environment variables are taken into account affecting the final `.yml` configuration, see below. ### Environment Variables -- `ENABLE_LOCAL_HTTPS` - Enable HTTPS with local certificates (default: 1 for dev) -- `TRAEFIK_LOG_LEVEL` - Log level (default: INFO) -- Service locations can be configured via `*_HOST` and `*_PORT` variables +- `ENABLE_LOCAL_HTTPS` - Enable HTTPS with local certificates (default: 1 in dev image) +- `TRAEFIK_LOG_LEVEL` - Log level (default: INFO, DEBUG in dev image) +- `ENABLE_DASHBOARD` - Enable traefik web-based dashboard, also sets `debug: true` for now +- `ENABLE_LOCAL_HTTPS` - Enable TLS using certs provided through `HTTPS_*_FILE`. Can be self-signed (used in dev by default) or manually generated/trusted. +- `ENABLE_AUTO_HTTPS` - Enable cert retrieval through ACME. Depends on further variables. + - `EXTERNAL_ADDRESS` - domain for which to retrieve cert + - `ACME_ENDPOINT` - when unset will fallback to traefiks default value for `acme.caServer: https://acme-v02.api.letsencrypt.org/directory` + - `ACME_EMAIL` - Email Address sent to acme endpoint during cert retrieval +- `*_HOST` and `*_PORT` - endpoints (container (host-)names) of OpenSlides microservices. Defaults should be fine in most cases. ## License -This service is part of OpenSlides and licensed under the MIT license. \ No newline at end of file +This service is part of OpenSlides and licensed under the MIT license. diff --git a/entrypoint b/entrypoint.sh similarity index 85% rename from entrypoint rename to entrypoint.sh index 9e7872e..3f60b69 100755 --- a/entrypoint +++ b/entrypoint.sh @@ -3,7 +3,7 @@ set -ae -# Define variables and set defaults were applicable +# Define variables and set defaults where applicable TRAEFIK_CONFIG="${TRAEFIK_CONFIG:-/etc/traefik/traefik.yml}" TRAEFIK_LOG_LEVEL="${TRAEFIK_LOG_LEVEL:-INFO}" ENABLE_DASHBOARD="${ENABLE_DASHBOARD:-}" @@ -45,43 +45,6 @@ CLIENT_PORT="${CLIENT_PORT:-9001}" # = Build static / install config = # ================================= -## Start with empty file -#echo "" > "$TRAEFIK_CONFIG" -# -## Add dashboard if enabled -#if [ -n "$ENABLE_DASHBOARD" ]; then -# echo "Enabling dashboard. 'debug: true' for now. NOT FOR PRODUCTION" -# cat >> "$TRAEFIK_CONFIG" << 'EOF' -#api: -# dashboard: true -# debug: true -#EOF -#fi -# -## Add provider to read routing config from file -#cat >> "$TRAEFIK_CONFIG" << 'EOF' -# -#providers: -# file: -# directory: ${DYNAMIC_DIR} -# watch: true -#EOF -# -## Enable /ping for HEALTHCHECK on default traefik endpoint (:8080) -#cat >> "$TRAEFIK_CONFIG" << 'EOF' -# -#ping: {} -#EOF -# -## Add logging config -#cat >> "$TRAEFIK_CONFIG" << 'EOF' -# -#log: -# level: ${TRAEFIK_LOG_LEVEL} -# -#accessLog: {} -#EOF - # Generate base config from template envsubst < /templates/traefik.yml > "$TRAEFIK_CONFIG" @@ -96,7 +59,7 @@ api: EOF fi -# Add entrypoint in accordance to HTTPS related variables +# Add entryPoints in accordance to HTTPS related variables cat >> "$TRAEFIK_CONFIG" << 'EOF' entryPoints: main: diff --git a/templates/traefik.yml b/templates/traefik.yml index 0cc8671..c5cb72f 100644 --- a/templates/traefik.yml +++ b/templates/traefik.yml @@ -15,6 +15,6 @@ log: accessLog: {} -# entryPoints are generated dynamically in entrypoint script as their +# entryPoints are generated dynamically in entrypoint.sh script as their # definitions depend on TLS configuration From 2b88339f28ba3f16dea430f0a638f193b95a421d Mon Sep 17 00:00:00 2001 From: Adrian Richter Date: Tue, 2 Dec 2025 12:35:52 +0100 Subject: [PATCH 11/12] Remove leftover WIP templates --- templates/entrypoint_acme.yml | 4 ---- templates/entrypoint_web.yml | 4 ---- 2 files changed, 8 deletions(-) delete mode 100644 templates/entrypoint_acme.yml delete mode 100644 templates/entrypoint_web.yml diff --git a/templates/entrypoint_acme.yml b/templates/entrypoint_acme.yml deleted file mode 100644 index 6e0940c..0000000 --- a/templates/entrypoint_acme.yml +++ /dev/null @@ -1,4 +0,0 @@ - -entryPoints: - amce: - address: ":8001" diff --git a/templates/entrypoint_web.yml b/templates/entrypoint_web.yml deleted file mode 100644 index f3aae6e..0000000 --- a/templates/entrypoint_web.yml +++ /dev/null @@ -1,4 +0,0 @@ - -entryPoints: - web: - address: ":8000" From d47b82afedd59bf74561c953ad0ce7a3bc63281a Mon Sep 17 00:00:00 2001 From: Adrian Richter Date: Mon, 8 Dec 2025 13:37:09 +0100 Subject: [PATCH 12/12] Add missing certResolver field --- Dockerfile | 2 +- README.md | 2 +- entrypoint.sh | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 09828fb..126a006 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ ARG CONTEXT=prod -FROM traefik:3.6.0 as base +FROM traefik:3.6.0 AS base ## Setup ARG CONTEXT diff --git a/README.md b/README.md index fdbe294..ca454c4 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ The proxy service is configured through: - `TRAEFIK_LOG_LEVEL` - Log level (default: INFO, DEBUG in dev image) - `ENABLE_DASHBOARD` - Enable traefik web-based dashboard, also sets `debug: true` for now - `ENABLE_LOCAL_HTTPS` - Enable TLS using certs provided through `HTTPS_*_FILE`. Can be self-signed (used in dev by default) or manually generated/trusted. -- `ENABLE_AUTO_HTTPS` - Enable cert retrieval through ACME. Depends on further variables. +- `ENABLE_AUTO_HTTPS` - Enable cert retrieval through ACME. Depends on further variables. (Overruled by `ENABLE_LOCAL_HTTPS` if both are set) - `EXTERNAL_ADDRESS` - domain for which to retrieve cert - `ACME_ENDPOINT` - when unset will fallback to traefiks default value for `acme.caServer: https://acme-v02.api.letsencrypt.org/directory` - `ACME_EMAIL` - Email Address sent to acme endpoint during cert retrieval diff --git a/entrypoint.sh b/entrypoint.sh index 3f60b69..4e20aa7 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -79,10 +79,10 @@ elif [ -n "$ENABLE_AUTO_HTTPS" ]; then tls: domains: - main: ${EXTERNAL_ADDRESS} + certResolver: acmeResolver EOF # Additionally a plain HTTP endpoint to answer ACME challenges on must be # configured - echo "Configuring traefik to answer acme challenges on port 8001." cat >> "$TRAEFIK_CONFIG" << 'EOF' acme: @@ -105,6 +105,10 @@ EOF caServer: ${ACME_ENDPOINT} EOF fi + + echo "traefik was configured to automatically retrieve a TLS certificates via acme." + echo "Make sure incoming challange requests (to HOST:80/.well-known/acme-challenge/) reach this container on port 8001" + echo "In most cases forwarding the hosts port 80 to containers port 8001 is enough." fi