From 354a579788773974c99be0e4022a1bdaf9b895fa Mon Sep 17 00:00:00 2001 From: George Brownbridge Date: Tue, 6 Jan 2026 12:01:42 +0000 Subject: [PATCH 01/64] add-traefik-support: Added a stack config setting to allow the reverse-proxy to be specified. --- .../stack/clients/core/StackClient.java | 9 +++++++++ .../cmclinnovations/stack/services/ServiceManager.java | 10 ++++++---- .../src/main/java/com/cmclinnovations/stack/Stack.java | 1 + .../java/com/cmclinnovations/stack/StackConfig.java | 8 ++++++++ 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/StackClient.java b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/StackClient.java index 8350f876..2c3d3c95 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/StackClient.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/StackClient.java @@ -36,6 +36,7 @@ public final class StackClient { private static StackHost stackHost = new StackHost(); private static boolean isolated = false; + private static String reverseProxyName; static { String envVarStackName = System.getenv(StackClient.STACK_NAME_KEY); @@ -107,6 +108,14 @@ public static Path getAbsDataPath() { return getStackBaseDir().resolve("inputs").resolve("data"); } + public static void setReverseProxyName(String reverseProxyName) { + StackClient.reverseProxyName = reverseProxyName; + } + + public static String getReverseProxyName() { + return reverseProxyName; + } + /** * Get a RemoteRDBStoreClient for the named Postgres RDB running in this stack. * diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java index 14a92f7e..d87d2dc4 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java @@ -167,15 +167,17 @@ public S initialiseService(String stackName, String serviceN DockerService dockerService = getOrInitialiseService(stackName, StackClient.getContainerEngineName()); dockerService.doPreStartUpConfiguration(newContainerService); + if (!StackClient.getReverseProxyName().equals(serviceName)) { + ReverseProxyService reverseProxyService = getOrInitialiseService(stackName, + StackClient.getReverseProxyName()); + reverseProxyService.addService(newContainerService); + } + dockerService.writeEndpointConfigs(newContainerService); if (dockerService.startContainer(newContainerService)) { dockerService.doFirstTimePostStartUpConfiguration(newContainerService); } dockerService.doEveryTimePostStartUpConfiguration(newContainerService); - if (!NginxService.TYPE.equals(serviceName)) { - ReverseProxyService reverseProxyService = getOrInitialiseService(stackName, NginxService.TYPE); - reverseProxyService.addService(newContainerService); - } } services.put(serviceName, newService); diff --git a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java index 692cdc6e..14136c07 100644 --- a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java +++ b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java @@ -83,6 +83,7 @@ private Stack(String name, ServiceManager manager, StackConfig config) { if (null != config) { StackClient.setStackHost(config.getHost()); StackClient.setIsolated(config.isIsolated()); + StackClient.setReverseProxyName(config.getReverseProxyName()); } } diff --git a/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java b/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java index 95b2d62a..7c4470ec 100644 --- a/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java +++ b/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java @@ -7,6 +7,7 @@ import java.util.Map; import com.cmclinnovations.stack.clients.core.StackHost; +import com.cmclinnovations.stack.services.NginxService; import com.fasterxml.jackson.annotation.JsonProperty; public class StackConfig { @@ -30,6 +31,9 @@ private enum Selector { @JsonProperty private final Boolean isolated = false; + @JsonProperty("reverseProxy") + private final String reverseProxy = NginxService.TYPE; + @JsonProperty("hostName") private void setHostName(String hostName) { host = new StackHost(hostName); @@ -54,4 +58,8 @@ Map getVolumes() { public boolean isIsolated() { return isolated; } + + public String getReverseProxyName() { + return reverseProxy; + } } From 4f645cbf6e7f771c953e9483b89197cd66c3d1fb Mon Sep 17 00:00:00 2001 From: George Brownbridge Date: Tue, 6 Jan 2026 12:03:13 +0000 Subject: [PATCH 02/64] add-traefik-support: Bumped stack version. --- stack-clients/docker-compose.yml | 2 +- stack-clients/pom.xml | 2 +- stack-data-uploader/docker-compose.yml | 2 +- stack-data-uploader/pom.xml | 4 ++-- stack-manager/docker-compose.yml | 2 +- stack-manager/pom.xml | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/stack-clients/docker-compose.yml b/stack-clients/docker-compose.yml index 1596f5b1..5801c070 100644 --- a/stack-clients/docker-compose.yml +++ b/stack-clients/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-client: - image: ghcr.io/theworldavatar/stack-client${IMAGE_SUFFIX}:1.56.2 + image: ghcr.io/theworldavatar/stack-client${IMAGE_SUFFIX}:1.57.0-traefik-support-SNAPSHOT secrets: - blazegraph_password - postgis_password diff --git a/stack-clients/pom.xml b/stack-clients/pom.xml index a76fd8d7..c0b5e052 100644 --- a/stack-clients/pom.xml +++ b/stack-clients/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-clients - 1.56.2 + 1.57.0-traefik-support-SNAPSHOT Stack Clients https://theworldavatar.io diff --git a/stack-data-uploader/docker-compose.yml b/stack-data-uploader/docker-compose.yml index 2492d196..c110fea3 100644 --- a/stack-data-uploader/docker-compose.yml +++ b/stack-data-uploader/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-data-uploader: - image: ghcr.io/theworldavatar/stack-data-uploader${IMAGE_SUFFIX}:1.56.2 + image: ghcr.io/theworldavatar/stack-data-uploader${IMAGE_SUFFIX}:1.57.0-traefik-support-SNAPSHOT secrets: - blazegraph_password - postgis_password diff --git a/stack-data-uploader/pom.xml b/stack-data-uploader/pom.xml index 9a621abf..4f6025ef 100644 --- a/stack-data-uploader/pom.xml +++ b/stack-data-uploader/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-data-uploader - 1.56.2 + 1.57.0-traefik-support-SNAPSHOT Stack Data Uploader https://theworldavatar.io @@ -38,7 +38,7 @@ com.cmclinnovations stack-clients - 1.56.2 + 1.57.0-traefik-support-SNAPSHOT diff --git a/stack-manager/docker-compose.yml b/stack-manager/docker-compose.yml index 83e67e62..f2536823 100644 --- a/stack-manager/docker-compose.yml +++ b/stack-manager/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-manager: - image: ghcr.io/theworldavatar/stack-manager${IMAGE_SUFFIX}:1.56.2 + image: ghcr.io/theworldavatar/stack-manager${IMAGE_SUFFIX}:1.57.0-traefik-support-SNAPSHOT environment: EXTERNAL_PORT: "${EXTERNAL_PORT-3838}" STACK_BASE_DIR: "${STACK_BASE_DIR}" diff --git a/stack-manager/pom.xml b/stack-manager/pom.xml index ab2fc7ba..d8984e8a 100644 --- a/stack-manager/pom.xml +++ b/stack-manager/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-manager - 1.56.2 + 1.57.0-traefik-support-SNAPSHOT Stack Manager https://theworldavatar.io @@ -38,7 +38,7 @@ com.cmclinnovations stack-clients - 1.56.2 + 1.57.0-traefik-support-SNAPSHOT From 1124705c8ddb1aecb75fc62c754f22ee50ea9c0d Mon Sep 17 00:00:00 2001 From: George Brownbridge Date: Tue, 6 Jan 2026 12:07:39 +0000 Subject: [PATCH 03/64] add-traefik-support: Added "empty" `TraefikService` class. --- .../stack/services/TraefikService.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java new file mode 100644 index 00000000..090be337 --- /dev/null +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -0,0 +1,36 @@ +package com.cmclinnovations.stack.services; + +import java.util.HashMap; +import java.util.Map; + +import com.cmclinnovations.stack.clients.core.StackClient; +import com.cmclinnovations.stack.clients.utils.FileUtils; +import com.cmclinnovations.stack.services.config.Connection; +import com.cmclinnovations.stack.services.config.ServiceConfig; + +public class TraefikService extends ContainerService implements ReverseProxyService { + + public TraefikService(String stackName, ServiceConfig config) { + super(stackName, config); + } + + @Override + public void addService(ContainerService service) { + Map labels = service.getContainerSpec().getLabels(); + if (null == labels) { + labels = new HashMap<>(); + service.getContainerSpec().withLabels(labels); + } + labels.put("traefik.enable", "true"); + + service.getConfig().getEndpoints().forEach((name,connection) -> { + // TODO: Set the labels correctly + // labels.put("traefik.http.routers." + service.getContainerName() + ".rule", + // "PathPrefix(`" + FileUtils.fixSlashes(connection.getExternalPath().getPath(), true, false) + "`)"); + // labels.put( + // "traefik.http.services." + service.getServiceName() + ".loadbalancer.server.port", + // String.valueOf(connection.getInternalPort())); + }); + } + +} From 84a1fd5101908c65144ce3f924cbbcb211d9945e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 16 Jan 2026 11:12:55 +0000 Subject: [PATCH 04/64] get debugger working --- stack-manager/.vscode/tasks.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/stack-manager/.vscode/tasks.json b/stack-manager/.vscode/tasks.json index af41b2f3..06455fe2 100644 --- a/stack-manager/.vscode/tasks.json +++ b/stack-manager/.vscode/tasks.json @@ -33,7 +33,10 @@ ], "options": { "shell": { - "executable": "bash" + "executable": "bash", + "args": [ + "-c" + ] } } }, From 52332ce27ebf8665f4441009379e7777f0bf7b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 16 Jan 2026 14:57:27 +0000 Subject: [PATCH 05/64] traefik service config file --- .../stack/services/built-ins/traefik.json | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json diff --git a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json new file mode 100644 index 00000000..ff525b4c --- /dev/null +++ b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json @@ -0,0 +1,46 @@ +{ + "type": "traefik", + "ServiceSpec": { + "Name": "traefik", + "TaskTemplate": { + "ContainerSpec": { + "Image": "traefik:v3.6", + "Mounts": [ + { + "Type": "bind", + "Source": "/var/run/docker.sock", + "Target": "/var/run/docker.sock", + "ReadOnly": true + }, + { + "Type": "volume", + "Source": "traefik_config", + "Target": "/etc/traefik" + } + ] + } + }, + "EndpointSpec": { + "Ports": [ + { + "Name": "web", + "Protocol": "tcp", + "TargetPort": "80", + "PublishedPort": "3838" + }, + { + "Name": "websecure", + "Protocol": "tcp", + "TargetPort": "443", + "PublishedPort": "443" + }, + { + "Name": "dashboard", + "Protocol": "tcp", + "TargetPort": "8080", + "PublishedPort": "8080" + } + ] + } + } +} \ No newline at end of file From 461b7c684f5520a262d558fd660cd1ba2bbb3da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 16 Jan 2026 14:57:47 +0000 Subject: [PATCH 06/64] addreverseproxy method in stack.java --- .../main/java/com/cmclinnovations/stack/Stack.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java index 14136c07..159b769d 100644 --- a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java +++ b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java @@ -135,6 +135,9 @@ private List calculateSelectedServicesFromConfig(List defaultSer + ", explicitly included by user. Please remove them from the \"includes\" list in the stack config file."); } + // Add the reverse proxy service specified in the config + addReverseProxyIfAbsent(selectedServices); + // Add user specified services selectedServices.addAll(config.getIncludedServices()); // Remove any excluded services (default and user specified) @@ -142,6 +145,15 @@ private List calculateSelectedServicesFromConfig(List defaultSer return selectedServices; } + private void addReverseProxyIfAbsent(List selectedServices) { + String reverseProxyService = config.getReverseProxyName(); + if (reverseProxyService != null && !reverseProxyService.isEmpty()) { + if (!selectedServices.contains(reverseProxyService)) { + selectedServices.add(reverseProxyService); + } + } + } + private void handleUserSuppliedData(List selectedServices) { Map volumes = config.getVolumes(); From 88aca33e37413b9120be16319ae4a3860b53a666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 20 Jan 2026 11:44:24 +0000 Subject: [PATCH 07/64] remove snashot --- stack-clients/docker-compose.yml | 2 +- stack-clients/pom.xml | 4 ++-- stack-data-uploader/docker-compose.yml | 2 +- stack-data-uploader/pom.xml | 4 ++-- stack-manager/docker-compose.yml | 2 +- stack-manager/pom.xml | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/stack-clients/docker-compose.yml b/stack-clients/docker-compose.yml index 1596f5b1..42751b47 100644 --- a/stack-clients/docker-compose.yml +++ b/stack-clients/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-client: - image: ghcr.io/theworldavatar/stack-client${IMAGE_SUFFIX}:1.56.2 + image: ghcr.io/theworldavatar/stack-client${IMAGE_SUFFIX}:1.56.3-update-parent-SNAPSHOT secrets: - blazegraph_password - postgis_password diff --git a/stack-clients/pom.xml b/stack-clients/pom.xml index a76fd8d7..8bd6ce18 100644 --- a/stack-clients/pom.xml +++ b/stack-clients/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-clients - 1.56.2 + 1.56.3-update-parent-SNAPSHOT Stack Clients https://theworldavatar.io @@ -15,7 +15,7 @@ uk.ac.cam.cares.jps jps-parent-pom - 2.3.2 + 2.4.0 diff --git a/stack-data-uploader/docker-compose.yml b/stack-data-uploader/docker-compose.yml index 2492d196..2ab1be15 100644 --- a/stack-data-uploader/docker-compose.yml +++ b/stack-data-uploader/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-data-uploader: - image: ghcr.io/theworldavatar/stack-data-uploader${IMAGE_SUFFIX}:1.56.2 + image: ghcr.io/theworldavatar/stack-data-uploader${IMAGE_SUFFIX}:1.56.3-update-parent-SNAPSHOT secrets: - blazegraph_password - postgis_password diff --git a/stack-data-uploader/pom.xml b/stack-data-uploader/pom.xml index 9a621abf..e22659f2 100644 --- a/stack-data-uploader/pom.xml +++ b/stack-data-uploader/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-data-uploader - 1.56.2 + 1.56.3-update-parent-SNAPSHOT Stack Data Uploader https://theworldavatar.io @@ -38,7 +38,7 @@ com.cmclinnovations stack-clients - 1.56.2 + 1.56.3-update-parent-SNAPSHOT diff --git a/stack-manager/docker-compose.yml b/stack-manager/docker-compose.yml index 83e67e62..609e3b0a 100644 --- a/stack-manager/docker-compose.yml +++ b/stack-manager/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-manager: - image: ghcr.io/theworldavatar/stack-manager${IMAGE_SUFFIX}:1.56.2 + image: ghcr.io/theworldavatar/stack-manager${IMAGE_SUFFIX}:1.56.3-update-parent-SNAPSHOT environment: EXTERNAL_PORT: "${EXTERNAL_PORT-3838}" STACK_BASE_DIR: "${STACK_BASE_DIR}" diff --git a/stack-manager/pom.xml b/stack-manager/pom.xml index ab2fc7ba..0ed7134d 100644 --- a/stack-manager/pom.xml +++ b/stack-manager/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-manager - 1.56.2 + 1.56.3-update-parent-SNAPSHOT Stack Manager https://theworldavatar.io @@ -38,7 +38,7 @@ com.cmclinnovations stack-clients - 1.56.2 + 1.56.3-update-parent-SNAPSHOT From 711978e7841690b0bbe3058cc81e4308bb9542de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 20 Jan 2026 12:19:09 +0000 Subject: [PATCH 08/64] fix version --- stack-clients/docker-compose.yml | 2 +- stack-clients/pom.xml | 2 +- stack-data-uploader/docker-compose.yml | 2 +- stack-data-uploader/pom.xml | 4 ++-- stack-manager/docker-compose.yml | 2 +- stack-manager/pom.xml | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/stack-clients/docker-compose.yml b/stack-clients/docker-compose.yml index 42751b47..2f32c919 100644 --- a/stack-clients/docker-compose.yml +++ b/stack-clients/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-client: - image: ghcr.io/theworldavatar/stack-client${IMAGE_SUFFIX}:1.56.3-update-parent-SNAPSHOT + image: ghcr.io/theworldavatar/stack-client${IMAGE_SUFFIX}:1.56.3 secrets: - blazegraph_password - postgis_password diff --git a/stack-clients/pom.xml b/stack-clients/pom.xml index 8bd6ce18..4d14832d 100644 --- a/stack-clients/pom.xml +++ b/stack-clients/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-clients - 1.56.3-update-parent-SNAPSHOT + 1.56.3 Stack Clients https://theworldavatar.io diff --git a/stack-data-uploader/docker-compose.yml b/stack-data-uploader/docker-compose.yml index 2ab1be15..c08d56a0 100644 --- a/stack-data-uploader/docker-compose.yml +++ b/stack-data-uploader/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-data-uploader: - image: ghcr.io/theworldavatar/stack-data-uploader${IMAGE_SUFFIX}:1.56.3-update-parent-SNAPSHOT + image: ghcr.io/theworldavatar/stack-data-uploader${IMAGE_SUFFIX}:1.56.3 secrets: - blazegraph_password - postgis_password diff --git a/stack-data-uploader/pom.xml b/stack-data-uploader/pom.xml index e22659f2..04986269 100644 --- a/stack-data-uploader/pom.xml +++ b/stack-data-uploader/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-data-uploader - 1.56.3-update-parent-SNAPSHOT + 1.56.3 Stack Data Uploader https://theworldavatar.io @@ -38,7 +38,7 @@ com.cmclinnovations stack-clients - 1.56.3-update-parent-SNAPSHOT + 1.56.3 diff --git a/stack-manager/docker-compose.yml b/stack-manager/docker-compose.yml index 609e3b0a..95afaba4 100644 --- a/stack-manager/docker-compose.yml +++ b/stack-manager/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-manager: - image: ghcr.io/theworldavatar/stack-manager${IMAGE_SUFFIX}:1.56.3-update-parent-SNAPSHOT + image: ghcr.io/theworldavatar/stack-manager${IMAGE_SUFFIX}:1.56.3 environment: EXTERNAL_PORT: "${EXTERNAL_PORT-3838}" STACK_BASE_DIR: "${STACK_BASE_DIR}" diff --git a/stack-manager/pom.xml b/stack-manager/pom.xml index 0ed7134d..bd991a9b 100644 --- a/stack-manager/pom.xml +++ b/stack-manager/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-manager - 1.56.3-update-parent-SNAPSHOT + 1.56.3 Stack Manager https://theworldavatar.io @@ -38,7 +38,7 @@ com.cmclinnovations stack-clients - 1.56.3-update-parent-SNAPSHOT + 1.56.3 From 53e725ca0e2307fa8b97ae0441c3df8da9d91e39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 20 Jan 2026 14:02:11 +0000 Subject: [PATCH 09/64] rm redundant import --- .../com/cmclinnovations/stack/clients/core/datasets/RML.java | 1 - 1 file changed, 1 deletion(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/datasets/RML.java b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/datasets/RML.java index a9ca70f6..cb6deb73 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/datasets/RML.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/datasets/RML.java @@ -1,7 +1,6 @@ package com.cmclinnovations.stack.clients.core.datasets; import java.nio.file.Path; -import java.util.Map; import com.cmclinnovations.stack.clients.rml.RmlMapperClient; From 7667dd97fae47f907d9cb683e14cae2f4c28fac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 20 Jan 2026 14:07:25 +0000 Subject: [PATCH 10/64] replace deprecated docker api call --- .../stack/clients/docker/DockerClient.java | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/DockerClient.java b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/DockerClient.java index 85391b99..a8b5cde4 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/DockerClient.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/DockerClient.java @@ -53,7 +53,9 @@ import com.github.dockerjava.core.DefaultDockerClientConfig.Builder; import com.github.dockerjava.core.DockerClientBuilder; import com.github.dockerjava.core.DockerClientConfig; -import com.github.dockerjava.core.command.ExecStartResultCallback; +import com.github.dockerjava.api.async.ResultCallback; +import com.github.dockerjava.api.model.Frame; +import com.github.dockerjava.api.model.StreamType; import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; import com.github.dockerjava.transport.DockerHttpClient; @@ -115,7 +117,6 @@ public String executeSimpleCommand(String containerId, String... cmd) { .withOutputStream(outputStream) .withErrorStream(outputStream) .exec(); - String output = outputStream.toString(); return execId; } @@ -231,11 +232,21 @@ public String exec() { execStartCmd.withStdIn(inputStream); } - // ExecStartResultCallback is marked deprecated but seems to do exactly what we - // want and without knowing why it is deprecated any issues with it can't be - // overcome anyway. - try (ExecStartResultCallback result = execStartCmd - .exec(new ExecStartResultCallback(outputStream, errorStream))) { + try (ResultCallback.Adapter result = execStartCmd + .exec(new ResultCallback.Adapter() { + @Override + public void onNext(Frame frame) { + try { + if (frame.getStreamType() == StreamType.STDOUT && outputStream != null) { + outputStream.write(frame.getPayload()); + } else if (frame.getStreamType() == StreamType.STDERR && errorStream != null) { + errorStream.write(frame.getPayload()); + } + } catch (IOException ex) { + throw new RuntimeException("Failed to write frame payload", ex); + } + } + })) { if (wait) { if (!result.awaitCompletion(evaluationTimeout, TimeUnit.SECONDS)) { LOGGER.warn("Docker exec command '{}' still running after the {} second execution timeout.", @@ -553,7 +564,7 @@ public boolean isContainerUp(String containerName) { public String getContainerId(String containerName) { return getContainer(containerName).map(Container::getId) - .orElseThrow(() -> new NoSuchElementException("Cannot get container "+containerName+".")); + .orElseThrow(() -> new NoSuchElementException("Cannot get container " + containerName + ".")); } private Map> convertToConfigFilterMap(String configName, Map labelMap) { From dc8dee177ac240dbad1b5838b1eabeb64fcd1a00 Mon Sep 17 00:00:00 2001 From: George Brownbridge Date: Tue, 6 Jan 2026 12:01:42 +0000 Subject: [PATCH 11/64] add-traefik-support: Added a stack config setting to allow the reverse-proxy to be specified. --- .../stack/clients/core/StackClient.java | 9 +++++++++ .../cmclinnovations/stack/services/ServiceManager.java | 10 ++++++---- .../src/main/java/com/cmclinnovations/stack/Stack.java | 1 + .../java/com/cmclinnovations/stack/StackConfig.java | 8 ++++++++ 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/StackClient.java b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/StackClient.java index 8350f876..2c3d3c95 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/StackClient.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/StackClient.java @@ -36,6 +36,7 @@ public final class StackClient { private static StackHost stackHost = new StackHost(); private static boolean isolated = false; + private static String reverseProxyName; static { String envVarStackName = System.getenv(StackClient.STACK_NAME_KEY); @@ -107,6 +108,14 @@ public static Path getAbsDataPath() { return getStackBaseDir().resolve("inputs").resolve("data"); } + public static void setReverseProxyName(String reverseProxyName) { + StackClient.reverseProxyName = reverseProxyName; + } + + public static String getReverseProxyName() { + return reverseProxyName; + } + /** * Get a RemoteRDBStoreClient for the named Postgres RDB running in this stack. * diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java index 14a92f7e..d87d2dc4 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java @@ -167,15 +167,17 @@ public S initialiseService(String stackName, String serviceN DockerService dockerService = getOrInitialiseService(stackName, StackClient.getContainerEngineName()); dockerService.doPreStartUpConfiguration(newContainerService); + if (!StackClient.getReverseProxyName().equals(serviceName)) { + ReverseProxyService reverseProxyService = getOrInitialiseService(stackName, + StackClient.getReverseProxyName()); + reverseProxyService.addService(newContainerService); + } + dockerService.writeEndpointConfigs(newContainerService); if (dockerService.startContainer(newContainerService)) { dockerService.doFirstTimePostStartUpConfiguration(newContainerService); } dockerService.doEveryTimePostStartUpConfiguration(newContainerService); - if (!NginxService.TYPE.equals(serviceName)) { - ReverseProxyService reverseProxyService = getOrInitialiseService(stackName, NginxService.TYPE); - reverseProxyService.addService(newContainerService); - } } services.put(serviceName, newService); diff --git a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java index 692cdc6e..14136c07 100644 --- a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java +++ b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java @@ -83,6 +83,7 @@ private Stack(String name, ServiceManager manager, StackConfig config) { if (null != config) { StackClient.setStackHost(config.getHost()); StackClient.setIsolated(config.isIsolated()); + StackClient.setReverseProxyName(config.getReverseProxyName()); } } diff --git a/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java b/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java index 95b2d62a..7c4470ec 100644 --- a/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java +++ b/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java @@ -7,6 +7,7 @@ import java.util.Map; import com.cmclinnovations.stack.clients.core.StackHost; +import com.cmclinnovations.stack.services.NginxService; import com.fasterxml.jackson.annotation.JsonProperty; public class StackConfig { @@ -30,6 +31,9 @@ private enum Selector { @JsonProperty private final Boolean isolated = false; + @JsonProperty("reverseProxy") + private final String reverseProxy = NginxService.TYPE; + @JsonProperty("hostName") private void setHostName(String hostName) { host = new StackHost(hostName); @@ -54,4 +58,8 @@ Map getVolumes() { public boolean isIsolated() { return isolated; } + + public String getReverseProxyName() { + return reverseProxy; + } } From d04d90d5fb3cdb19dc4407a255faf89cd620dfce Mon Sep 17 00:00:00 2001 From: George Brownbridge Date: Tue, 6 Jan 2026 12:03:13 +0000 Subject: [PATCH 12/64] add-traefik-support: Bumped stack version. --- stack-clients/docker-compose.yml | 2 +- stack-clients/pom.xml | 2 +- stack-data-uploader/docker-compose.yml | 2 +- stack-data-uploader/pom.xml | 4 ++-- stack-manager/docker-compose.yml | 2 +- stack-manager/pom.xml | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/stack-clients/docker-compose.yml b/stack-clients/docker-compose.yml index 2f32c919..5801c070 100644 --- a/stack-clients/docker-compose.yml +++ b/stack-clients/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-client: - image: ghcr.io/theworldavatar/stack-client${IMAGE_SUFFIX}:1.56.3 + image: ghcr.io/theworldavatar/stack-client${IMAGE_SUFFIX}:1.57.0-traefik-support-SNAPSHOT secrets: - blazegraph_password - postgis_password diff --git a/stack-clients/pom.xml b/stack-clients/pom.xml index 4d14832d..b1d14acb 100644 --- a/stack-clients/pom.xml +++ b/stack-clients/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-clients - 1.56.3 + 1.57.0-traefik-support-SNAPSHOT Stack Clients https://theworldavatar.io diff --git a/stack-data-uploader/docker-compose.yml b/stack-data-uploader/docker-compose.yml index c08d56a0..c110fea3 100644 --- a/stack-data-uploader/docker-compose.yml +++ b/stack-data-uploader/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-data-uploader: - image: ghcr.io/theworldavatar/stack-data-uploader${IMAGE_SUFFIX}:1.56.3 + image: ghcr.io/theworldavatar/stack-data-uploader${IMAGE_SUFFIX}:1.57.0-traefik-support-SNAPSHOT secrets: - blazegraph_password - postgis_password diff --git a/stack-data-uploader/pom.xml b/stack-data-uploader/pom.xml index 04986269..4f6025ef 100644 --- a/stack-data-uploader/pom.xml +++ b/stack-data-uploader/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-data-uploader - 1.56.3 + 1.57.0-traefik-support-SNAPSHOT Stack Data Uploader https://theworldavatar.io @@ -38,7 +38,7 @@ com.cmclinnovations stack-clients - 1.56.3 + 1.57.0-traefik-support-SNAPSHOT diff --git a/stack-manager/docker-compose.yml b/stack-manager/docker-compose.yml index 95afaba4..f2536823 100644 --- a/stack-manager/docker-compose.yml +++ b/stack-manager/docker-compose.yml @@ -1,6 +1,6 @@ services: stack-manager: - image: ghcr.io/theworldavatar/stack-manager${IMAGE_SUFFIX}:1.56.3 + image: ghcr.io/theworldavatar/stack-manager${IMAGE_SUFFIX}:1.57.0-traefik-support-SNAPSHOT environment: EXTERNAL_PORT: "${EXTERNAL_PORT-3838}" STACK_BASE_DIR: "${STACK_BASE_DIR}" diff --git a/stack-manager/pom.xml b/stack-manager/pom.xml index bd991a9b..d8984e8a 100644 --- a/stack-manager/pom.xml +++ b/stack-manager/pom.xml @@ -7,7 +7,7 @@ com.cmclinnovations stack-manager - 1.56.3 + 1.57.0-traefik-support-SNAPSHOT Stack Manager https://theworldavatar.io @@ -38,7 +38,7 @@ com.cmclinnovations stack-clients - 1.56.3 + 1.57.0-traefik-support-SNAPSHOT From 70871f6b95e079b07d92f97b6289d02393334050 Mon Sep 17 00:00:00 2001 From: George Brownbridge Date: Tue, 6 Jan 2026 12:07:39 +0000 Subject: [PATCH 13/64] add-traefik-support: Added "empty" `TraefikService` class. --- .../stack/services/TraefikService.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java new file mode 100644 index 00000000..090be337 --- /dev/null +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -0,0 +1,36 @@ +package com.cmclinnovations.stack.services; + +import java.util.HashMap; +import java.util.Map; + +import com.cmclinnovations.stack.clients.core.StackClient; +import com.cmclinnovations.stack.clients.utils.FileUtils; +import com.cmclinnovations.stack.services.config.Connection; +import com.cmclinnovations.stack.services.config.ServiceConfig; + +public class TraefikService extends ContainerService implements ReverseProxyService { + + public TraefikService(String stackName, ServiceConfig config) { + super(stackName, config); + } + + @Override + public void addService(ContainerService service) { + Map labels = service.getContainerSpec().getLabels(); + if (null == labels) { + labels = new HashMap<>(); + service.getContainerSpec().withLabels(labels); + } + labels.put("traefik.enable", "true"); + + service.getConfig().getEndpoints().forEach((name,connection) -> { + // TODO: Set the labels correctly + // labels.put("traefik.http.routers." + service.getContainerName() + ".rule", + // "PathPrefix(`" + FileUtils.fixSlashes(connection.getExternalPath().getPath(), true, false) + "`)"); + // labels.put( + // "traefik.http.services." + service.getServiceName() + ".loadbalancer.server.port", + // String.valueOf(connection.getInternalPort())); + }); + } + +} From e882d97a1086b96c4acf92ed611c5bdf19d261d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 16 Jan 2026 11:12:55 +0000 Subject: [PATCH 14/64] get debugger working --- stack-manager/.vscode/tasks.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/stack-manager/.vscode/tasks.json b/stack-manager/.vscode/tasks.json index af41b2f3..06455fe2 100644 --- a/stack-manager/.vscode/tasks.json +++ b/stack-manager/.vscode/tasks.json @@ -33,7 +33,10 @@ ], "options": { "shell": { - "executable": "bash" + "executable": "bash", + "args": [ + "-c" + ] } } }, From 14bb232e7b3fc5eb782ba26ec75a20594f8bcc4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 16 Jan 2026 14:57:27 +0000 Subject: [PATCH 15/64] traefik service config file --- .../stack/services/built-ins/traefik.json | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json diff --git a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json new file mode 100644 index 00000000..ff525b4c --- /dev/null +++ b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json @@ -0,0 +1,46 @@ +{ + "type": "traefik", + "ServiceSpec": { + "Name": "traefik", + "TaskTemplate": { + "ContainerSpec": { + "Image": "traefik:v3.6", + "Mounts": [ + { + "Type": "bind", + "Source": "/var/run/docker.sock", + "Target": "/var/run/docker.sock", + "ReadOnly": true + }, + { + "Type": "volume", + "Source": "traefik_config", + "Target": "/etc/traefik" + } + ] + } + }, + "EndpointSpec": { + "Ports": [ + { + "Name": "web", + "Protocol": "tcp", + "TargetPort": "80", + "PublishedPort": "3838" + }, + { + "Name": "websecure", + "Protocol": "tcp", + "TargetPort": "443", + "PublishedPort": "443" + }, + { + "Name": "dashboard", + "Protocol": "tcp", + "TargetPort": "8080", + "PublishedPort": "8080" + } + ] + } + } +} \ No newline at end of file From f0d7239908bfc7744f220440a5fbc2ad8faf7914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 16 Jan 2026 14:57:47 +0000 Subject: [PATCH 16/64] addreverseproxy method in stack.java --- .../main/java/com/cmclinnovations/stack/Stack.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java index 14136c07..159b769d 100644 --- a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java +++ b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java @@ -135,6 +135,9 @@ private List calculateSelectedServicesFromConfig(List defaultSer + ", explicitly included by user. Please remove them from the \"includes\" list in the stack config file."); } + // Add the reverse proxy service specified in the config + addReverseProxyIfAbsent(selectedServices); + // Add user specified services selectedServices.addAll(config.getIncludedServices()); // Remove any excluded services (default and user specified) @@ -142,6 +145,15 @@ private List calculateSelectedServicesFromConfig(List defaultSer return selectedServices; } + private void addReverseProxyIfAbsent(List selectedServices) { + String reverseProxyService = config.getReverseProxyName(); + if (reverseProxyService != null && !reverseProxyService.isEmpty()) { + if (!selectedServices.contains(reverseProxyService)) { + selectedServices.add(reverseProxyService); + } + } + } + private void handleUserSuppliedData(List selectedServices) { Map volumes = config.getVolumes(); From 8bc930532b44569921cdf280a36c64db9690c909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 20 Jan 2026 14:02:11 +0000 Subject: [PATCH 17/64] rm redundant import --- .../com/cmclinnovations/stack/clients/core/datasets/RML.java | 1 - 1 file changed, 1 deletion(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/datasets/RML.java b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/datasets/RML.java index a9ca70f6..cb6deb73 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/datasets/RML.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/core/datasets/RML.java @@ -1,7 +1,6 @@ package com.cmclinnovations.stack.clients.core.datasets; import java.nio.file.Path; -import java.util.Map; import com.cmclinnovations.stack.clients.rml.RmlMapperClient; From e81af7a1eb429e1193d04147d78fc9ef97cb07b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 20 Jan 2026 14:07:25 +0000 Subject: [PATCH 18/64] replace deprecated docker api call --- .../stack/clients/docker/DockerClient.java | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/DockerClient.java b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/DockerClient.java index 85391b99..a8b5cde4 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/DockerClient.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/DockerClient.java @@ -53,7 +53,9 @@ import com.github.dockerjava.core.DefaultDockerClientConfig.Builder; import com.github.dockerjava.core.DockerClientBuilder; import com.github.dockerjava.core.DockerClientConfig; -import com.github.dockerjava.core.command.ExecStartResultCallback; +import com.github.dockerjava.api.async.ResultCallback; +import com.github.dockerjava.api.model.Frame; +import com.github.dockerjava.api.model.StreamType; import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; import com.github.dockerjava.transport.DockerHttpClient; @@ -115,7 +117,6 @@ public String executeSimpleCommand(String containerId, String... cmd) { .withOutputStream(outputStream) .withErrorStream(outputStream) .exec(); - String output = outputStream.toString(); return execId; } @@ -231,11 +232,21 @@ public String exec() { execStartCmd.withStdIn(inputStream); } - // ExecStartResultCallback is marked deprecated but seems to do exactly what we - // want and without knowing why it is deprecated any issues with it can't be - // overcome anyway. - try (ExecStartResultCallback result = execStartCmd - .exec(new ExecStartResultCallback(outputStream, errorStream))) { + try (ResultCallback.Adapter result = execStartCmd + .exec(new ResultCallback.Adapter() { + @Override + public void onNext(Frame frame) { + try { + if (frame.getStreamType() == StreamType.STDOUT && outputStream != null) { + outputStream.write(frame.getPayload()); + } else if (frame.getStreamType() == StreamType.STDERR && errorStream != null) { + errorStream.write(frame.getPayload()); + } + } catch (IOException ex) { + throw new RuntimeException("Failed to write frame payload", ex); + } + } + })) { if (wait) { if (!result.awaitCompletion(evaluationTimeout, TimeUnit.SECONDS)) { LOGGER.warn("Docker exec command '{}' still running after the {} second execution timeout.", @@ -553,7 +564,7 @@ public boolean isContainerUp(String containerName) { public String getContainerId(String containerName) { return getContainer(containerName).map(Container::getId) - .orElseThrow(() -> new NoSuchElementException("Cannot get container "+containerName+".")); + .orElseThrow(() -> new NoSuchElementException("Cannot get container " + containerName + ".")); } private Map> convertToConfigFilterMap(String configName, Map labelMap) { From 26f6c7dbfb21b2b930319beab7b5d2fa55ed52a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 21 Jan 2026 11:32:31 +0000 Subject: [PATCH 19/64] rm duplicate docker socket mount for traefik --- .../cmclinnovations/stack/services/built-ins/traefik.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json index ff525b4c..31e9f55d 100644 --- a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json +++ b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/built-ins/traefik.json @@ -6,12 +6,6 @@ "ContainerSpec": { "Image": "traefik:v3.6", "Mounts": [ - { - "Type": "bind", - "Source": "/var/run/docker.sock", - "Target": "/var/run/docker.sock", - "ReadOnly": true - }, { "Type": "volume", "Source": "traefik_config", From 5784de33c26bb68fef45be285fe32fbae7cdbe99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 21 Jan 2026 11:32:50 +0000 Subject: [PATCH 20/64] give traefik service clasee a type field --- .../stack/services/TraefikService.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java index 090be337..d8218f69 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -3,13 +3,12 @@ import java.util.HashMap; import java.util.Map; -import com.cmclinnovations.stack.clients.core.StackClient; -import com.cmclinnovations.stack.clients.utils.FileUtils; -import com.cmclinnovations.stack.services.config.Connection; import com.cmclinnovations.stack.services.config.ServiceConfig; public class TraefikService extends ContainerService implements ReverseProxyService { + public static final String TYPE = "traefik"; + public TraefikService(String stackName, ServiceConfig config) { super(stackName, config); } @@ -23,13 +22,15 @@ public void addService(ContainerService service) { } labels.put("traefik.enable", "true"); - service.getConfig().getEndpoints().forEach((name,connection) -> { + service.getConfig().getEndpoints().forEach((name, connection) -> { // TODO: Set the labels correctly // labels.put("traefik.http.routers." + service.getContainerName() + ".rule", - // "PathPrefix(`" + FileUtils.fixSlashes(connection.getExternalPath().getPath(), true, false) + "`)"); + // "PathPrefix(`" + FileUtils.fixSlashes(connection.getExternalPath().getPath(), + // true, false) + "`)"); // labels.put( - // "traefik.http.services." + service.getServiceName() + ".loadbalancer.server.port", - // String.valueOf(connection.getInternalPort())); + // "traefik.http.services." + service.getServiceName() + + // ".loadbalancer.server.port", + // String.valueOf(connection.getInternalPort())); }); } From 1b4617e32bf7b3b5f7e0bf5106bc74f7ca72d184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 21 Jan 2026 11:39:34 +0000 Subject: [PATCH 21/64] rm bad traefik service starter method --- .../main/java/com/cmclinnovations/stack/Stack.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java index 159b769d..14136c07 100644 --- a/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java +++ b/stack-manager/src/main/java/com/cmclinnovations/stack/Stack.java @@ -135,9 +135,6 @@ private List calculateSelectedServicesFromConfig(List defaultSer + ", explicitly included by user. Please remove them from the \"includes\" list in the stack config file."); } - // Add the reverse proxy service specified in the config - addReverseProxyIfAbsent(selectedServices); - // Add user specified services selectedServices.addAll(config.getIncludedServices()); // Remove any excluded services (default and user specified) @@ -145,15 +142,6 @@ private List calculateSelectedServicesFromConfig(List defaultSer return selectedServices; } - private void addReverseProxyIfAbsent(List selectedServices) { - String reverseProxyService = config.getReverseProxyName(); - if (reverseProxyService != null && !reverseProxyService.isEmpty()) { - if (!selectedServices.contains(reverseProxyService)) { - selectedServices.add(reverseProxyService); - } - } - } - private void handleUserSuppliedData(List selectedServices) { Map volumes = config.getVolumes(); From 6b50203d2002c5909156f220815843b6da6f596c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 21 Jan 2026 11:40:09 +0000 Subject: [PATCH 22/64] fix the reverseProxy string field in StackConfig --- .../src/main/java/com/cmclinnovations/stack/StackConfig.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java b/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java index 7c4470ec..fc97f92a 100644 --- a/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java +++ b/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import com.cmclinnovations.stack.clients.core.StackHost; import com.cmclinnovations.stack.services.NginxService; @@ -32,7 +33,7 @@ private enum Selector { private final Boolean isolated = false; @JsonProperty("reverseProxy") - private final String reverseProxy = NginxService.TYPE; + private final Optional reverseProxy = Optional.empty(); @JsonProperty("hostName") private void setHostName(String hostName) { @@ -60,6 +61,6 @@ public boolean isIsolated() { } public String getReverseProxyName() { - return reverseProxy; + return reverseProxy.orElse(NginxService.TYPE); } } From 55d4d6b4e558cba6964fa93fdd591365957b392e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 21 Jan 2026 11:42:45 +0000 Subject: [PATCH 23/64] rm an unused import --- .../com/cmclinnovations/stack/clients/rdf4j/Rdf4jClient.java | 1 - 1 file changed, 1 deletion(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/rdf4j/Rdf4jClient.java b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/rdf4j/Rdf4jClient.java index d75c0528..6dd94585 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/rdf4j/Rdf4jClient.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/rdf4j/Rdf4jClient.java @@ -4,7 +4,6 @@ import org.eclipse.rdf4j.federated.repository.FedXRepositoryConfigBuilder; import org.eclipse.rdf4j.repository.config.RepositoryConfig; -import org.eclipse.rdf4j.repository.config.RepositoryImplConfig; import org.eclipse.rdf4j.repository.manager.RemoteRepositoryManager; import org.eclipse.rdf4j.repository.sail.config.SailRepositoryConfig; import org.eclipse.rdf4j.repository.sparql.config.SPARQLRepositoryConfig; From 702924e768283d9878387aa2851ed4c7217e03c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 21 Jan 2026 16:13:40 +0000 Subject: [PATCH 24/64] organise imports --- .../stack/clients/docker/PodmanClient.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/PodmanClient.java b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/PodmanClient.java index 3ee82684..c0a87e01 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/PodmanClient.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/clients/docker/PodmanClient.java @@ -7,15 +7,16 @@ import com.cmclinnovations.stack.clients.core.StackClient; import com.cmclinnovations.stack.clients.utils.JsonHelper; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.dockerjava.api.model.Config; +import com.github.dockerjava.jaxrs.ApiClientExtension; + import io.theworldavatar.swagger.podman.ApiClient; import io.theworldavatar.swagger.podman.ApiException; import io.theworldavatar.swagger.podman.api.NetworksApi; import io.theworldavatar.swagger.podman.api.SecretsApi; import io.theworldavatar.swagger.podman.model.Network; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.dockerjava.api.model.Config; -import com.github.dockerjava.jaxrs.ApiClientExtension; public class PodmanClient extends DockerClient { From fbaa083cef5efbe81aa6b359511e0905c6ab6a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 21 Jan 2026 16:13:57 +0000 Subject: [PATCH 25/64] add a traefik config file --- .../services/traefik/configs/traefik.yml | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml diff --git a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml new file mode 100644 index 00000000..78e77013 --- /dev/null +++ b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml @@ -0,0 +1,21 @@ +api: + dashboard: true + insecure: true + +entryPoints: + web: + address: ":80" + websecure: + address: ":443" + traefik: + address: ":8080" + +providers: + docker: + endpoint: "unix:///var/run/docker.sock" + exposedByDefault: false + swarmMode: true + network: "${STACK_NAME}" + +log: + level: INFO From 9224d02fb6fbc8b6f79afcbf2a89f8aecf0acc6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 21 Jan 2026 16:15:30 +0000 Subject: [PATCH 26/64] implement traefik config method --- .../stack/services/TraefikService.java | 64 +++++++++++++++++-- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java index d8218f69..02c91221 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -1,28 +1,80 @@ package com.cmclinnovations.stack.services; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; +import com.cmclinnovations.stack.clients.core.StackClient; +import com.cmclinnovations.stack.clients.utils.FileUtils; import com.cmclinnovations.stack.services.config.ServiceConfig; +import com.github.dockerjava.api.model.ContainerSpec; public class TraefikService extends ContainerService implements ReverseProxyService { public static final String TYPE = "traefik"; + private static final String TRAEFIK_CONF_DIR = "/etc/traefik/"; + private static final String TRAEFIK_CONFIG_TEMPLATE = "traefik/configs/traefik.yml"; + public TraefikService(String stackName, ServiceConfig config) { super(stackName, config); } @Override - public void addService(ContainerService service) { - Map labels = service.getContainerSpec().getLabels(); - if (null == labels) { - labels = new HashMap<>(); - service.getContainerSpec().withLabels(labels); + public void doFirstTimePostStartUpConfiguration() { + try (InputStream inStream = new BufferedInputStream( + TraefikService.class.getResourceAsStream(TRAEFIK_CONFIG_TEMPLATE))) { + + String configContent = new String(inStream.readAllBytes(), StandardCharsets.UTF_8); + // Replace the ${STACK_NAME} placeholder with actual stack name + String stackName = getEnvironmentVariable(StackClient.STACK_NAME_KEY); + configContent = configContent.replace("${STACK_NAME}", stackName); + + // Write the configuration file to the container + Map files = new HashMap<>(); + files.put("traefik.yml", configContent.getBytes(StandardCharsets.UTF_8)); + sendFilesContent(files, TRAEFIK_CONF_DIR); + + } catch (IOException ex) { + throw new RuntimeException("Failed to configure Traefik", ex); } + } + + @Override + public void addService(ContainerService service) { + ContainerSpec containerSpec = service.getContainerSpec(); + Map labels = new HashMap<>(); + containerSpec.withLabels(labels); labels.put("traefik.enable", "true"); - service.getConfig().getEndpoints().forEach((name, connection) -> { + service.getConfig().getEndpoints().forEach((endpointName, connection) -> { + URI externalPath = connection.getExternalPath(); + if (null != externalPath) { + String serviceName = service.getName(); + String routerName = serviceName + "_" + endpointName; + String pathPrefix = FileUtils.fixSlashes(externalPath.getPath(), true, false); + + // Configure router with path prefix rule + labels.put("traefik.http.routers." + routerName + ".rule", + "PathPrefix(`" + pathPrefix + "`)"); + labels.put("traefik.http.routers." + routerName + ".entrypoints", "web"); + + // Configure service with the internal port + URL url = connection.getUrl(); + int port = url.getPort(); + if (port == -1) { + port = 80; // Default port + } + labels.put("traefik.http.routers." + routerName + ".service", routerName); + labels.put("traefik.http.services." + routerName + ".loadbalancer.server.port", + String.valueOf(port)); + } + // TODO: Set the labels correctly // labels.put("traefik.http.routers." + service.getContainerName() + ".rule", // "PathPrefix(`" + FileUtils.fixSlashes(connection.getExternalPath().getPath(), From 0c469a57be43b86f0ebab5f3cd532ecd6f43a592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Thu, 22 Jan 2026 12:16:36 +0000 Subject: [PATCH 27/64] format and do imports on save for everyone --- stack-clients/.vscode/settings.json | 9 +++++++++ stack-data-uploader/.vscode/settings.json | 9 +++++++++ stack-manager/.vscode/settings.json | 9 +++++++++ 3 files changed, 27 insertions(+) create mode 100644 stack-clients/.vscode/settings.json create mode 100644 stack-data-uploader/.vscode/settings.json create mode 100644 stack-manager/.vscode/settings.json diff --git a/stack-clients/.vscode/settings.json b/stack-clients/.vscode/settings.json new file mode 100644 index 00000000..c7d20e1f --- /dev/null +++ b/stack-clients/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "[java]": { + "editor.formatOnSave": true + } +} diff --git a/stack-data-uploader/.vscode/settings.json b/stack-data-uploader/.vscode/settings.json new file mode 100644 index 00000000..c7d20e1f --- /dev/null +++ b/stack-data-uploader/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "[java]": { + "editor.formatOnSave": true + } +} diff --git a/stack-manager/.vscode/settings.json b/stack-manager/.vscode/settings.json new file mode 100644 index 00000000..c7d20e1f --- /dev/null +++ b/stack-manager/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "[java]": { + "editor.formatOnSave": true + } +} From a95a58a44f99f38bf3b7782404488db07b8f4817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Thu, 22 Jan 2026 12:17:03 +0000 Subject: [PATCH 28/64] fix the provider in traefik.yaml --- .../stack/services/traefik/configs/traefik.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml index 78e77013..8c0a18a7 100644 --- a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml +++ b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://json.schemastore.org/traefik-v3.json api: dashboard: true insecure: true @@ -11,10 +12,9 @@ entryPoints: address: ":8080" providers: - docker: + swarm: endpoint: "unix:///var/run/docker.sock" exposedByDefault: false - swarmMode: true network: "${STACK_NAME}" log: From d817ff95454da78f90fa390ead5130d56e3b9272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Mon, 26 Jan 2026 18:49:49 +0000 Subject: [PATCH 29/64] merge the labels --- .../stack/services/DockerService.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/DockerService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/DockerService.java index b79ceb22..669a1bc8 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/DockerService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/DockerService.java @@ -9,6 +9,7 @@ import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -338,8 +339,14 @@ void removeService(String serviceName) { protected ServiceSpec configureServiceSpec(ContainerService service) { ServiceSpec serviceSpec = service.getServiceSpec() - .withName(service.getContainerName()) - .withLabels(StackClient.getStackNameLabelMap()); + .withName(service.getContainerName()); + // Merge existing labels with stack name labels + Map serviceLabels = new HashMap<>(); + if (serviceSpec.getLabels() != null) { + serviceLabels.putAll(serviceSpec.getLabels()); + } + serviceLabels.putAll(StackClient.getStackNameLabelMap()); + serviceSpec.withLabels(serviceLabels); TaskSpec taskTemplate = service.getTaskTemplate(); if (null == taskTemplate.getRestartPolicy()) { taskTemplate.withRestartPolicy(new ServiceRestartPolicy() @@ -350,8 +357,14 @@ protected ServiceSpec configureServiceSpec(ContainerService service) { .withTarget(network.getId()) .withAliases(List.of(service.getName())))); ContainerSpec containerSpec = service.getContainerSpec() - .withLabels(StackClient.getStackNameLabelMap()) .withHostname(service.getName()); + // Merge existing container labels with stack name labels + Map containerLabels = new HashMap<>(); + if (containerSpec.getLabels() != null) { + containerLabels.putAll(containerSpec.getLabels()); + } + containerLabels.putAll(StackClient.getStackNameLabelMap()); + containerSpec.withLabels(containerLabels); interpolateEnvironmentVariables(containerSpec); From 5640561bfc9f39181e8a8500bb45cfed9c42e736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Mon, 26 Jan 2026 18:50:29 +0000 Subject: [PATCH 30/64] better name for a method --- .../com/cmclinnovations/stack/services/NginxService.java | 2 +- .../stack/services/ReverseProxyService.java | 2 +- .../com/cmclinnovations/stack/services/ServiceManager.java | 2 +- .../test/java/com/cmclinnovations/stack/StackHostTest.java | 7 ++++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/NginxService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/NginxService.java index f8024fc5..f3058fae 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/NginxService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/NginxService.java @@ -78,7 +78,7 @@ private void updateExternalPort(ServiceConfig config) { } } - public void addService(ContainerService service) { + public void addStackServiceToReverseProxy(ContainerService service) { NgxConfig locationConfigOut = new NgxConfig(); diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ReverseProxyService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ReverseProxyService.java index cc0175f2..b1b927b6 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ReverseProxyService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ReverseProxyService.java @@ -2,5 +2,5 @@ public interface ReverseProxyService extends Service { - public void addService(ContainerService service); + public void addStackServiceToReverseProxy(ContainerService service); } diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java index d87d2dc4..7615600e 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ServiceManager.java @@ -170,7 +170,7 @@ public S initialiseService(String stackName, String serviceN if (!StackClient.getReverseProxyName().equals(serviceName)) { ReverseProxyService reverseProxyService = getOrInitialiseService(stackName, StackClient.getReverseProxyName()); - reverseProxyService.addService(newContainerService); + reverseProxyService.addStackServiceToReverseProxy(newContainerService); } dockerService.writeEndpointConfigs(newContainerService); diff --git a/stack-clients/src/test/java/com/cmclinnovations/stack/StackHostTest.java b/stack-clients/src/test/java/com/cmclinnovations/stack/StackHostTest.java index 73f62998..d40d97ee 100644 --- a/stack-clients/src/test/java/com/cmclinnovations/stack/StackHostTest.java +++ b/stack-clients/src/test/java/com/cmclinnovations/stack/StackHostTest.java @@ -45,11 +45,12 @@ void testNameJson() { () -> Assertions.assertEquals("host", stackHost.getStringBuilder().withName().build())); } - @Test - void testEmptyStrings() { + @Test + void testEmptyStrings() { StackHost stackHostDefault = new StackHost(); StackHost stackHostJson = Assertions - .assertDoesNotThrow(() -> objectMapper.readValue("{\"proto\":\"\", \"name\":\"\",\"port\":\" \",\"path\":\" \"}", StackHost.class)); + .assertDoesNotThrow(() -> objectMapper + .readValue("{\"proto\":\"\", \"name\":\"\",\"port\":\" \",\"path\":\" \"}", StackHost.class)); Assertions.assertAll( () -> Assertions.assertEquals(stackHostDefault.getProto(), stackHostJson.getProto()), () -> Assertions.assertEquals(stackHostDefault.getName(), stackHostJson.getName()), From 01fe3679bab8bb7c1b8e89d51e3911441337e073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Mon, 26 Jan 2026 18:50:42 +0000 Subject: [PATCH 31/64] loads of label stuff --- .../stack/services/TraefikService.java | 56 +++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java index 02c91221..8e0d724b 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -6,19 +6,28 @@ import java.net.URI; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import com.cmclinnovations.stack.clients.core.StackClient; +import com.cmclinnovations.stack.clients.docker.DockerClient; import com.cmclinnovations.stack.clients.utils.FileUtils; import com.cmclinnovations.stack.services.config.ServiceConfig; import com.github.dockerjava.api.model.ContainerSpec; +import com.github.dockerjava.api.model.ContainerSpecConfig; +import com.github.dockerjava.api.model.ContainerSpecFile; +import com.github.dockerjava.api.model.ServiceSpec; public class TraefikService extends ContainerService implements ReverseProxyService { + private static final String EXTERNAL_PORT = "EXTERNAL_PORT"; + public static final String TYPE = "traefik"; - private static final String TRAEFIK_CONF_DIR = "/etc/traefik/"; + private static final String TRAEFIK_CONFIG_NAME = "traefik_config"; + private static final String TRAEFIK_CONFIG_PATH = "/etc/traefik/traefik.yml"; private static final String TRAEFIK_CONFIG_TEMPLATE = "traefik/configs/traefik.yml"; public TraefikService(String stackName, ServiceConfig config) { @@ -26,19 +35,40 @@ public TraefikService(String stackName, ServiceConfig config) { } @Override - public void doFirstTimePostStartUpConfiguration() { + protected void doPreStartUpConfiguration() { try (InputStream inStream = new BufferedInputStream( TraefikService.class.getResourceAsStream(TRAEFIK_CONFIG_TEMPLATE))) { String configContent = new String(inStream.readAllBytes(), StandardCharsets.UTF_8); // Replace the ${STACK_NAME} placeholder with actual stack name String stackName = getEnvironmentVariable(StackClient.STACK_NAME_KEY); + String stackPortString = System.getenv(EXTERNAL_PORT); + configContent = configContent.replace("${STACK_NAME}", stackName); + configContent = configContent.replace("${STACK_PORT}", stackPortString); - // Write the configuration file to the container - Map files = new HashMap<>(); - files.put("traefik.yml", configContent.getBytes(StandardCharsets.UTF_8)); - sendFilesContent(files, TRAEFIK_CONF_DIR); + // Create Docker Config for Traefik + DockerClient dockerClient = DockerClient.getInstance(); + if (!dockerClient.configExists(TRAEFIK_CONFIG_NAME)) { + dockerClient.addConfig(TRAEFIK_CONFIG_NAME, configContent.getBytes(StandardCharsets.UTF_8)); + } + + // Mount the config into the container + ContainerSpec containerSpec = getContainerSpec(); + List configs = containerSpec.getConfigs(); + if (null == configs) { + configs = new ArrayList<>(); + containerSpec.withConfigs(configs); + } + + ContainerSpecConfig traefikConfig = new ContainerSpecConfig() + .withConfigName(TRAEFIK_CONFIG_NAME) + .withFile(new ContainerSpecFile() + .withName(TRAEFIK_CONFIG_PATH) + .withUid("0") + .withGid("0") + .withMode(0444L)); + configs.add(traefikConfig); } catch (IOException ex) { throw new RuntimeException("Failed to configure Traefik", ex); @@ -46,16 +76,17 @@ public void doFirstTimePostStartUpConfiguration() { } @Override - public void addService(ContainerService service) { - ContainerSpec containerSpec = service.getContainerSpec(); - Map labels = new HashMap<>(); - containerSpec.withLabels(labels); + public void addStackServiceToReverseProxy(ContainerService service) { + // Traefik's Swarm provider reads service-level labels, not container labels + ServiceSpec serviceSpec = service.getServiceSpec(); + Map existingLabels = serviceSpec.getLabels(); + final Map labels = (existingLabels != null) ? existingLabels : new HashMap<>(); labels.put("traefik.enable", "true"); service.getConfig().getEndpoints().forEach((endpointName, connection) -> { URI externalPath = connection.getExternalPath(); if (null != externalPath) { - String serviceName = service.getName(); + String serviceName = service.getContainerName(); String routerName = serviceName + "_" + endpointName; String pathPrefix = FileUtils.fixSlashes(externalPath.getPath(), true, false); @@ -84,6 +115,9 @@ public void addService(ContainerService service) { // ".loadbalancer.server.port", // String.valueOf(connection.getInternalPort())); }); + + // Set labels on the service spec after they've been populated + serviceSpec.withLabels(labels); } } From 8e92c93fb40c2ab3d0cd96bc8ffa70216d70e02a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Mon, 26 Jan 2026 18:50:53 +0000 Subject: [PATCH 32/64] customisable stack port --- .../cmclinnovations/stack/services/traefik/configs/traefik.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml index 8c0a18a7..7e7a5e0f 100644 --- a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml +++ b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml @@ -5,7 +5,7 @@ api: entryPoints: web: - address: ":80" + address: ":${STACK_PORT}" websecure: address: ":443" traefik: From 72e070292b5339b6819b321c71f4f63b22c7eb29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Mon, 26 Jan 2026 19:13:51 +0000 Subject: [PATCH 33/64] rm the bad host port casting --- .../java/com/cmclinnovations/stack/services/TraefikService.java | 2 -- .../cmclinnovations/stack/services/traefik/configs/traefik.yml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java index 8e0d724b..1abf0d30 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -42,10 +42,8 @@ protected void doPreStartUpConfiguration() { String configContent = new String(inStream.readAllBytes(), StandardCharsets.UTF_8); // Replace the ${STACK_NAME} placeholder with actual stack name String stackName = getEnvironmentVariable(StackClient.STACK_NAME_KEY); - String stackPortString = System.getenv(EXTERNAL_PORT); configContent = configContent.replace("${STACK_NAME}", stackName); - configContent = configContent.replace("${STACK_PORT}", stackPortString); // Create Docker Config for Traefik DockerClient dockerClient = DockerClient.getInstance(); diff --git a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml index 7e7a5e0f..8c0a18a7 100644 --- a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml +++ b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml @@ -5,7 +5,7 @@ api: entryPoints: web: - address: ":${STACK_PORT}" + address: ":80" websecure: address: ":443" traefik: From 26af8d87915c479ed6bbaedfb1c76123c0f0cb41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 28 Jan 2026 17:12:25 +0000 Subject: [PATCH 34/64] reproduce specific port functionality --- .../stack/services/NginxService.java | 23 +--------- .../stack/services/ReverseProxyService.java | 43 +++++++++++++++++++ .../stack/services/TraefikService.java | 10 +---- 3 files changed, 46 insertions(+), 30 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/NginxService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/NginxService.java index f3058fae..cbdd2e62 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/NginxService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/NginxService.java @@ -14,8 +14,6 @@ import com.cmclinnovations.stack.exceptions.InvalidTemplateException; import com.cmclinnovations.stack.services.config.Connection; import com.cmclinnovations.stack.services.config.ServiceConfig; -import com.github.dockerjava.api.model.EndpointSpec; -import com.github.dockerjava.api.model.PortConfig; import com.github.odiszapc.nginxparser.NgxBlock; import com.github.odiszapc.nginxparser.NgxComment; import com.github.odiszapc.nginxparser.NgxConfig; @@ -27,8 +25,6 @@ public final class NginxService extends ContainerService implements ReverseProxyService { - private static final String EXTERNAL_PORT = "EXTERNAL_PORT"; - public static final String TYPE = "nginx"; private static final String TEMPLATE_TYPE = "Nginx config"; @@ -63,21 +59,6 @@ protected void doPreStartUpConfiguration() { } } - private void updateExternalPort(ServiceConfig config) { - String externalPort = System.getenv(EXTERNAL_PORT); - if (null != externalPort) { - EndpointSpec endpointSpec = config.getDockerServiceSpec().getEndpointSpec(); - if (null != endpointSpec) { - List ports = endpointSpec.getPorts(); - if (null != ports) { - ports.stream() - .filter(port -> port.getTargetPort() == 80) - .forEach(port -> port.withPublishedPort(Integer.parseInt(externalPort))); - } - } - } - } - public void addStackServiceToReverseProxy(ContainerService service) { NgxConfig locationConfigOut = new NgxConfig(); @@ -170,9 +151,7 @@ private String getProxyPassValue(Connection connection, String hostname) { } private String getServerURL(Connection connection, String hostname) { - URL url = connection.getUrl(); - int port = url.getPort(); - return hostname + ":" + ((-1 == port) ? 80 : port); + return hostname + ":" + getPortOrDefault(connection.getUrl()); } private final class ConfigSender { diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ReverseProxyService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ReverseProxyService.java index b1b927b6..7bcc6503 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/ReverseProxyService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/ReverseProxyService.java @@ -1,6 +1,49 @@ package com.cmclinnovations.stack.services; +import java.net.URL; +import java.util.List; + +import com.cmclinnovations.stack.services.config.ServiceConfig; +import com.github.dockerjava.api.model.EndpointSpec; +import com.github.dockerjava.api.model.PortConfig; + public interface ReverseProxyService extends Service { public void addStackServiceToReverseProxy(ContainerService service); + + /** + * Updates the external port mapping for the reverse proxy service. + * This allows multiple stacks to run on the same host by exposing each stack's + * reverse proxy on a different external port. + * + * @param config The service configuration containing the endpoint + * specifications + */ + default void updateExternalPort(ServiceConfig config) { + String externalPort = System.getenv("EXTERNAL_PORT"); + if (null != externalPort) { + EndpointSpec endpointSpec = config.getDockerServiceSpec().getEndpointSpec(); + if (null != endpointSpec) { + List ports = endpointSpec.getPorts(); + if (null != ports) { + ports.stream() + .filter(port -> port.getTargetPort() == 80) + .forEach(port -> port.withPublishedPort(Integer.parseInt(externalPort))); + } + } + } + } + + /** + * Gets the port from a URL, defaulting to 80 if not specified. + * This is a common pattern when working with HTTP services that don't + * explicitly specify a port. + * + * @param url The URL to extract the port from + * @return The port number, or 80 if the URL doesn't specify a port (-1) + */ + default int getPortOrDefault(URL url) { + int port = url.getPort(); + return (port == -1) ? 80 : port; + } } diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java index 1abf0d30..bd263750 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -4,7 +4,6 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; -import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; @@ -22,8 +21,6 @@ public class TraefikService extends ContainerService implements ReverseProxyService { - private static final String EXTERNAL_PORT = "EXTERNAL_PORT"; - public static final String TYPE = "traefik"; private static final String TRAEFIK_CONFIG_NAME = "traefik_config"; @@ -32,6 +29,7 @@ public class TraefikService extends ContainerService implements ReverseProxyServ public TraefikService(String stackName, ServiceConfig config) { super(stackName, config); + updateExternalPort(config); } @Override @@ -94,11 +92,7 @@ public void addStackServiceToReverseProxy(ContainerService service) { labels.put("traefik.http.routers." + routerName + ".entrypoints", "web"); // Configure service with the internal port - URL url = connection.getUrl(); - int port = url.getPort(); - if (port == -1) { - port = 80; // Default port - } + int port = getPortOrDefault(connection.getUrl()); labels.put("traefik.http.routers." + routerName + ".service", routerName); labels.put("traefik.http.services." + routerName + ".loadbalancer.server.port", String.valueOf(port)); From 2755320e16f2575b299a9105082a824509f8ea46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Wed, 28 Jan 2026 18:54:55 +0000 Subject: [PATCH 35/64] try an auth setup with keycloak env vars --- .../stack/services/TraefikService.java | 69 ++++++++++++++++--- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java index bd263750..9bc63dc3 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -27,6 +27,12 @@ public class TraefikService extends ContainerService implements ReverseProxyServ private static final String TRAEFIK_CONFIG_PATH = "/etc/traefik/traefik.yml"; private static final String TRAEFIK_CONFIG_TEMPLATE = "traefik/configs/traefik.yml"; + // Keycloak authentication configuration + private static final String KEYCLOAK_AUTH_ENABLED = "KEYCLOAK_AUTH_ENABLED"; + private static final String KEYCLOAK_AUTH_URL = "KEYCLOAK_AUTH_URL"; + private static final String KEYCLOAK_REALM = "KEYCLOAK_REALM"; + private static final String AUTH_MIDDLEWARE_NAME = "keycloak-auth"; + public TraefikService(String stackName, ServiceConfig config) { super(stackName, config); updateExternalPort(config); @@ -79,6 +85,10 @@ public void addStackServiceToReverseProxy(ContainerService service) { final Map labels = (existingLabels != null) ? existingLabels : new HashMap<>(); labels.put("traefik.enable", "true"); + // Check if Keycloak authentication is enabled globally + boolean authEnabled = isKeycloakAuthEnabled(); + String authMiddleware = authEnabled ? AUTH_MIDDLEWARE_NAME : null; + service.getConfig().getEndpoints().forEach((endpointName, connection) -> { URI externalPath = connection.getExternalPath(); if (null != externalPath) { @@ -91,25 +101,66 @@ public void addStackServiceToReverseProxy(ContainerService service) { "PathPrefix(`" + pathPrefix + "`)"); labels.put("traefik.http.routers." + routerName + ".entrypoints", "web"); + // Add authentication middleware if enabled + if (authMiddleware != null) { + labels.put("traefik.http.routers." + routerName + ".middlewares", authMiddleware); + } + // Configure service with the internal port int port = getPortOrDefault(connection.getUrl()); labels.put("traefik.http.routers." + routerName + ".service", routerName); labels.put("traefik.http.services." + routerName + ".loadbalancer.server.port", String.valueOf(port)); } - - // TODO: Set the labels correctly - // labels.put("traefik.http.routers." + service.getContainerName() + ".rule", - // "PathPrefix(`" + FileUtils.fixSlashes(connection.getExternalPath().getPath(), - // true, false) + "`)"); - // labels.put( - // "traefik.http.services." + service.getServiceName() + - // ".loadbalancer.server.port", - // String.valueOf(connection.getInternalPort())); }); + // If auth is enabled, configure the ForwardAuth middleware globally for this + // Traefik instance + if (authEnabled) { + configureKeycloakAuthMiddleware(labels); + } + // Set labels on the service spec after they've been populated serviceSpec.withLabels(labels); } + /** + * Checks if Keycloak authentication is enabled via environment variable. + */ + private boolean isKeycloakAuthEnabled() { + String enabled = System.getenv(KEYCLOAK_AUTH_ENABLED); + return "true".equalsIgnoreCase(enabled); + } + + /** + * Configures the Keycloak ForwardAuth middleware on the Traefik service. + * This middleware will be applied to all routers that reference it. + */ + private void configureKeycloakAuthMiddleware(Map labels) { + String authUrl = System.getenv(KEYCLOAK_AUTH_URL); + String realm = System.getenv(KEYCLOAK_REALM); + + if (authUrl == null || realm == null) { + throw new RuntimeException( + "KEYCLOAK_AUTH_ENABLED is true but KEYCLOAK_AUTH_URL or KEYCLOAK_REALM is not set. " + + "Please configure these environment variables."); + } + + // Construct the Keycloak userinfo endpoint URL + // This endpoint validates bearer tokens and returns 200 for valid tokens, 401 + // for invalid + String userinfoEndpoint = authUrl.replaceAll("/+$", "") + "/realms/" + realm + + "/protocol/openid-connect/userinfo"; + + // Configure ForwardAuth middleware + labels.put("traefik.http.middlewares." + AUTH_MIDDLEWARE_NAME + ".forwardauth.address", userinfoEndpoint); + + // Forward the Authorization header to Keycloak + labels.put("traefik.http.middlewares." + AUTH_MIDDLEWARE_NAME + ".forwardauth.authResponseHeaders", + "Authorization"); + + // Trust forwarded headers + labels.put("traefik.http.middlewares." + AUTH_MIDDLEWARE_NAME + ".forwardauth.trustForwardHeader", "true"); + } + } From 77329b4ce945f30d4c1bc45889eeb49cbfbde560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Thu, 29 Jan 2026 17:40:35 +0000 Subject: [PATCH 36/64] stop setting traefik on for all containers --- .../stack/services/TraefikService.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java index 9bc63dc3..c88a932f 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -83,15 +83,19 @@ public void addStackServiceToReverseProxy(ContainerService service) { ServiceSpec serviceSpec = service.getServiceSpec(); Map existingLabels = serviceSpec.getLabels(); final Map labels = (existingLabels != null) ? existingLabels : new HashMap<>(); - labels.put("traefik.enable", "true"); // Check if Keycloak authentication is enabled globally boolean authEnabled = isKeycloakAuthEnabled(); String authMiddleware = authEnabled ? AUTH_MIDDLEWARE_NAME : null; + // Track if any endpoints with external paths were found + final boolean[] hasExternalEndpoints = { false }; + service.getConfig().getEndpoints().forEach((endpointName, connection) -> { URI externalPath = connection.getExternalPath(); if (null != externalPath) { + hasExternalEndpoints[0] = true; + String serviceName = service.getContainerName(); String routerName = serviceName + "_" + endpointName; String pathPrefix = FileUtils.fixSlashes(externalPath.getPath(), true, false); @@ -114,10 +118,15 @@ public void addStackServiceToReverseProxy(ContainerService service) { } }); - // If auth is enabled, configure the ForwardAuth middleware globally for this - // Traefik instance - if (authEnabled) { - configureKeycloakAuthMiddleware(labels); + // Only enable Traefik for services that have external endpoints + if (hasExternalEndpoints[0]) { + labels.put("traefik.enable", "true"); + + // If auth is enabled, configure the ForwardAuth middleware globally for this + // Traefik instance + if (authEnabled) { + configureKeycloakAuthMiddleware(labels); + } } // Set labels on the service spec after they've been populated From 200fe5fee7c680f3d7471464f74ef7ef640d0f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Thu, 29 Jan 2026 17:41:09 +0000 Subject: [PATCH 37/64] attach request headers to forward auth not response headers --- .../java/com/cmclinnovations/stack/services/TraefikService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java index c88a932f..55a5c34e 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -165,7 +165,7 @@ private void configureKeycloakAuthMiddleware(Map labels) { labels.put("traefik.http.middlewares." + AUTH_MIDDLEWARE_NAME + ".forwardauth.address", userinfoEndpoint); // Forward the Authorization header to Keycloak - labels.put("traefik.http.middlewares." + AUTH_MIDDLEWARE_NAME + ".forwardauth.authResponseHeaders", + labels.put("traefik.http.middlewares." + AUTH_MIDDLEWARE_NAME + ".forwardauth.authRequestHeaders", "Authorization"); // Trust forwarded headers From aaad30c52b687bee6e01fe3bc6da8afc66114283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Thu, 29 Jan 2026 17:42:11 +0000 Subject: [PATCH 38/64] necessary env vars for middleware proxy --- stack-manager/docker-compose.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/stack-manager/docker-compose.yml b/stack-manager/docker-compose.yml index f2536823..52f6ad8d 100644 --- a/stack-manager/docker-compose.yml +++ b/stack-manager/docker-compose.yml @@ -4,6 +4,9 @@ services: environment: EXTERNAL_PORT: "${EXTERNAL_PORT-3838}" STACK_BASE_DIR: "${STACK_BASE_DIR}" + KEYCLOAK_AUTH_ENABLED: "${KEYCLOAK_AUTH_ENABLED-false}" + KEYCLOAK_AUTH_URL: "${KEYCLOAK_AUTH_URL-}" + KEYCLOAK_REALM: "${KEYCLOAK_REALM-}" volumes: - jdbc_drivers:/jdbc - ./inputs/data:/inputs/data From e0d9e8b3d6e34104606493aa5b9d76efc5a7d63c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 6 Feb 2026 16:55:21 +0000 Subject: [PATCH 39/64] forwardauth config --- forwardauth/.env | 6 ++++++ forwardauth/bingo.log | 3 +++ forwardauth/compose.yml | 28 ++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 forwardauth/.env create mode 100644 forwardauth/bingo.log create mode 100644 forwardauth/compose.yml diff --git a/forwardauth/.env b/forwardauth/.env new file mode 100644 index 00000000..d4e8641e --- /dev/null +++ b/forwardauth/.env @@ -0,0 +1,6 @@ +ENCRYPTION_KEY=f57d73478dedc596cf41679568c62986 +CLIENT_SECRET=7awzFYIlK3io3ipSuJd07aXxShp9p9Vp +CLIENT_ID=bingus +PROVIDER_URI=https://dev.theworldavatar.io/realms/twa-test +SIGNING_SECRET=security_is_hard +STACK_NAME=bingus \ No newline at end of file diff --git a/forwardauth/bingo.log b/forwardauth/bingo.log new file mode 100644 index 00000000..c446640b --- /dev/null +++ b/forwardauth/bingo.log @@ -0,0 +1,3 @@ + +time="2026-02-05T18:06:17Z" level=debug msg="Authenticate request" headers="map[Accept:[text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7] Accept-Encoding:[gzip, deflate, br, zstd] Accept-Language:[en-IE,en-GB;q=0.9,en;q=0.8] Cache-Control:[max-age=0] Cookie:[_forward_auth_csrf=98b60036f1373bce89d3af6249b537b5] Dnt:[1] Sec-Ch-Ua:[\"Not(A:Brand\";v=\"8\", \"Chromium\";v=\"144\", \"Microsoft Edge\";v=\"144\"] Sec-Ch-Ua-Mobile:[?0] Sec-Ch-Ua-Platform:[\"Windows\"] Sec-Fetch-Dest:[document] Sec-Fetch-Mode:[navigate] Sec-Fetch-Site:[cross-site] Upgrade-Insecure-Requests:[1] User-Agent:[Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0] X-Forwarded-For:[10.0.0.2] X-Forwarded-Host:[localhost:1916] X-Forwarded-Port:[1916] X-Forwarded-Proto:[http] X-Forwarded-Server:[bingus-traefik] X-Real-Ip:[10.0.0.2]]" rule=default source_ip=10.0.0.2 +time="2026-02-05T18:06:17Z" level=debug msg="sending CSRF cookie and a redirect to OIDC login" source_ip=10.0.0.2 diff --git a/forwardauth/compose.yml b/forwardauth/compose.yml new file mode 100644 index 00000000..4e64cd19 --- /dev/null +++ b/forwardauth/compose.yml @@ -0,0 +1,28 @@ +services: + forwardauth: + image: mesosphere/traefik-forward-auth + networks: + - stack + environment: + - SECRET=${SIGNING_SECRET} + - PROVIDER_URI=${PROVIDER_URI} + - CLIENT_ID=${CLIENT_ID} + - CLIENT_SECRET=${CLIENT_SECRET} + - ENCRYPTION_KEY=${ENCRYPTION_KEY} + labels: + - "traefik.enable=true" + - "traefik.http.services.forwardauth.loadbalancer.server.port=4181" + - "traefik.http.routers.forwardauth.entrypoints=web" + # Router for OAuth callback endpoint - middleware will detect and process callback + + - "traefik.http.routers.forwardauth.rule=Path(`/_oauth`)" + # Middleware definition used by other services + - "traefik.http.middlewares.traefik-forward-auth.forwardauth.address=http://forwardauth:4181" + - "traefik.http.middlewares.traefik-forward-auth.forwardauth.authResponseHeaders=X-Forwarded-User" + - "traefik.http.middlewares.traefik-forward-auth.forwardauth.trustForwardHeader=true" + +networks: + stack: + name: ${STACK_NAME} + driver: overlay + external: true From 6525d18de732faf8ca0dc3560188576ec4757605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 6 Feb 2026 17:02:03 +0000 Subject: [PATCH 40/64] generalise traefikservice to auth with another authprovider --- .../stack/services/TraefikService.java | 62 +++++-------------- 1 file changed, 16 insertions(+), 46 deletions(-) diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java index 55a5c34e..c5dc2fac 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -27,11 +27,9 @@ public class TraefikService extends ContainerService implements ReverseProxyServ private static final String TRAEFIK_CONFIG_PATH = "/etc/traefik/traefik.yml"; private static final String TRAEFIK_CONFIG_TEMPLATE = "traefik/configs/traefik.yml"; - // Keycloak authentication configuration - private static final String KEYCLOAK_AUTH_ENABLED = "KEYCLOAK_AUTH_ENABLED"; - private static final String KEYCLOAK_AUTH_URL = "KEYCLOAK_AUTH_URL"; - private static final String KEYCLOAK_REALM = "KEYCLOAK_REALM"; - private static final String AUTH_MIDDLEWARE_NAME = "keycloak-auth"; + // Forward authentication middleware name (defined by the forwardauth service) + private static final String AUTH_ENABLED = "AUTH_ENABLED"; + private static final String AUTH_MIDDLEWARE_NAME = "traefik-forward-auth"; public TraefikService(String stackName, ServiceConfig config) { super(stackName, config); @@ -84,8 +82,10 @@ public void addStackServiceToReverseProxy(ContainerService service) { Map existingLabels = serviceSpec.getLabels(); final Map labels = (existingLabels != null) ? existingLabels : new HashMap<>(); - // Check if Keycloak authentication is enabled globally - boolean authEnabled = isKeycloakAuthEnabled(); + // Check if authentication is enabled globally + // The forwardauth service defines the middleware that handles OAuth with + // Keycloak + boolean authEnabled = isAuthEnabled(); String authMiddleware = authEnabled ? AUTH_MIDDLEWARE_NAME : null; // Track if any endpoints with external paths were found @@ -122,11 +122,10 @@ public void addStackServiceToReverseProxy(ContainerService service) { if (hasExternalEndpoints[0]) { labels.put("traefik.enable", "true"); - // If auth is enabled, configure the ForwardAuth middleware globally for this - // Traefik instance - if (authEnabled) { - configureKeycloakAuthMiddleware(labels); - } + // Note: The traefik-forward-auth middleware is defined by the forwardauth + // service + // Services that need authentication simply reference this middleware in their + // router config } // Set labels on the service spec after they've been populated @@ -134,42 +133,13 @@ public void addStackServiceToReverseProxy(ContainerService service) { } /** - * Checks if Keycloak authentication is enabled via environment variable. + * Checks if authentication is enabled via environment variable. + * When enabled, services will use the traefik-forward-auth middleware + * that is defined and configured by the forwardauth service. */ - private boolean isKeycloakAuthEnabled() { - String enabled = System.getenv(KEYCLOAK_AUTH_ENABLED); + private boolean isAuthEnabled() { + String enabled = System.getenv(AUTH_ENABLED); return "true".equalsIgnoreCase(enabled); } - /** - * Configures the Keycloak ForwardAuth middleware on the Traefik service. - * This middleware will be applied to all routers that reference it. - */ - private void configureKeycloakAuthMiddleware(Map labels) { - String authUrl = System.getenv(KEYCLOAK_AUTH_URL); - String realm = System.getenv(KEYCLOAK_REALM); - - if (authUrl == null || realm == null) { - throw new RuntimeException( - "KEYCLOAK_AUTH_ENABLED is true but KEYCLOAK_AUTH_URL or KEYCLOAK_REALM is not set. " + - "Please configure these environment variables."); - } - - // Construct the Keycloak userinfo endpoint URL - // This endpoint validates bearer tokens and returns 200 for valid tokens, 401 - // for invalid - String userinfoEndpoint = authUrl.replaceAll("/+$", "") + "/realms/" + realm - + "/protocol/openid-connect/userinfo"; - - // Configure ForwardAuth middleware - labels.put("traefik.http.middlewares." + AUTH_MIDDLEWARE_NAME + ".forwardauth.address", userinfoEndpoint); - - // Forward the Authorization header to Keycloak - labels.put("traefik.http.middlewares." + AUTH_MIDDLEWARE_NAME + ".forwardauth.authRequestHeaders", - "Authorization"); - - // Trust forwarded headers - labels.put("traefik.http.middlewares." + AUTH_MIDDLEWARE_NAME + ".forwardauth.trustForwardHeader", "true"); - } - } From dcd2eed877fad1d63a2578024ba06c837ba46b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 6 Feb 2026 17:06:46 +0000 Subject: [PATCH 41/64] oprional reverse proxy type --- .../src/main/java/com/cmclinnovations/stack/StackConfig.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java b/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java index fc97f92a..5fbe8127 100644 --- a/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java +++ b/stack-manager/src/main/java/com/cmclinnovations/stack/StackConfig.java @@ -5,7 +5,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import com.cmclinnovations.stack.clients.core.StackHost; import com.cmclinnovations.stack.services.NginxService; @@ -33,7 +32,7 @@ private enum Selector { private final Boolean isolated = false; @JsonProperty("reverseProxy") - private final Optional reverseProxy = Optional.empty(); + private String reverseProxy; @JsonProperty("hostName") private void setHostName(String hostName) { @@ -61,6 +60,6 @@ public boolean isIsolated() { } public String getReverseProxyName() { - return reverseProxy.orElse(NginxService.TYPE); + return reverseProxy != null ? reverseProxy : NginxService.TYPE; } } From 17a010083e6d118f54dc6d18e2873510be59cf49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 17 Feb 2026 15:37:50 +0000 Subject: [PATCH 42/64] pass env vars into stack-manager app --- stack-manager/docker-compose-stack.yml | 1 + stack-manager/docker-compose.yml | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/stack-manager/docker-compose-stack.yml b/stack-manager/docker-compose-stack.yml index 83d31e25..ce09e9c4 100644 --- a/stack-manager/docker-compose-stack.yml +++ b/stack-manager/docker-compose-stack.yml @@ -9,6 +9,7 @@ services: - "STACK_NAME=${STACK_NAME}" - "EXECUTABLE=${EXECUTABLE}" - "API_SOCK=${API_SOCK}" + - "AUTH_ENABLED=${AUTH_ENABLED}" security_opt: - label=disable volumes: diff --git a/stack-manager/docker-compose.yml b/stack-manager/docker-compose.yml index 52f6ad8d..a0447a01 100644 --- a/stack-manager/docker-compose.yml +++ b/stack-manager/docker-compose.yml @@ -4,9 +4,7 @@ services: environment: EXTERNAL_PORT: "${EXTERNAL_PORT-3838}" STACK_BASE_DIR: "${STACK_BASE_DIR}" - KEYCLOAK_AUTH_ENABLED: "${KEYCLOAK_AUTH_ENABLED-false}" - KEYCLOAK_AUTH_URL: "${KEYCLOAK_AUTH_URL-}" - KEYCLOAK_REALM: "${KEYCLOAK_REALM-}" + AUTH_ENABLED: "${AUTH_ENABLED-false}" volumes: - jdbc_drivers:/jdbc - ./inputs/data:/inputs/data From cf86bbbd06ae269fe9ac94c28d3790f5219bd491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 17 Feb 2026 15:38:01 +0000 Subject: [PATCH 43/64] format ApiClientExtension --- .../java/com/github/dockerjava/jaxrs/ApiClientExtension.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stack-clients/src/main/java/com/github/dockerjava/jaxrs/ApiClientExtension.java b/stack-clients/src/main/java/com/github/dockerjava/jaxrs/ApiClientExtension.java index c569c235..042bdbee 100644 --- a/stack-clients/src/main/java/com/github/dockerjava/jaxrs/ApiClientExtension.java +++ b/stack-clients/src/main/java/com/github/dockerjava/jaxrs/ApiClientExtension.java @@ -15,9 +15,10 @@ import org.glassfish.jersey.apache.connector.ApacheConnectorProvider; import org.glassfish.jersey.client.ClientConfig; -import io.theworldavatar.swagger.podman.ApiClient; import com.github.dockerjava.jaxrs.filter.ResponseStatusExceptionFilter; +import io.theworldavatar.swagger.podman.ApiClient; + final public class ApiClientExtension extends ApiClient { private final PoolingHttpClientConnectionManager connManager; From ba973fb7ed11387127e6f55e14f385d6a2333ccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 17 Feb 2026 15:38:56 +0000 Subject: [PATCH 44/64] static and dynamic builtin config for traefik via stack --- .../stack/services/TraefikService.java | 77 ++++++++++++++----- .../services/traefik/configs/dynamic.yml | 44 +++++++++++ .../services/traefik/configs/traefik.yml | 10 ++- 3 files changed, 108 insertions(+), 23 deletions(-) create mode 100644 stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/dynamic.yml diff --git a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java index c5dc2fac..75ee6ca6 100644 --- a/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java +++ b/stack-clients/src/main/java/com/cmclinnovations/stack/services/TraefikService.java @@ -27,9 +27,13 @@ public class TraefikService extends ContainerService implements ReverseProxyServ private static final String TRAEFIK_CONFIG_PATH = "/etc/traefik/traefik.yml"; private static final String TRAEFIK_CONFIG_TEMPLATE = "traefik/configs/traefik.yml"; + private static final String TRAEFIK_DYNAMIC_CONFIG_NAME = "traefik_dynamic_config"; + private static final String TRAEFIK_DYNAMIC_CONFIG_PATH = "/etc/traefik/dynamic.yml"; + private static final String TRAEFIK_DYNAMIC_CONFIG_TEMPLATE = "traefik/configs/dynamic.yml"; + // Forward authentication middleware name (defined by the forwardauth service) private static final String AUTH_ENABLED = "AUTH_ENABLED"; - private static final String AUTH_MIDDLEWARE_NAME = "traefik-forward-auth"; + private static final String AUTH_MIDDLEWARE_NAME = "oauth-auth-redirect"; public TraefikService(String stackName, ServiceConfig config) { super(stackName, config); @@ -38,29 +42,63 @@ public TraefikService(String stackName, ServiceConfig config) { @Override protected void doPreStartUpConfiguration() { + DockerClient dockerClient = DockerClient.getInstance(); + ContainerSpec containerSpec = getContainerSpec(); + List configs = containerSpec.getConfigs(); + if (null == configs) { + configs = new ArrayList<>(); + containerSpec.withConfigs(configs); + } + + String stackName = getEnvironmentVariable(StackClient.STACK_NAME_KEY); + + // Create and mount static Traefik configuration + configureTraefikStaticConfig(dockerClient, configs, stackName); + + // Create and mount dynamic Traefik configuration (for middlewares / custom + // rules and routers etc) + configureTraefikDynamicConfig(dockerClient, configs); + } + + private void configureTraefikDynamicConfig(DockerClient dockerClient, List configs) { + if (isAuthEnabled()) { + try (InputStream inStream = new BufferedInputStream( + TraefikService.class.getResourceAsStream(TRAEFIK_DYNAMIC_CONFIG_TEMPLATE))) { + + String dynamicConfigContent = new String(inStream.readAllBytes(), StandardCharsets.UTF_8); + + if (!dockerClient.configExists(TRAEFIK_DYNAMIC_CONFIG_NAME)) { + dockerClient.addConfig(TRAEFIK_DYNAMIC_CONFIG_NAME, + dynamicConfigContent.getBytes(StandardCharsets.UTF_8)); + } + + ContainerSpecConfig dynamicConfig = new ContainerSpecConfig() + .withConfigName(TRAEFIK_DYNAMIC_CONFIG_NAME) + .withFile(new ContainerSpecFile() + .withName(TRAEFIK_DYNAMIC_CONFIG_PATH) + .withUid("0") + .withGid("0") + .withMode(0444L)); + configs.add(dynamicConfig); + + } catch (IOException ex) { + throw new RuntimeException("Failed to configure Traefik dynamic config", ex); + } + } + } + + private void configureTraefikStaticConfig(DockerClient dockerClient, List configs, + String stackName) { try (InputStream inStream = new BufferedInputStream( TraefikService.class.getResourceAsStream(TRAEFIK_CONFIG_TEMPLATE))) { String configContent = new String(inStream.readAllBytes(), StandardCharsets.UTF_8); - // Replace the ${STACK_NAME} placeholder with actual stack name - String stackName = getEnvironmentVariable(StackClient.STACK_NAME_KEY); - configContent = configContent.replace("${STACK_NAME}", stackName); - // Create Docker Config for Traefik - DockerClient dockerClient = DockerClient.getInstance(); if (!dockerClient.configExists(TRAEFIK_CONFIG_NAME)) { dockerClient.addConfig(TRAEFIK_CONFIG_NAME, configContent.getBytes(StandardCharsets.UTF_8)); } - // Mount the config into the container - ContainerSpec containerSpec = getContainerSpec(); - List configs = containerSpec.getConfigs(); - if (null == configs) { - configs = new ArrayList<>(); - containerSpec.withConfigs(configs); - } - ContainerSpecConfig traefikConfig = new ContainerSpecConfig() .withConfigName(TRAEFIK_CONFIG_NAME) .withFile(new ContainerSpecFile() @@ -71,7 +109,7 @@ protected void doPreStartUpConfiguration() { configs.add(traefikConfig); } catch (IOException ex) { - throw new RuntimeException("Failed to configure Traefik", ex); + throw new RuntimeException("Failed to configure Traefik static config", ex); } } @@ -83,10 +121,9 @@ public void addStackServiceToReverseProxy(ContainerService service) { final Map labels = (existingLabels != null) ? existingLabels : new HashMap<>(); // Check if authentication is enabled globally - // The forwardauth service defines the middleware that handles OAuth with - // Keycloak + // The middleware is defined in Traefik's dynamic config (file provider) boolean authEnabled = isAuthEnabled(); - String authMiddleware = authEnabled ? AUTH_MIDDLEWARE_NAME : null; + String authMiddleware = authEnabled ? AUTH_MIDDLEWARE_NAME + "@file" : null; // Track if any endpoints with external paths were found final boolean[] hasExternalEndpoints = { false }; @@ -134,8 +171,8 @@ public void addStackServiceToReverseProxy(ContainerService service) { /** * Checks if authentication is enabled via environment variable. - * When enabled, services will use the traefik-forward-auth middleware - * that is defined and configured by the forwardauth service. + * When enabled, services will use the forwardauth middleware + * that is defined in Traefik's dynamic configuration (file provider). */ private boolean isAuthEnabled() { String enabled = System.getenv(AUTH_ENABLED); diff --git a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/dynamic.yml b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/dynamic.yml new file mode 100644 index 00000000..83ab63b9 --- /dev/null +++ b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/dynamic.yml @@ -0,0 +1,44 @@ +# Dynamic Traefik configuration for middlewares +http: + routers: + services-oauth2-route: + rule: "PathPrefix(`/oauth2/`)" + middlewares: + - auth-headers + service: oauth-backend + + services: + oauth-backend: + loadBalancer: + servers: + - url: http://oauth2-proxy:4180 + + middlewares: + auth-headers: + headers: + sslRedirect: false + stsSeconds: 315360000 + browserXssFilter: true + contentTypeNosniff: true + forceSTSHeader: true + stsIncludeSubdomains: true + stsPreload: true + frameDeny: true + oauth-auth-redirect: + forwardAuth: + address: http://oauth2-proxy:4180/ + trustForwardHeader: true + authResponseHeaders: + - X-Auth-Request-User + - X-Auth-Request-Email + - X-Auth-Request-Access-Token + - Authorization + authRequestHeaders: + - Authorization + oauth-auth-wo-redirect: + forwardAuth: + address: http://oauth2-proxy:4180/oauth2/auth + trustForwardHeader: true + authResponseHeaders: + - X-Auth-Request-Access-Token + - Authorization diff --git a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml index 8c0a18a7..63c3921a 100644 --- a/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml +++ b/stack-clients/src/main/resources/com/cmclinnovations/stack/services/traefik/configs/traefik.yml @@ -12,10 +12,14 @@ entryPoints: address: ":8080" providers: - swarm: + # this one will read labels from containers (podman probably), or services in the case of swarm mode + docker: # swarm endpoint: "unix:///var/run/docker.sock" exposedByDefault: false - network: "${STACK_NAME}" - + network: ${STACK_NAME} + # this one reads the file specified, hot refreshes the config when it changes. More responsive than the containers which needs updating of services / containers to trigger a refresh. + file: + filename: "/etc/traefik/dynamic.yml" + watch: true log: level: INFO From 94f7c2adddfc531f8d61e7cc7da684ff4700c46a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 17 Feb 2026 15:44:23 +0000 Subject: [PATCH 45/64] ignore .env --- stack-manager/stack.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/stack-manager/stack.sh b/stack-manager/stack.sh index e00c5eba..45ee1ec4 100755 --- a/stack-manager/stack.sh +++ b/stack-manager/stack.sh @@ -3,6 +3,9 @@ # This fixes issues with WSL not mounting the Windows directories in a stable way. cd . +# Export Keycloak authentication configuration before running commands +export AUTH_ENABLED=true + COMMAND=$1 shift From 5a65be986839df550b981fe9984c2c6b06ce7dc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 17 Feb 2026 15:59:21 +0000 Subject: [PATCH 46/64] test skeleton --- .../authenticated_curl_through_traefik.sh | 5 ++ forwardauth/test/checklist.md | 57 +++++++++++++++++ forwardauth/test/curl_for_token.sh | 15 +++++ forwardauth/test/test.sh | 62 +++++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100755 forwardauth/test/authenticated_curl_through_traefik.sh create mode 100644 forwardauth/test/checklist.md create mode 100755 forwardauth/test/curl_for_token.sh create mode 100644 forwardauth/test/test.sh diff --git a/forwardauth/test/authenticated_curl_through_traefik.sh b/forwardauth/test/authenticated_curl_through_traefik.sh new file mode 100755 index 00000000..c0f251fe --- /dev/null +++ b/forwardauth/test/authenticated_curl_through_traefik.sh @@ -0,0 +1,5 @@ +#!/usr/bin/bash + +TOKEN=$(./curl_for_token_dev.sh | jq -r '.access_token') + +curl -v -H "Authorization: Bearer $TOKEN" http://localhost:1916/whoami/ \ No newline at end of file diff --git a/forwardauth/test/checklist.md b/forwardauth/test/checklist.md new file mode 100644 index 00000000..bab8c51f --- /dev/null +++ b/forwardauth/test/checklist.md @@ -0,0 +1,57 @@ +# Pre-Production Security Checklist + +## Authentication & Authorization +- [ ] Unauthenticated users are redirected to Keycloak +- [ ] Valid tokens grant access to protected resources +- [ ] Invalid/expired tokens are rejected +- [ ] Public endpoints remain accessible without authentication +- [ ] Session cookies have appropriate security flags (Secure, HttpOnly, SameSite) +- [ ] PKCE (S256) is enabled for OAuth2 flow + +## Configuration Validation +- [ ] `COOKIE_SECURE=true` in production (HTTPS only) +- [ ] `COOKIE_DOMAIN` matches production domain +- [ ] `REDIRECT_URL` points to correct production URL +- [ ] `OIDC_ISSUER_URL` points to production Keycloak +- [ ] Client secret is stored securely (not in version control) +- [ ] Cookie secret is cryptographically random (32+ bytes) + +## Network & Routing +- [ ] Traefik middleware is applied to all protected services +- [ ] OAuth2 endpoints (`/oauth2/*`) are publicly accessible +- [ ] ForwardAuth endpoint (`/oauth2/auth`) returns 401 for unauthenticated +- [ ] No sensitive endpoints are accidentally exposed +- [ ] Rate limiting is configured on authentication endpoints + +## Error Handling +- [ ] 401/403 errors properly redirect to sign-in +- [ ] OAuth callback errors are logged +- [ ] Users see helpful error messages (not stack traces) +- [ ] Failed auth attempts are logged for monitoring + +## Monitoring & Logging +- [ ] Authentication failures are logged +- [ ] OAuth2-proxy logs are captured and monitored +- [ ] Traefik access logs show authentication status +- [ ] Alerts configured for authentication service downtime + +## Performance +- [ ] ForwardAuth requests complete in <100ms +- [ ] Sessions are cached appropriately +- [ ] No authentication loops detected +- [ ] Load testing completed with expected user count + +## Rollback Plan +- [ ] Documentation for disabling authentication +- [ ] Backup of working configuration +- [ ] Process to revert to previous state +- [ ] Communication plan for users during issues + +## Production Monitoring +Set up monitoring dashboards: + +Authentication success/failure rates +OAuth2-proxy response times +Keycloak availability +Session cookie lifetimes +User error rates (401/403) \ No newline at end of file diff --git a/forwardauth/test/curl_for_token.sh b/forwardauth/test/curl_for_token.sh new file mode 100755 index 00000000..556c761f --- /dev/null +++ b/forwardauth/test/curl_for_token.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# shellcheck disable=SC1091 + +# shellcheck source=.env +source .env + +curl -s -X POST "${OIDC_TOKEN_URL}" \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -d 'grant_type=password' \ + -d "client_id=${OIDC_CLIENT_ID}" \ + -d "client_secret=${OIDC_CLIENT_SECRET}" \ + -d "username=${USERNAME}" \ + -d "password=${PASSWORD}" \ + -d 'scope=openid' \ No newline at end of file diff --git a/forwardauth/test/test.sh b/forwardauth/test/test.sh new file mode 100644 index 00000000..f0dc14db --- /dev/null +++ b/forwardauth/test/test.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +set -e + +echo "🔐 Testing Authentication Stack" +echo "================================" + +# Test 1: Unauthenticated request should redirect +echo -n "1. Testing unauthenticated redirect... " +REDIRECT=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/whoami") +if [ "$REDIRECT" = "302" ]; then + echo "✅ PASS (Got 302 redirect)" +else + echo "❌ FAIL (Expected 302, got $REDIRECT)" + exit 1 +fi + +# Test 2: Public endpoint should work without auth +echo -n "2. Testing public endpoint access... " +PUBLIC=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/whoami-public") +if [ "$PUBLIC" = "200" ]; then + echo "✅ PASS (Got 200 OK)" +else + echo "❌ FAIL (Expected 200, got $PUBLIC)" + exit 1 +fi + +# Test 3: Get valid token and test authenticated access +echo -n "3. Testing authenticated access... " + +ACCESS_TOKEN=$(./curl_for_token_dev.sh | jq -r '.access_token') + +if [ "$ACCESS_TOKEN" = "null" ] || [ -z "$ACCESS_TOKEN" ]; then + echo "❌ FAIL (Could not get access token)" + echo "Response: $TOKEN_RESPONSE" + exit 1 +fi + +# Simulate OAuth2 flow by setting cookie +AUTHED=$(curl -s -o /dev/null -w "%{http_code}" \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + "$BASE_URL/whoami") + +if [ "$AUTHED" = "200" ] || [ "$AUTHED" = "302" ]; then + echo "✅ PASS (Got $AUTHED)" +else + echo "❌ FAIL (Expected 200 or 302, got $AUTHED)" + exit 1 +fi + +# Test 4: OAuth2 endpoints are accessible +echo -n "4. Testing OAuth2 endpoints... " +OAUTH_AUTH=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/oauth2/auth") +if [ "$OAUTH_AUTH" = "401" ] || [ "$OAUTH_AUTH" = "403" ]; then + echo "✅ PASS (Got $OAUTH_AUTH - expected for unauthenticated)" +else + echo "⚠️ WARNING (Got $OAUTH_AUTH, expected 401/403)" +fi + +echo "" +echo "================================" +echo "✅ All tests passed!" \ No newline at end of file From 613470666b69d7270e61c9605a7f7816e11c83b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 17 Feb 2026 16:40:02 +0000 Subject: [PATCH 47/64] auth services config folder --- forwardauth/auth-app/compose.yml | 62 +++++++++++++++++++ .../oauth2-proxy/oauth2-proxy.template.cfg | 34 ++++++++++ forwardauth/auth-app/traefik/dynamic.yml | 44 +++++++++++++ forwardauth/auth-app/traefik/traefik.yml | 24 +++++++ 4 files changed, 164 insertions(+) create mode 100644 forwardauth/auth-app/compose.yml create mode 100644 forwardauth/auth-app/oauth2-proxy/oauth2-proxy.template.cfg create mode 100644 forwardauth/auth-app/traefik/dynamic.yml create mode 100644 forwardauth/auth-app/traefik/traefik.yml diff --git a/forwardauth/auth-app/compose.yml b/forwardauth/auth-app/compose.yml new file mode 100644 index 00000000..9583efae --- /dev/null +++ b/forwardauth/auth-app/compose.yml @@ -0,0 +1,62 @@ +name: proxy +services: + traefik: + image: traefik:v3.6 + container_name: standalone-traefik + ports: + - "1916:80" # web entrypoint + - "443:443" # websecure entrypoint + - "8080:8080" # dashboard + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./traefik/traefik.yml:/etc/traefik/traefik.yml:rob + - ./traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro + networks: + - bongus + environment: + - STACK_NAME=bongus + restart: unless-stopped + + oauth2-proxy: + container_name: oauth2-proxy + image: quay.io/oauth2-proxy/oauth2-proxy:v7.14.2 + command: --config /oauth2-proxy.cfg + hostname: oauth2-proxy + env_file: + - .env + volumes: + - "./oauth2-proxy/oauth2-proxy.cfg:/oauth2-proxy.cfg" + restart: unless-stopped + networks: + - bongus + + # Test service with authentication + whoami: + image: traefik/whoami + container_name: test-whoami + networks: + - bongus + labels: + - "traefik.enable=true" + - "traefik.docker.network=bongus" + - "traefik.http.routers.whoami.rule=PathPrefix(`/whoami`)" + - "traefik.http.routers.whoami.entrypoints=web" + - "traefik.http.routers.whoami.middlewares=oauth-auth-redirect@file" + - "traefik.http.services.whoami.loadbalancer.server.port=80" + + # Test service WITHOUT authentication for comparison + whoami-public: + image: traefik/whoami + container_name: test-whoami-public + networks: + - bongus + labels: + - "traefik.enable=true" + - "traefik.docker.network=bongus" + - "traefik.http.routers.whoami-public.rule=PathPrefix(`/whoami-public`)" + - "traefik.http.routers.whoami-public.entrypoints=web" + - "traefik.http.services.whoami-public.loadbalancer.server.port=80" + +networks: + bongus: + name: bongus diff --git a/forwardauth/auth-app/oauth2-proxy/oauth2-proxy.template.cfg b/forwardauth/auth-app/oauth2-proxy/oauth2-proxy.template.cfg new file mode 100644 index 00000000..4d9fdf9d --- /dev/null +++ b/forwardauth/auth-app/oauth2-proxy/oauth2-proxy.template.cfg @@ -0,0 +1,34 @@ +#traefik +reverse_proxy="true" # are we running behind a reverse proxy + +# oauth2-proxy +http_address="0.0.0.0:4180" #listen on all IPv4 interfaces + +upstreams=["static://202"] +email_domains="*" + +# Keycloak provider +provider="keycloak-oidc" +provider_display_name="Keycloak" +client_secret="xxxxxxxxxx" +client_id="xxxx" +oidc_issuer_url="xxxxxxxxxxxx" +redirect_url="xxxxxxxxxxxxxx" +scope="openid email profile groups" +code_challenge_method="S256" + +# Cookies +cookie_secret="" +cookie_secure="false" +cookie_samesite="lax" +whitelist_domains=["xxxx"] + +# Logging +request_logging="true" +auth_logging="true" +standard_logging="true" +skip_auth_strip_headers="false" + +# Headers +set_xauthrequest="true" +set_authorization_header="true" diff --git a/forwardauth/auth-app/traefik/dynamic.yml b/forwardauth/auth-app/traefik/dynamic.yml new file mode 100644 index 00000000..83ab63b9 --- /dev/null +++ b/forwardauth/auth-app/traefik/dynamic.yml @@ -0,0 +1,44 @@ +# Dynamic Traefik configuration for middlewares +http: + routers: + services-oauth2-route: + rule: "PathPrefix(`/oauth2/`)" + middlewares: + - auth-headers + service: oauth-backend + + services: + oauth-backend: + loadBalancer: + servers: + - url: http://oauth2-proxy:4180 + + middlewares: + auth-headers: + headers: + sslRedirect: false + stsSeconds: 315360000 + browserXssFilter: true + contentTypeNosniff: true + forceSTSHeader: true + stsIncludeSubdomains: true + stsPreload: true + frameDeny: true + oauth-auth-redirect: + forwardAuth: + address: http://oauth2-proxy:4180/ + trustForwardHeader: true + authResponseHeaders: + - X-Auth-Request-User + - X-Auth-Request-Email + - X-Auth-Request-Access-Token + - Authorization + authRequestHeaders: + - Authorization + oauth-auth-wo-redirect: + forwardAuth: + address: http://oauth2-proxy:4180/oauth2/auth + trustForwardHeader: true + authResponseHeaders: + - X-Auth-Request-Access-Token + - Authorization diff --git a/forwardauth/auth-app/traefik/traefik.yml b/forwardauth/auth-app/traefik/traefik.yml new file mode 100644 index 00000000..ee6038df --- /dev/null +++ b/forwardauth/auth-app/traefik/traefik.yml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://json.schemastore.org/traefik-v3.json +api: + dashboard: true + insecure: true + +entryPoints: + web: + address: ":80" + websecure: + address: ":443" + traefik: + address: ":8080" + +providers: + docker: + endpoint: "unix:///var/run/docker.sock" + exposedByDefault: false + network: "bingus" + file: + filename: "/etc/traefik/dynamic.yml" + watch: true + +log: + level: DEBUG From 7dc618c100d05a93cf91c9ffd404e369ab577ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 17 Feb 2026 16:43:22 +0000 Subject: [PATCH 48/64] ignore logs and sensitive files --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 28c97c91..9df4afb1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,8 @@ testing_temp/ **/target/ WEB-INF/ log*.xml -port.txt \ No newline at end of file +port.txt +*.env +forwardauth/.env +forwardauth/auth-app/oauth2-proxy/oauth2-proxy.cfg +*.log \ No newline at end of file From 7b29c56e2fea894b8d32c4bbea7c34d6e86a0097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Tue, 17 Feb 2026 16:44:58 +0000 Subject: [PATCH 49/64] tidy up alternate configs --- forwardauth/compose.yml | 28 ----- forwardauth/mesosphere/compose.yml | 157 +++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 28 deletions(-) delete mode 100644 forwardauth/compose.yml create mode 100644 forwardauth/mesosphere/compose.yml diff --git a/forwardauth/compose.yml b/forwardauth/compose.yml deleted file mode 100644 index 4e64cd19..00000000 --- a/forwardauth/compose.yml +++ /dev/null @@ -1,28 +0,0 @@ -services: - forwardauth: - image: mesosphere/traefik-forward-auth - networks: - - stack - environment: - - SECRET=${SIGNING_SECRET} - - PROVIDER_URI=${PROVIDER_URI} - - CLIENT_ID=${CLIENT_ID} - - CLIENT_SECRET=${CLIENT_SECRET} - - ENCRYPTION_KEY=${ENCRYPTION_KEY} - labels: - - "traefik.enable=true" - - "traefik.http.services.forwardauth.loadbalancer.server.port=4181" - - "traefik.http.routers.forwardauth.entrypoints=web" - # Router for OAuth callback endpoint - middleware will detect and process callback - - - "traefik.http.routers.forwardauth.rule=Path(`/_oauth`)" - # Middleware definition used by other services - - "traefik.http.middlewares.traefik-forward-auth.forwardauth.address=http://forwardauth:4181" - - "traefik.http.middlewares.traefik-forward-auth.forwardauth.authResponseHeaders=X-Forwarded-User" - - "traefik.http.middlewares.traefik-forward-auth.forwardauth.trustForwardHeader=true" - -networks: - stack: - name: ${STACK_NAME} - driver: overlay - external: true diff --git a/forwardauth/mesosphere/compose.yml b/forwardauth/mesosphere/compose.yml new file mode 100644 index 00000000..922a51f5 --- /dev/null +++ b/forwardauth/mesosphere/compose.yml @@ -0,0 +1,157 @@ +secrets: + cloudflare-token: + file: "./secrets/cloudflare-token.secret" + cloudflare-email: + file: "./secrets/cloudflare-email.secret" + +services: + traefik: + image: "traefik:v2.11" + restart: always + container_name: "traefik" + networks: + - mynetwork + command: + - "--log.level=DEBUG" + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --api.dashboard=true + # Set up LetsEncrypt certificate resolver + - --certificatesresolvers.letsencrypt.acme.dnschallenge=true + - --certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare + - --certificatesResolvers.letsencrypt.acme.dnschallenge.resolvers=1.1.1.1:53,1.0.0.1:53 + - --certificatesResolvers.letsencrypt.acme.dnschallenge.delayBeforeCheck=20 + - --certificatesresolvers.letsencrypt.acme.email=youremail@gmail.com # CHANGE HERE + - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json + # staging environment of LE, remove for real certs + #- --certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory - this is the staging url + - --certificatesresolvers.letsencrypt.acme.caserver=https://acme-v02.api.letsencrypt.org/directory + # Set up an insecure listener that redirects all traffic to TLS + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.web.http.redirections.entrypoint.scheme=https + # - --entrypoints.websecure.http.middlewares=traefik-forward-auth + # Set up the TLS configuration for our websecure listener + - --entrypoints.websecure.http.tls=true + - --entrypoints.websecure.http.tls.certResolver=letsencrypt + - --entrypoints.websecure.http.tls.domains[0].main=yourdomain.org # CHANGE HERE + - --entrypoints.websecure.http.tls.domains[0].sans=*.yourdomain.org # CHANGE HERE + secrets: + - "cloudflare-token" + - "cloudflare-email" + environment: + - "CF_DNS_API_TOKEN_FILE=/run/secrets/cloudflare-token" + - "CF_API_EMAIL_FILE=/run/secrets/cloudflare-email" + ports: + - "80:80" + - "443:443" + volumes: + - "./certs:/letsencrypt" # create folder certs + - "/var/run/docker.sock:/var/run/docker.sock:ro" + labels: + - "traefik.enable=true" + - "traefik.http.routers.traefik.rule=Host(`traefik.dev.yourdomain.org`)" # CHANGE HERE + - "traefik.http.routers.traefik.entrypoints=websecure" + - "traefik.http.routers.traefik.service=api@internal" + - "traefik.http.routers.traefik.tls.certresolver=letsencrypt" + - "traefik.http.routers.traefik.middlewares=traefik-auth" + - "traefik.http.middlewares.traefik-auth.basicauth.users=user:{SHA}EtfyZU+wgWTV563pLJlJWWgyzDSck=" # user is the login name after ":"here i just use an sha coded password so that traefik dashboard is secured as well, you can just delete this if you want or encrypt one password online + + paste-bin: + container_name: paste-bin + image: ghcr.io/enchant97/hasty-paste:latest + restart: unless-stopped + depends_on: + - traefik-forward-auth + networks: + - mynetwork + labels: + - "traefik.enable=true" + - "traefik.http.routers.paste.rule=Host(`paste.dev.yourdomain.org`)" # CHANGE HERE + - "traefik.http.routers.paste.entrypoints=websecure" + - "traefik.http.routers.paste.tls.certresolver=letsencrypt" + - "traefik.http.routers.paste.middlewares=forwardauth" # Add this line to any image to make it use forwardauth + + traefik-forward-auth: + image: mesosphere/traefik-forward-auth + container_name: traefik-forward-auth + restart: on-failure + depends_on: + - traefik + - keycloak + environment: + - TZ=Europe/Berlin + - SECRET=fd92459cd7ygafc2df466sdg3747z43ce0d # CHANGE HERE, random secret + - PROVIDER_URI=https://keycloak.dev.yourdomain.org/realms/myrealm # CHANGE HERE , myrealm is a realm i created in keyclak + - CLIENT_ID=my-client # CHANGE HERE + - CLIENT_SECRET=wueZ37IOE42Sv3XVqJyhrLX3WIk4y # CHANGE HERE + - COOKIE_DOMAIN=https://dev.yourdomain.org + - DISABLE_SSL_VERIFICATION=true # might be unnecessary + - INSECURE_COOKIE=1 + - ENCRYPTION_KEY=7347373954253633845947936 # CHANGE HERE, random key + - SCOPE=profile email openid # this is a must!! + networks: + - mynetwork + labels: + - "traefik.enable=true" + - "traefik.docker.network=web" + - "traefik.http.services.traefik-forward-auth.loadbalancer.server.port=4181" + - "traefik.http.routers.traefik-forward-auth.entrypoints=websecure" + - "traefik.http.routers.traefik-forward-auth.middlewares=forwardauth" + - "traefik.http.middlewares.forwardauth.forwardauth.address=http://traefik-forward-auth:4181" + - "traefik.http.middlewares.forwardauth.forwardauth.authResponseHeaders=X-Forwarded-User" + - "traefik.http.middlewares.forwardauth.forwardauth.trustForwardHeader=true" + + keycloakdb: + image: postgres:16.2-alpine + container_name: keycloakdb + environment: + - POSTGRES_DB=keycloak + - POSTGRES_USER=keycloak + - POSTGRES_PASSWORD=password + - POSTGRES_ROOT_PASSWORD=password + networks: + - mynetwork + ports: + - "5432:5432" + volumes: + - keycloakdata:/var/lib/postgresql/data + labels: + - "traefik.enable=false" + + keycloak: + image: quay.io/keycloak/keycloak:24.0 + container_name: keycloak + hostname: keycloak + environment: + - KC_HOSTNAME_STRICT=false + - KC_DB=postgres + - KC_DB_URL=jdbc:postgresql://keycloakdb/keycloak + - KC_DB_URL_PORT=5432 + - KC_DB_USERNAME=keycloak + - KC_DB_PASSWORD=password + - KC_DB_SCHEMA=public + - KC_LOG_LEVEL=info + - KC_FEATURES=docker + - KEYCLOAK_ADMIN=admin + - KEYCLOAK_ADMIN_PASSWORD=password + - KC_PROXY=edge + networks: + - mynetwork + depends_on: + - traefik + - keycloakdb + labels: + - "traefik.enable=true" + - "traefik.http.routers.keycloak.rule=Host(`keycloak.dev.yourdomain.org`)" # CHANGE HERE + - "traefik.http.routers.keycloak.entrypoints=websecure" + - "traefik.http.routers.keycloak.tls.certresolver=letsencrypt" + entrypoint: ["/opt/keycloak/bin/kc.sh", "start-dev"] + +networks: + mynetwork: + driver: bridge + +volumes: + keycloakdata: From 8cae2fde341e3063addfaa560c8251eebd7ac3fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Thu, 19 Feb 2026 16:09:07 +0000 Subject: [PATCH 50/64] deploy script --- forwardauth/auth-app/deploy/README.md | 195 +++++++++++++++++ forwardauth/auth-app/deploy/deploy.sh | 289 ++++++++++++++++++++++++++ 2 files changed, 484 insertions(+) create mode 100644 forwardauth/auth-app/deploy/README.md create mode 100755 forwardauth/auth-app/deploy/deploy.sh diff --git a/forwardauth/auth-app/deploy/README.md b/forwardauth/auth-app/deploy/README.md new file mode 100644 index 00000000..7025cc77 --- /dev/null +++ b/forwardauth/auth-app/deploy/README.md @@ -0,0 +1,195 @@ +# Traefik/OAuth2-Proxy Deployment Script + +This script automates the deployment of the Traefik reverse proxy with OAuth2-Proxy authentication across multiple CReDO hosts. + +## Supported Hosts + +| Host | User | Traefik Port | Nginx Port | FQDN | +|------|------|--------------|------------|------| +| ci01.credo | shared | 9050 | 8050 | ci01.credo | +| cd01.credo | cadent | 9051 | 8051 | cd01.credo | +| cd02.credo | ngt | 9051 | 8051 | cd02.credo | +| cd03.credo | shared | 9050 | 8050 | cd03.credo | + +The script automatically detects the hostname and maps: +- `credo-integration-01` → `ci01.credo` +- `credo-datahost-01` → `cd01.credo` +- `credo-datahost-02` → `cd02.credo` +- `credo-datahost-03` → `cd03.credo` + +## Prerequisites + +- Podman and podman-compose installed +- Access to the Keycloak admin console to retrieve/configure client secrets +- Appropriate user permissions on the target host + +## Usage + +### 1. Basic Deployment + +Run the script without arguments to auto-detect the host, or specify a short hostname: + +```bash +# Auto-detect current host from $(hostname) +./deploy.sh + +# Or specify host explicitly +./deploy.sh ci01.credo +``` + +The script will automatically map full hostnames to short names: +- On `credo-integration-01`: auto-detects as `ci01.credo` +- On `credo-datahost-01`: auto-detects as `cd01.credo` +- On `credo-datahost-02`: auto-detects as `cd02.credo` +- On `credo-datahost-03`: auto-detects as `cd03.credo` + +Run as the appropriate user for the host: + +```bash +# Auto-detect current host +sudo -u ./deploy.sh + +# Or specify host explicitly +sudo -u ./deploy.sh ci01.credo +``` + +Examples for each host: +```bash +# ci01.credo (auto-detected from credo-integration-01) +sudo -u shared ./deploy.sh + +# cd01.credo (auto-detected from credo-datahost-01) +sudo -u cadent ./deploy.sh + +# cd02.credo (auto-detected from credo-datahost-02) +sudo -u ngt ./deploy.sh + +# cd03.credo (auto-detected from credo-datahost-03) +sudo -u shared ./deploy.sh +``` + +### 2. First-Time Setup + +On first deployment, you'll be prompted for the Keycloak client secret: + +``` +Enter Keycloak client secret for 'traefik' (or press Enter to generate random): +``` + +**Important:** If you press Enter and a random secret is generated, you **must** configure this secret in Keycloak: +1. Log into Keycloak admin console at https://idm-credo.hartree.app +2. Navigate to: Realms → CReDO → Clients → traefik +3. Go to Credentials tab +4. Set the Client Secret to match the generated value (shown in `.env` file) + +### 3. What the Script Does + +1. **Detects/validates** the current host +2. **Generates or reads secrets** for OAuth2 configuration +3. **Creates `.env` file** with: + - Keycloak client credentials + - Generated security secrets + - Host-specific configuration +4. **Generates `oauth2-proxy.cfg`** from template with: + - Correct hostname/FQDN + - Keycloak connection details + - Cookie and JWT settings +5. **Updates `compose.yml`** to expose correct Traefik port +6. **Updates `traefik/dynamic.yml`** to proxy to correct Nginx port +7. **Stops any existing containers** +8. **Starts the stack** using podman-compose + +## Configuration Files + +After running the script, the following files will be created/updated: + +- **`.env`** - Environment variables with secrets and configuration +- **`oauth2-proxy/oauth2-proxy.cfg`** - OAuth2-Proxy configuration +- **`compose.yml`** - Updated with correct Traefik port +- **`traefik/dynamic.yml`** - Updated with correct Nginx port + +## Accessing the Services + +After deployment: + +- **Traefik Dashboard:** http://localhost:8080 +- **Traefik Entry Point:** http://localhost:[TRAEFIK_PORT] +- **Protected Test Service:** http://localhost:[TRAEFIK_PORT]/whoami +- **Public Test Service:** http://localhost:[TRAEFIK_PORT]/whoami-public +- **External Access:** https://[FQDN] + +## Managing the Stack + +### View logs +```bash +cd /home/opeppard/repositories/stack/forwardauth/auth-app +podman-compose logs -f +``` + +### Check container status +```bash +podman ps +``` + +### Stop the stack +```bash +podman-compose down +``` + +### Restart the stack +```bash +podman-compose restart +``` + +### Full redeployment +```bash +./deploy/deploy.sh [hostname] +``` + +## Troubleshooting + +### Containers not starting +1. Check podman logs: `podman-compose logs` +2. Verify ports are not already in use: `ss -tlnp | grep [PORT]` +3. Check SELinux context if applicable +4. Verify XDG_RUNTIME_DIR is set correctly + +### Authentication not working +1. Verify Keycloak client secret matches in both Keycloak and `.env` +2. Check redirect URI in Keycloak matches the FQDN +3. Review oauth2-proxy logs: `podman-compose logs oauth2-proxy` +4. Ensure the FQDN is accessible and resolves correctly + +### Permission issues +1. Ensure you're running as the correct user for the host +2. Check podman socket permissions +3. Verify file ownership in the deployment directory + +## Security Notes + +- The `.env` file contains sensitive secrets - keep it secure and don't commit to git +- Cookie secrets are used for session management +- Client secrets should be obtained from Keycloak admin +- All traffic should use HTTPS in production (configure in Traefik) + +## Network Configuration + +The stack creates/uses a network named `bongus` for inter-container communication. +All services should be on this network to communicate properly. + +## Keycloak Configuration + +Ensure the following settings in Keycloak for the `traefik` client: + +- **Client ID:** traefik +- **Access Type:** confidential +- **Valid Redirect URIs:** + - https://ci01.credo/* + - https://cd01.credo/* + - https://cd02.credo/* + - https://cd03.credo/* + - http://localhost:[TRAEFIK_PORT]/* +- **Web Origins:** + +- **Client Protocol:** openid-connect + +Note: The FQDNs now use the short hostnames from `/etc/hosts` (e.g., `ci01.credo`) rather than the full DAFNI domain names. diff --git a/forwardauth/auth-app/deploy/deploy.sh b/forwardauth/auth-app/deploy/deploy.sh new file mode 100755 index 00000000..62d95cda --- /dev/null +++ b/forwardauth/auth-app/deploy/deploy.sh @@ -0,0 +1,289 @@ +#!/bin/bash + +set -e + +# Script to deploy Traefik/OAuth2-Proxy stack on different CReDO hosts +# Usage: ./deploy.sh [hostname] + +# Color codes for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BASE_DIR="$(dirname "$SCRIPT_DIR")" + +# Function to print colored messages +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Host configuration table +declare -A HOST_USER +declare -A HOST_TRAEFIK_PORT +declare -A HOST_NGINX_PORT +declare -A HOST_FQDN +declare -A HOSTNAME_TO_SHORT + +# Mapping from full hostname to short name +HOSTNAME_TO_SHORT["credo-integration-01"]="ci01.credo" +HOSTNAME_TO_SHORT["credo-datahost-01"]="cd01.credo" +HOSTNAME_TO_SHORT["credo-datahost-02"]="cd02.credo" +HOSTNAME_TO_SHORT["credo-datahost-03"]="cd03.credo" + +# ci01.credo configuration +HOST_USER["ci01.credo"]="shared" +HOST_TRAEFIK_PORT["ci01.credo"]="9050" +HOST_NGINX_PORT["ci01.credo"]="8050" +HOST_FQDN["ci01.credo"]="ci01.credo" + +# cd01.credo configuration +HOST_USER["cd01.credo"]="cadent" +HOST_TRAEFIK_PORT["cd01.credo"]="9051" +HOST_NGINX_PORT["cd01.credo"]="8051" +HOST_FQDN["cd01.credo"]="cd01.credo" + +# cd02.credo configuration +HOST_USER["cd02.credo"]="ngt" +HOST_TRAEFIK_PORT["cd02.credo"]="9051" +HOST_NGINX_PORT["cd02.credo"]="8051" +HOST_FQDN["cd02.credo"]="cd02.credo" + +# cd03.credo configuration +HOST_USER["cd03.credo"]="shared" +HOST_TRAEFIK_PORT["cd03.credo"]="9050" +HOST_NGINX_PORT["cd03.credo"]="8050" +HOST_FQDN["cd03.credo"]="cd03.credo" + +# Keycloak configuration (same for all hosts) +KEYCLOAK_REALM="CReDO" +KEYCLOAK_URL="https://idm-credo.hartree.app/realms/${KEYCLOAK_REALM}" +KEYCLOAK_CLIENT_ID="traefik" + +# Detect current host or use provided argument +if [ -n "$1" ]; then + CURRENT_HOST="$1" +else + # Get the full hostname and map to short name + FULL_HOSTNAME=$(hostname) + CURRENT_HOST="${HOSTNAME_TO_SHORT[$FULL_HOSTNAME]}" + + # If not found in mapping, try using hostname directly + if [ -z "$CURRENT_HOST" ]; then + CURRENT_HOST="$FULL_HOSTNAME" + fi +fi + +log_info "Deploying for host: $CURRENT_HOST" + +# Validate host +if [ -z "${HOST_USER[$CURRENT_HOST]}" ]; then + log_error "Unknown host: $CURRENT_HOST" + log_error "Valid hosts: ci01.credo, cd01.credo, cd02.credo, cd03.credo" + exit 1 +fi + +# Get configuration for current host +DEPLOY_USER="${HOST_USER[$CURRENT_HOST]}" +TRAEFIK_PORT="${HOST_TRAEFIK_PORT[$CURRENT_HOST]}" +NGINX_PORT="${HOST_NGINX_PORT[$CURRENT_HOST]}" +FQDN="${HOST_FQDN[$CURRENT_HOST]}" +STACK_NAME="${CURRENT_HOST//./-}-stack" + +log_info "Configuration:" +log_info " User: $DEPLOY_USER" +log_info " Traefik Port: $TRAEFIK_PORT" +log_info " Nginx Port: $NGINX_PORT" +log_info " FQDN: $FQDN" +log_info " Stack Name: $STACK_NAME" + +# Function to generate random string for secrets +generate_secret() { + local length=${1:-32} + openssl rand -hex "$length" | head -c "$length" +} + +# Check if running as correct user +CURRENT_USER=$(whoami) +if [ "$CURRENT_USER" != "$DEPLOY_USER" ] && [ "$CURRENT_USER" != "root" ]; then + log_warn "Current user ($CURRENT_USER) is not the deployment user ($DEPLOY_USER)" + log_warn "Consider running as: sudo -u $DEPLOY_USER $0 $CURRENT_HOST" +fi + +# Read or generate secrets +log_info "Setting up secrets..." + +# Try to read existing client secret from .env if it exists +if [ -f "$BASE_DIR/.env" ]; then + log_info "Reading existing .env file for secrets..." + source "$BASE_DIR/.env" 2>/dev/null || true +fi + +# If CLIENT_SECRET is not set, prompt for it +if [ -z "$CLIENT_SECRET" ]; then + log_warn "CLIENT_SECRET not found in existing .env" + read -rsp "Enter Keycloak client secret for '$KEYCLOAK_CLIENT_ID' (or press Enter to generate random): " CLIENT_SECRET + echo + if [ -z "$CLIENT_SECRET" ]; then + log_warn "Generating random CLIENT_SECRET (you'll need to configure this in Keycloak)" + CLIENT_SECRET=$(generate_secret 32) + fi +fi + +# Generate other secrets if not present +if [ -z "$ENCRYPTION_KEY" ]; then + ENCRYPTION_KEY=$(generate_secret 32) +fi + +if [ -z "$SIGNING_SECRET" ]; then + SIGNING_SECRET=$(generate_secret 32) +fi + +if [ -z "$COOKIE_SECRET" ]; then + COOKIE_SECRET=$(generate_secret 32) +fi + +# Create .env file +log_info "Writing .env file..." +cat > "$BASE_DIR/.env" << EOF +# Keycloak OAuth2 Configuration +CLIENT_ID=$KEYCLOAK_CLIENT_ID +CLIENT_SECRET=$CLIENT_SECRET +PROVIDER_URI=$KEYCLOAK_URL + +# Security Secrets +ENCRYPTION_KEY=$ENCRYPTION_KEY +SIGNING_SECRET=$SIGNING_SECRET +COOKIE_SECRET=$COOKIE_SECRET + +# Stack Configuration +STACK_NAME=$STACK_NAME +HOST_FQDN=$FQDN + +# Port Configuration +TRAEFIK_PORT=$TRAEFIK_PORT +NGINX_PORT=$NGINX_PORT +EOF + +log_info ".env file created" + +# Create oauth2-proxy.cfg from template +log_info "Generating oauth2-proxy.cfg..." +cat > "$BASE_DIR/oauth2-proxy/oauth2-proxy.cfg" << EOF +#traefik +reverse_proxy="true" # are we running behind a reverse proxy + +# oauth2-proxy +http_address="0.0.0.0:4180" #listen on all IPv4 interfaces + +upstreams=["static://202"] +email_domains="*" + +# Keycloak provider +provider="keycloak-oidc" +provider_display_name="CReDO Keycloak" +client_secret="$CLIENT_SECRET" +client_id="$KEYCLOAK_CLIENT_ID" +oidc_issuer_url="$KEYCLOAK_URL" +redirect_url="$FQDN" +scope="openid email profile groups" +code_challenge_method="S256" +insecure_oidc_allow_unverified_email="true" + +# Cookies +cookie_secret="$COOKIE_SECRET" +cookie_secure="false" +cookie_samesite="lax" +whitelist_domains=["$FQDN","localhost:$TRAEFIK_PORT","127.0.0.1:$TRAEFIK_PORT"] +skip_jwt_bearer_tokens="true" +extra_jwt_issuers=["$KEYCLOAK_URL=$KEYCLOAK_CLIENT_ID"] + +# Logging +request_logging="true" +auth_logging="true" +standard_logging="true" +skip_auth_strip_headers="false" + +# Headers +set_xauthrequest="true" +set_authorization_header="true" +EOF + +log_info "oauth2-proxy.cfg created" + +# Update compose.yml with correct Traefik port +log_info "Updating compose.yml with Traefik port..." +sed -i.bak "s/\"[0-9]*:80\"/\"$TRAEFIK_PORT:80\"/" "$BASE_DIR/compose.yml" +log_info "compose.yml updated (Traefik port: $TRAEFIK_PORT:80)" + +# Update dynamic.yml with correct Nginx port +log_info "Updating dynamic.yml with Nginx port..." +sed -i.bak "s|http://host.containers.internal:[0-9]*|http://host.containers.internal:$NGINX_PORT|" "$BASE_DIR/traefik/dynamic.yml" +log_info "dynamic.yml updated (Nginx port: $NGINX_PORT)" + +# Set XDG_RUNTIME_DIR for podman if not set +if [ -z "$XDG_RUNTIME_DIR" ]; then + export XDG_RUNTIME_DIR="/run/user/$(id -u)" +fi + +log_info "XDG_RUNTIME_DIR: $XDG_RUNTIME_DIR" + +# Check if podman-compose is available +if ! command -v podman-compose &> /dev/null; then + log_error "podman-compose not found. Please install it first." + exit 1 +fi + +# Stop existing containers if running +log_info "Stopping existing containers..." +cd "$BASE_DIR" +podman-compose down 2>/dev/null || log_warn "No existing containers to stop" + +# Start the stack +log_info "Starting stack..." +podman-compose up -d + +# Check if containers started successfully +sleep 3 +if podman ps | grep -q "standalone-traefik"; then + log_info "${GREEN}✓${NC} Traefik container is running" +else + log_error "Traefik container failed to start" +fi + +if podman ps | grep -q "oauth2-proxy"; then + log_info "${GREEN}✓${NC} OAuth2-Proxy container is running" +else + log_error "OAuth2-Proxy container failed to start" +fi + +# Display access information +log_info "" +log_info "==========================================" +log_info "Deployment Complete!" +log_info "==========================================" +log_info "Traefik Dashboard: http://localhost:8080" +log_info "Traefik Entry: http://localhost:$TRAEFIK_PORT" +log_info "Protected Service: http://localhost:$TRAEFIK_PORT/whoami" +log_info "Public Service: http://localhost:$TRAEFIK_PORT/whoami-public" +log_info "" +log_info "External Access: https://$FQDN" +log_info "" +log_info "Stack Name: $STACK_NAME" +log_info "==========================================" +log_info "" +log_info "To view logs:" +log_info " podman-compose logs -f" +log_info "" +log_info "To stop the stack:" +log_info " podman-compose down" From a9225f5bacb4d5626c2c5dfd52b1e61907564dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Thu, 19 Feb 2026 16:28:00 +0000 Subject: [PATCH 51/64] update hostnames --- forwardauth/auth-app/deploy/deploy.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/forwardauth/auth-app/deploy/deploy.sh b/forwardauth/auth-app/deploy/deploy.sh index 62d95cda..3db754d1 100755 --- a/forwardauth/auth-app/deploy/deploy.sh +++ b/forwardauth/auth-app/deploy/deploy.sh @@ -37,9 +37,13 @@ declare -A HOSTNAME_TO_SHORT # Mapping from full hostname to short name HOSTNAME_TO_SHORT["credo-integration-01"]="ci01.credo" +HOSTNAME_TO_SHORT["credo-integration-01.dafni.rl.ac.uk"]="ci01.credo" HOSTNAME_TO_SHORT["credo-datahost-01"]="cd01.credo" +HOSTNAME_TO_SHORT["credo-datahost-01.dafni.rl.ac.uk"]="cd01.credo" HOSTNAME_TO_SHORT["credo-datahost-02"]="cd02.credo" +HOSTNAME_TO_SHORT["credo-datahost-02.dafni.rl.ac.uk"]="cd02.credo" HOSTNAME_TO_SHORT["credo-datahost-03"]="cd03.credo" +HOSTNAME_TO_SHORT["credo-datahost-03.dafni.rl.ac.uk"]="cd03.credo" # ci01.credo configuration HOST_USER["ci01.credo"]="shared" From 93227f53a7a27cb654d40b2059218918cbb17fd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Thu, 19 Feb 2026 16:37:45 +0000 Subject: [PATCH 52/64] fix compose file for SELinux --- forwardauth/auth-app/compose.yml | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/forwardauth/auth-app/compose.yml b/forwardauth/auth-app/compose.yml index 9583efae..1cffaa1b 100644 --- a/forwardauth/auth-app/compose.yml +++ b/forwardauth/auth-app/compose.yml @@ -2,21 +2,29 @@ name: proxy services: traefik: image: traefik:v3.6 - container_name: standalone-traefik + container_name: standalone-traefik-op ports: - - "1916:80" # web entrypoint - - "443:443" # websecure entrypoint + - "2025:80" # web entrypoint + # - "443:443" # websecure entrypoint - "8080:8080" # dashboard volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - - ./traefik/traefik.yml:/etc/traefik/traefik.yml:rob - - ./traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro + # docker + # - /var/run/docker.sock:/var/run/docker.sock:ro + # - ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro + # - ./traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro + + # podman and SELinux + - ${XDG_RUNTIME_DIR}/podman/podman.sock:/var/run/docker.sock:ro + - ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro,Z + - ./traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro,Z networks: - bongus environment: - STACK_NAME=bongus + # podman + security_opt: + - label=disable restart: unless-stopped - oauth2-proxy: container_name: oauth2-proxy image: quay.io/oauth2-proxy/oauth2-proxy:v7.14.2 @@ -25,7 +33,7 @@ services: env_file: - .env volumes: - - "./oauth2-proxy/oauth2-proxy.cfg:/oauth2-proxy.cfg" + - "./oauth2-proxy/oauth2-proxy.cfg:/oauth2-proxy.cfg:ro,Z" restart: unless-stopped networks: - bongus From 6bfa79c6d009795f784085d73357f142d1e5b36b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Thu, 19 Feb 2026 16:39:21 +0000 Subject: [PATCH 53/64] dynamic schema --- forwardauth/auth-app/traefik/dynamic.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/forwardauth/auth-app/traefik/dynamic.yml b/forwardauth/auth-app/traefik/dynamic.yml index 83ab63b9..cf5f74f2 100644 --- a/forwardauth/auth-app/traefik/dynamic.yml +++ b/forwardauth/auth-app/traefik/dynamic.yml @@ -1,4 +1,6 @@ # Dynamic Traefik configuration for middlewares +# $schema: https://www.schemastore.org/traefik-v3-file-provider.json + http: routers: services-oauth2-route: From a25c83c878ff7a04bfa093da0af9ce9b3340ec9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Thu, 19 Feb 2026 16:39:37 +0000 Subject: [PATCH 54/64] map to nginx on credo hosts --- forwardauth/auth-app/traefik/dynamic.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/forwardauth/auth-app/traefik/dynamic.yml b/forwardauth/auth-app/traefik/dynamic.yml index cf5f74f2..d4899dae 100644 --- a/forwardauth/auth-app/traefik/dynamic.yml +++ b/forwardauth/auth-app/traefik/dynamic.yml @@ -8,12 +8,21 @@ http: middlewares: - auth-headers service: oauth-backend - + nginx-route: + rule: "PathPrefix(`/`)" + middlewares: + - oauth-auth-redirect + service: nginx-backend + priority: 1 services: oauth-backend: loadBalancer: servers: - url: http://oauth2-proxy:4180 + nginx-backend: + loadBalancer: + servers: + - url: http://host.containers.internal:9050 # nginx port goes here middlewares: auth-headers: From 093b03f63dd7c3abc851211bf37fe69c7b87d3a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Thu, 19 Feb 2026 16:40:55 +0000 Subject: [PATCH 55/64] fix test scripts --- forwardauth/.env | 6 ------ forwardauth/test/authenticated_curl_through_traefik.sh | 4 ++-- forwardauth/test/curl_for_token.sh | 4 +--- 3 files changed, 3 insertions(+), 11 deletions(-) delete mode 100644 forwardauth/.env diff --git a/forwardauth/.env b/forwardauth/.env deleted file mode 100644 index d4e8641e..00000000 --- a/forwardauth/.env +++ /dev/null @@ -1,6 +0,0 @@ -ENCRYPTION_KEY=f57d73478dedc596cf41679568c62986 -CLIENT_SECRET=7awzFYIlK3io3ipSuJd07aXxShp9p9Vp -CLIENT_ID=bingus -PROVIDER_URI=https://dev.theworldavatar.io/realms/twa-test -SIGNING_SECRET=security_is_hard -STACK_NAME=bingus \ No newline at end of file diff --git a/forwardauth/test/authenticated_curl_through_traefik.sh b/forwardauth/test/authenticated_curl_through_traefik.sh index c0f251fe..c8b1fcc4 100755 --- a/forwardauth/test/authenticated_curl_through_traefik.sh +++ b/forwardauth/test/authenticated_curl_through_traefik.sh @@ -1,5 +1,5 @@ #!/usr/bin/bash -TOKEN=$(./curl_for_token_dev.sh | jq -r '.access_token') +TOKEN=$(./curl_for_token.sh | jq -r '.access_token') -curl -v -H "Authorization: Bearer $TOKEN" http://localhost:1916/whoami/ \ No newline at end of file +curl -v -H "Authorization: Bearer $TOKEN" http://localhost:2025/whoami/ \ No newline at end of file diff --git a/forwardauth/test/curl_for_token.sh b/forwardauth/test/curl_for_token.sh index 556c761f..29f63975 100755 --- a/forwardauth/test/curl_for_token.sh +++ b/forwardauth/test/curl_for_token.sh @@ -1,7 +1,5 @@ #!/bin/bash -# shellcheck disable=SC1091 - # shellcheck source=.env source .env @@ -12,4 +10,4 @@ curl -s -X POST "${OIDC_TOKEN_URL}" \ -d "client_secret=${OIDC_CLIENT_SECRET}" \ -d "username=${USERNAME}" \ -d "password=${PASSWORD}" \ - -d 'scope=openid' \ No newline at end of file + -d 'scope=openid' \ No newline at end of file From 852996ab4874c3107bb477c8c8cad84eb70c29e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 20 Feb 2026 10:29:14 +0000 Subject: [PATCH 56/64] update deploy script and doc --- forwardauth/auth-app/deploy/README.md | 100 ++++++-------------------- forwardauth/auth-app/deploy/deploy.sh | 25 ++++--- 2 files changed, 38 insertions(+), 87 deletions(-) diff --git a/forwardauth/auth-app/deploy/README.md b/forwardauth/auth-app/deploy/README.md index 7025cc77..b7c14060 100644 --- a/forwardauth/auth-app/deploy/README.md +++ b/forwardauth/auth-app/deploy/README.md @@ -1,15 +1,15 @@ -# Traefik/OAuth2-Proxy Deployment Script +# Traefik/OAuth2-Proxy Deployment on credo hosts This script automates the deployment of the Traefik reverse proxy with OAuth2-Proxy authentication across multiple CReDO hosts. ## Supported Hosts -| Host | User | Traefik Port | Nginx Port | FQDN | +| Host | User | Traefik Port | Nginx Port | Short hostname | |------|------|--------------|------------|------| -| ci01.credo | shared | 9050 | 8050 | ci01.credo | -| cd01.credo | cadent | 9051 | 8051 | cd01.credo | -| cd02.credo | ngt | 9051 | 8051 | cd02.credo | -| cd03.credo | shared | 9050 | 8050 | cd03.credo | +| credo-integration-01.dafni.rl.ac.uk | shared | 9050 | 8050 | ci01.credo | +| credo-datahost-01.dafni.rl.ac.uk | cadent | 9051 | 8051 | cd01.credo | +| credo-datahost-02.dafni.rl.ac.uk | ngt | 9051 | 8051 | cd02.credo | +| credo-datahost-03.dafni.rl.ac.uk | shared | 9050 | 8050 | cd03.credo | The script automatically detects the hostname and maps: - `credo-integration-01` → `ci01.credo` @@ -33,8 +33,6 @@ Run the script without arguments to auto-detect the host, or specify a short hos # Auto-detect current host from $(hostname) ./deploy.sh -# Or specify host explicitly -./deploy.sh ci01.credo ``` The script will automatically map full hostnames to short names: @@ -48,11 +46,24 @@ Run as the appropriate user for the host: ```bash # Auto-detect current host sudo -u ./deploy.sh +``` + + +```bash +# ci01.credo (auto-detected from credo-integration-01) +sudo -u shared bash -c 'cd ~ && rm -rf stack && git clone https://github.com/TheWorldAvatar/stack.git && cd stack && git checkout add-traefik-support && cd forwardauth/auth-app && ./deploy/deploy.sh' + +# cd01.credo (auto-detected from credo-datahost-01) +sudo -u cadent bash -c 'cd ~ && rm -rf stack && git clone https://github.com/TheWorldAvatar/stack.git && cd stack && git checkout add-traefik-support && cd forwardauth/auth-app && ./deploy/deploy.sh' + +# cd02.credo (auto-detected from credo-datahost-02) +sudo -u ngt bash -c 'cd ~ && rm -rf stack && git clone https://github.com/TheWorldAvatar/stack.git && cd stack && git checkout add-traefik-support && cd forwardauth/auth-app && ./deploy/deploy.sh' -# Or specify host explicitly -sudo -u ./deploy.sh ci01.credo +# cd03.credo (auto-detected from credo-datahost-03) +sudo -u shared bash -c 'cd ~ && rm -rf stack && git clone https://github.com/TheWorldAvatar/stack.git && cd stack && git checkout add-traefik-support && cd forwardauth/auth-app && ./deploy/deploy.sh' ``` + Examples for each host: ```bash # ci01.credo (auto-detected from credo-integration-01) @@ -73,19 +84,13 @@ sudo -u shared ./deploy.sh On first deployment, you'll be prompted for the Keycloak client secret: ``` -Enter Keycloak client secret for 'traefik' (or press Enter to generate random): +Enter Keycloak client secret for the 'traefik' client : ``` -**Important:** If you press Enter and a random secret is generated, you **must** configure this secret in Keycloak: -1. Log into Keycloak admin console at https://idm-credo.hartree.app -2. Navigate to: Realms → CReDO → Clients → traefik -3. Go to Credentials tab -4. Set the Client Secret to match the generated value (shown in `.env` file) - ### 3. What the Script Does 1. **Detects/validates** the current host -2. **Generates or reads secrets** for OAuth2 configuration +2. **Reads secrets** for OAuth2 configuration 3. **Creates `.env` file** with: - Keycloak client credentials - Generated security secrets @@ -118,65 +123,6 @@ After deployment: - **Public Test Service:** http://localhost:[TRAEFIK_PORT]/whoami-public - **External Access:** https://[FQDN] -## Managing the Stack - -### View logs -```bash -cd /home/opeppard/repositories/stack/forwardauth/auth-app -podman-compose logs -f -``` - -### Check container status -```bash -podman ps -``` - -### Stop the stack -```bash -podman-compose down -``` - -### Restart the stack -```bash -podman-compose restart -``` - -### Full redeployment -```bash -./deploy/deploy.sh [hostname] -``` - -## Troubleshooting - -### Containers not starting -1. Check podman logs: `podman-compose logs` -2. Verify ports are not already in use: `ss -tlnp | grep [PORT]` -3. Check SELinux context if applicable -4. Verify XDG_RUNTIME_DIR is set correctly - -### Authentication not working -1. Verify Keycloak client secret matches in both Keycloak and `.env` -2. Check redirect URI in Keycloak matches the FQDN -3. Review oauth2-proxy logs: `podman-compose logs oauth2-proxy` -4. Ensure the FQDN is accessible and resolves correctly - -### Permission issues -1. Ensure you're running as the correct user for the host -2. Check podman socket permissions -3. Verify file ownership in the deployment directory - -## Security Notes - -- The `.env` file contains sensitive secrets - keep it secure and don't commit to git -- Cookie secrets are used for session management -- Client secrets should be obtained from Keycloak admin -- All traffic should use HTTPS in production (configure in Traefik) - -## Network Configuration - -The stack creates/uses a network named `bongus` for inter-container communication. -All services should be on this network to communicate properly. - ## Keycloak Configuration Ensure the following settings in Keycloak for the `traefik` client: diff --git a/forwardauth/auth-app/deploy/deploy.sh b/forwardauth/auth-app/deploy/deploy.sh index 3db754d1..4d78e42f 100755 --- a/forwardauth/auth-app/deploy/deploy.sh +++ b/forwardauth/auth-app/deploy/deploy.sh @@ -113,8 +113,7 @@ log_info " Stack Name: $STACK_NAME" # Function to generate random string for secrets generate_secret() { - local length=${1:-32} - openssl rand -hex "$length" | head -c "$length" + dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64 | tr -d -- '\n' | tr -- '+/' '-_' ; echo } # Check if running as correct user @@ -136,12 +135,13 @@ fi # If CLIENT_SECRET is not set, prompt for it if [ -z "$CLIENT_SECRET" ]; then log_warn "CLIENT_SECRET not found in existing .env" - read -rsp "Enter Keycloak client secret for '$KEYCLOAK_CLIENT_ID' (or press Enter to generate random): " CLIENT_SECRET - echo - if [ -z "$CLIENT_SECRET" ]; then - log_warn "Generating random CLIENT_SECRET (you'll need to configure this in Keycloak)" - CLIENT_SECRET=$(generate_secret 32) - fi + while [ -z "$CLIENT_SECRET" ]; do + read -rsp "Enter Keycloak client secret for '$KEYCLOAK_CLIENT_ID': " CLIENT_SECRET + echo + if [ -z "$CLIENT_SECRET" ]; then + log_error "CLIENT_SECRET is required. Please enter a valid secret." + fi + done fi # Generate other secrets if not present @@ -235,9 +235,14 @@ log_info "Updating dynamic.yml with Nginx port..." sed -i.bak "s|http://host.containers.internal:[0-9]*|http://host.containers.internal:$NGINX_PORT|" "$BASE_DIR/traefik/dynamic.yml" log_info "dynamic.yml updated (Nginx port: $NGINX_PORT)" -# Set XDG_RUNTIME_DIR for podman if not set +# Set XDG_RUNTIME_DIR for podman if not set and fix podman socket if [ -z "$XDG_RUNTIME_DIR" ]; then export XDG_RUNTIME_DIR="/run/user/$(id -u)" + # hack to fix bad podman state + podman system migrate + # use a socket without systemd + podman system service --time 5 + fi log_info "XDG_RUNTIME_DIR: $XDG_RUNTIME_DIR" @@ -254,7 +259,7 @@ cd "$BASE_DIR" podman-compose down 2>/dev/null || log_warn "No existing containers to stop" # Start the stack -log_info "Starting stack..." +log_info "Starting auth stack..." podman-compose up -d # Check if containers started successfully From 545eb7ed475f6f6fd821b472a3ba1b8bdc703e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 20 Feb 2026 10:36:30 +0000 Subject: [PATCH 57/64] update testt script --- forwardauth/test/authenticated_curl_through_traefik.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forwardauth/test/authenticated_curl_through_traefik.sh b/forwardauth/test/authenticated_curl_through_traefik.sh index c8b1fcc4..49e0e756 100755 --- a/forwardauth/test/authenticated_curl_through_traefik.sh +++ b/forwardauth/test/authenticated_curl_through_traefik.sh @@ -2,4 +2,4 @@ TOKEN=$(./curl_for_token.sh | jq -r '.access_token') -curl -v -H "Authorization: Bearer $TOKEN" http://localhost:2025/whoami/ \ No newline at end of file +curl -v -w '\n' -H "Authorization: Bearer $TOKEN" http://localhost:9050/CentralStackAgent/getScenarios \ No newline at end of file From 060529e22c96f21943bcaf44eae609a955c2ff7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ois=C3=ADn?= Date: Fri, 20 Feb 2026 12:17:11 +0000 Subject: [PATCH 58/64] call out env var --- forwardauth/auth-app/oauth2-proxy/oauth2-proxy.template.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forwardauth/auth-app/oauth2-proxy/oauth2-proxy.template.cfg b/forwardauth/auth-app/oauth2-proxy/oauth2-proxy.template.cfg index 4d9fdf9d..d7c32dd0 100644 --- a/forwardauth/auth-app/oauth2-proxy/oauth2-proxy.template.cfg +++ b/forwardauth/auth-app/oauth2-proxy/oauth2-proxy.template.cfg @@ -18,7 +18,7 @@ scope="openid email profile groups" code_challenge_method="S256" # Cookies -cookie_secret="" +cookie_secret="xxxxxxxxxx" cookie_secure="false" cookie_samesite="lax" whitelist_domains=["xxxx"] From 775e552049dc88feb37e1bd67364925311781d39 Mon Sep 17 00:00:00 2001 From: ushcode Date: Fri, 20 Feb 2026 15:15:33 +0000 Subject: [PATCH 59/64] clean up auth compose --- forwardauth/auth-app/compose.yml | 36 +++++++------------------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/forwardauth/auth-app/compose.yml b/forwardauth/auth-app/compose.yml index 1cffaa1b..ea49db2b 100644 --- a/forwardauth/auth-app/compose.yml +++ b/forwardauth/auth-app/compose.yml @@ -2,7 +2,7 @@ name: proxy services: traefik: image: traefik:v3.6 - container_name: standalone-traefik-op + container_name: traefik ports: - "2025:80" # web entrypoint # - "443:443" # websecure entrypoint @@ -18,9 +18,9 @@ services: - ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro,Z - ./traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro,Z networks: - - bongus + - auth environment: - - STACK_NAME=bongus + - STACK_NAME=auth # podman security_opt: - label=disable @@ -36,35 +36,15 @@ services: - "./oauth2-proxy/oauth2-proxy.cfg:/oauth2-proxy.cfg:ro,Z" restart: unless-stopped networks: - - bongus + - auth # Test service with authentication whoami: image: traefik/whoami - container_name: test-whoami + container_name: whoami networks: - - bongus - labels: - - "traefik.enable=true" - - "traefik.docker.network=bongus" - - "traefik.http.routers.whoami.rule=PathPrefix(`/whoami`)" - - "traefik.http.routers.whoami.entrypoints=web" - - "traefik.http.routers.whoami.middlewares=oauth-auth-redirect@file" - - "traefik.http.services.whoami.loadbalancer.server.port=80" - - # Test service WITHOUT authentication for comparison - whoami-public: - image: traefik/whoami - container_name: test-whoami-public - networks: - - bongus - labels: - - "traefik.enable=true" - - "traefik.docker.network=bongus" - - "traefik.http.routers.whoami-public.rule=PathPrefix(`/whoami-public`)" - - "traefik.http.routers.whoami-public.entrypoints=web" - - "traefik.http.services.whoami-public.loadbalancer.server.port=80" + - auth networks: - bongus: - name: bongus + auth: + name: auth From ded2e228e09ef312fcd3a96f2a67de2913526cfc Mon Sep 17 00:00:00 2001 From: ushcode Date: Fri, 20 Feb 2026 17:21:02 +0000 Subject: [PATCH 60/64] fix middleware and simplify --- forwardauth/auth-app/traefik/dynamic.test.yml | 64 +++++++++++++++++++ forwardauth/auth-app/traefik/dynamic.yml | 27 +++++--- forwardauth/auth-app/traefik/traefik.yml | 9 +-- 3 files changed, 87 insertions(+), 13 deletions(-) create mode 100644 forwardauth/auth-app/traefik/dynamic.test.yml diff --git a/forwardauth/auth-app/traefik/dynamic.test.yml b/forwardauth/auth-app/traefik/dynamic.test.yml new file mode 100644 index 00000000..75ffef9b --- /dev/null +++ b/forwardauth/auth-app/traefik/dynamic.test.yml @@ -0,0 +1,64 @@ +# Dynamic Traefik configuration for middlewares +# $schema: https://www.schemastore.org/traefik-v3-file-provider.json + +http: + routers: + services-oauth2-route: + rule: "PathPrefix(`/oauth2/`)" + middlewares: + - auth-headers + service: oauth-backend + + whoami-public: + rule: "PathPrefix(`/public`)" + service: whoami-backend + + whoami-private: + rule: "PathPrefix(`/protected`)" + middlewares: + - oauth-auth-wo-redirect + service: whoami-backend + + whoami-web: + rule: "PathPrefix(`/login`)" + middlewares: + - oauth-auth-redirect + service: whoami-backend + + services: + oauth-backend: + loadBalancer: + servers: + - url: http://oauth2-proxy:4180 + whoami-backend: + loadBalancer: + servers: + - url: http://whoami:80 + + middlewares: + auth-headers: + headers: + sslRedirect: false + stsSeconds: 315360000 + browserXssFilter: true + contentTypeNosniff: true + forceSTSHeader: true + stsIncludeSubdomains: true + stsPreload: true + frameDeny: true + oauth-auth-redirect: + forwardAuth: + address: http://oauth2-proxy:4180/ + trustForwardHeader: true + authResponseHeaders: + - X-Auth-Request-Access-Token + - Authorization + authRequestHeaders: + - Cookie + oauth-auth-wo-redirect: + forwardAuth: + address: http://oauth2-proxy:4180/oauth2/auth + trustForwardHeader: true + authResponseHeaders: + - X-Auth-Request-Access-Token + - Authorization diff --git a/forwardauth/auth-app/traefik/dynamic.yml b/forwardauth/auth-app/traefik/dynamic.yml index d4899dae..75ffef9b 100644 --- a/forwardauth/auth-app/traefik/dynamic.yml +++ b/forwardauth/auth-app/traefik/dynamic.yml @@ -8,21 +8,32 @@ http: middlewares: - auth-headers service: oauth-backend - nginx-route: - rule: "PathPrefix(`/`)" + + whoami-public: + rule: "PathPrefix(`/public`)" + service: whoami-backend + + whoami-private: + rule: "PathPrefix(`/protected`)" + middlewares: + - oauth-auth-wo-redirect + service: whoami-backend + + whoami-web: + rule: "PathPrefix(`/login`)" middlewares: - oauth-auth-redirect - service: nginx-backend - priority: 1 + service: whoami-backend + services: oauth-backend: loadBalancer: servers: - url: http://oauth2-proxy:4180 - nginx-backend: + whoami-backend: loadBalancer: servers: - - url: http://host.containers.internal:9050 # nginx port goes here + - url: http://whoami:80 middlewares: auth-headers: @@ -40,12 +51,10 @@ http: address: http://oauth2-proxy:4180/ trustForwardHeader: true authResponseHeaders: - - X-Auth-Request-User - - X-Auth-Request-Email - X-Auth-Request-Access-Token - Authorization authRequestHeaders: - - Authorization + - Cookie oauth-auth-wo-redirect: forwardAuth: address: http://oauth2-proxy:4180/oauth2/auth diff --git a/forwardauth/auth-app/traefik/traefik.yml b/forwardauth/auth-app/traefik/traefik.yml index ee6038df..b1203281 100644 --- a/forwardauth/auth-app/traefik/traefik.yml +++ b/forwardauth/auth-app/traefik/traefik.yml @@ -12,10 +12,11 @@ entryPoints: address: ":8080" providers: - docker: - endpoint: "unix:///var/run/docker.sock" - exposedByDefault: false - network: "bingus" + # podman or non swarm docker, switch to 'swarm' if needed + # docker: + # endpoint: "unix:///var/run/docker.sock" + # exposedByDefault: false + # network: "" file: filename: "/etc/traefik/dynamic.yml" watch: true From ccfd1f16162a5cd0c109ff1b8d1793eea8e33503 Mon Sep 17 00:00:00 2001 From: ushcode Date: Fri, 20 Feb 2026 17:21:40 +0000 Subject: [PATCH 61/64] test scripts --- forwardauth/test/authenticated_curl_through_traefik.sh | 4 ++-- forwardauth/test/curl_for_token.sh | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/forwardauth/test/authenticated_curl_through_traefik.sh b/forwardauth/test/authenticated_curl_through_traefik.sh index 49e0e756..e0c0d1a8 100755 --- a/forwardauth/test/authenticated_curl_through_traefik.sh +++ b/forwardauth/test/authenticated_curl_through_traefik.sh @@ -1,5 +1,5 @@ #!/usr/bin/bash -TOKEN=$(./curl_for_token.sh | jq -r '.access_token') +TOKEN=$(./curl_for_token.sh) -curl -v -w '\n' -H "Authorization: Bearer $TOKEN" http://localhost:9050/CentralStackAgent/getScenarios \ No newline at end of file +curl -v -w '\n' -H "Authorization: Bearer $TOKEN" http://localhost:2025/protected \ No newline at end of file diff --git a/forwardauth/test/curl_for_token.sh b/forwardauth/test/curl_for_token.sh index 29f63975..b0233ed3 100755 --- a/forwardauth/test/curl_for_token.sh +++ b/forwardauth/test/curl_for_token.sh @@ -3,11 +3,15 @@ # shellcheck source=.env source .env -curl -s -X POST "${OIDC_TOKEN_URL}" \ +curl_token_endpoint() { + curl -s -X POST "${OIDC_TOKEN_URL}" \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'grant_type=password' \ -d "client_id=${OIDC_CLIENT_ID}" \ -d "client_secret=${OIDC_CLIENT_SECRET}" \ -d "username=${USERNAME}" \ -d "password=${PASSWORD}" \ - -d 'scope=openid' \ No newline at end of file + -d 'scope=openid' +} + +curl_token_endpoint | jq -r '.access_token' \ No newline at end of file From 6a00c7c707243d6b3ff66e57bd0beea9277534b1 Mon Sep 17 00:00:00 2001 From: ushcode Date: Fri, 20 Feb 2026 17:22:48 +0000 Subject: [PATCH 62/64] rm unneeded crap --- forwardauth/bingo.log | 3 - forwardauth/mesosphere/compose.yml | 157 ----------------------------- 2 files changed, 160 deletions(-) delete mode 100644 forwardauth/bingo.log delete mode 100644 forwardauth/mesosphere/compose.yml diff --git a/forwardauth/bingo.log b/forwardauth/bingo.log deleted file mode 100644 index c446640b..00000000 --- a/forwardauth/bingo.log +++ /dev/null @@ -1,3 +0,0 @@ - -time="2026-02-05T18:06:17Z" level=debug msg="Authenticate request" headers="map[Accept:[text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7] Accept-Encoding:[gzip, deflate, br, zstd] Accept-Language:[en-IE,en-GB;q=0.9,en;q=0.8] Cache-Control:[max-age=0] Cookie:[_forward_auth_csrf=98b60036f1373bce89d3af6249b537b5] Dnt:[1] Sec-Ch-Ua:[\"Not(A:Brand\";v=\"8\", \"Chromium\";v=\"144\", \"Microsoft Edge\";v=\"144\"] Sec-Ch-Ua-Mobile:[?0] Sec-Ch-Ua-Platform:[\"Windows\"] Sec-Fetch-Dest:[document] Sec-Fetch-Mode:[navigate] Sec-Fetch-Site:[cross-site] Upgrade-Insecure-Requests:[1] User-Agent:[Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0] X-Forwarded-For:[10.0.0.2] X-Forwarded-Host:[localhost:1916] X-Forwarded-Port:[1916] X-Forwarded-Proto:[http] X-Forwarded-Server:[bingus-traefik] X-Real-Ip:[10.0.0.2]]" rule=default source_ip=10.0.0.2 -time="2026-02-05T18:06:17Z" level=debug msg="sending CSRF cookie and a redirect to OIDC login" source_ip=10.0.0.2 diff --git a/forwardauth/mesosphere/compose.yml b/forwardauth/mesosphere/compose.yml deleted file mode 100644 index 922a51f5..00000000 --- a/forwardauth/mesosphere/compose.yml +++ /dev/null @@ -1,157 +0,0 @@ -secrets: - cloudflare-token: - file: "./secrets/cloudflare-token.secret" - cloudflare-email: - file: "./secrets/cloudflare-email.secret" - -services: - traefik: - image: "traefik:v2.11" - restart: always - container_name: "traefik" - networks: - - mynetwork - command: - - "--log.level=DEBUG" - - --providers.docker=true - - --providers.docker.exposedbydefault=false - - --api.dashboard=true - # Set up LetsEncrypt certificate resolver - - --certificatesresolvers.letsencrypt.acme.dnschallenge=true - - --certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare - - --certificatesResolvers.letsencrypt.acme.dnschallenge.resolvers=1.1.1.1:53,1.0.0.1:53 - - --certificatesResolvers.letsencrypt.acme.dnschallenge.delayBeforeCheck=20 - - --certificatesresolvers.letsencrypt.acme.email=youremail@gmail.com # CHANGE HERE - - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json - # staging environment of LE, remove for real certs - #- --certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory - this is the staging url - - --certificatesresolvers.letsencrypt.acme.caserver=https://acme-v02.api.letsencrypt.org/directory - # Set up an insecure listener that redirects all traffic to TLS - - --entrypoints.web.address=:80 - - --entrypoints.websecure.address=:443 - - --entrypoints.web.http.redirections.entrypoint.to=websecure - - --entrypoints.web.http.redirections.entrypoint.scheme=https - # - --entrypoints.websecure.http.middlewares=traefik-forward-auth - # Set up the TLS configuration for our websecure listener - - --entrypoints.websecure.http.tls=true - - --entrypoints.websecure.http.tls.certResolver=letsencrypt - - --entrypoints.websecure.http.tls.domains[0].main=yourdomain.org # CHANGE HERE - - --entrypoints.websecure.http.tls.domains[0].sans=*.yourdomain.org # CHANGE HERE - secrets: - - "cloudflare-token" - - "cloudflare-email" - environment: - - "CF_DNS_API_TOKEN_FILE=/run/secrets/cloudflare-token" - - "CF_API_EMAIL_FILE=/run/secrets/cloudflare-email" - ports: - - "80:80" - - "443:443" - volumes: - - "./certs:/letsencrypt" # create folder certs - - "/var/run/docker.sock:/var/run/docker.sock:ro" - labels: - - "traefik.enable=true" - - "traefik.http.routers.traefik.rule=Host(`traefik.dev.yourdomain.org`)" # CHANGE HERE - - "traefik.http.routers.traefik.entrypoints=websecure" - - "traefik.http.routers.traefik.service=api@internal" - - "traefik.http.routers.traefik.tls.certresolver=letsencrypt" - - "traefik.http.routers.traefik.middlewares=traefik-auth" - - "traefik.http.middlewares.traefik-auth.basicauth.users=user:{SHA}EtfyZU+wgWTV563pLJlJWWgyzDSck=" # user is the login name after ":"here i just use an sha coded password so that traefik dashboard is secured as well, you can just delete this if you want or encrypt one password online - - paste-bin: - container_name: paste-bin - image: ghcr.io/enchant97/hasty-paste:latest - restart: unless-stopped - depends_on: - - traefik-forward-auth - networks: - - mynetwork - labels: - - "traefik.enable=true" - - "traefik.http.routers.paste.rule=Host(`paste.dev.yourdomain.org`)" # CHANGE HERE - - "traefik.http.routers.paste.entrypoints=websecure" - - "traefik.http.routers.paste.tls.certresolver=letsencrypt" - - "traefik.http.routers.paste.middlewares=forwardauth" # Add this line to any image to make it use forwardauth - - traefik-forward-auth: - image: mesosphere/traefik-forward-auth - container_name: traefik-forward-auth - restart: on-failure - depends_on: - - traefik - - keycloak - environment: - - TZ=Europe/Berlin - - SECRET=fd92459cd7ygafc2df466sdg3747z43ce0d # CHANGE HERE, random secret - - PROVIDER_URI=https://keycloak.dev.yourdomain.org/realms/myrealm # CHANGE HERE , myrealm is a realm i created in keyclak - - CLIENT_ID=my-client # CHANGE HERE - - CLIENT_SECRET=wueZ37IOE42Sv3XVqJyhrLX3WIk4y # CHANGE HERE - - COOKIE_DOMAIN=https://dev.yourdomain.org - - DISABLE_SSL_VERIFICATION=true # might be unnecessary - - INSECURE_COOKIE=1 - - ENCRYPTION_KEY=7347373954253633845947936 # CHANGE HERE, random key - - SCOPE=profile email openid # this is a must!! - networks: - - mynetwork - labels: - - "traefik.enable=true" - - "traefik.docker.network=web" - - "traefik.http.services.traefik-forward-auth.loadbalancer.server.port=4181" - - "traefik.http.routers.traefik-forward-auth.entrypoints=websecure" - - "traefik.http.routers.traefik-forward-auth.middlewares=forwardauth" - - "traefik.http.middlewares.forwardauth.forwardauth.address=http://traefik-forward-auth:4181" - - "traefik.http.middlewares.forwardauth.forwardauth.authResponseHeaders=X-Forwarded-User" - - "traefik.http.middlewares.forwardauth.forwardauth.trustForwardHeader=true" - - keycloakdb: - image: postgres:16.2-alpine - container_name: keycloakdb - environment: - - POSTGRES_DB=keycloak - - POSTGRES_USER=keycloak - - POSTGRES_PASSWORD=password - - POSTGRES_ROOT_PASSWORD=password - networks: - - mynetwork - ports: - - "5432:5432" - volumes: - - keycloakdata:/var/lib/postgresql/data - labels: - - "traefik.enable=false" - - keycloak: - image: quay.io/keycloak/keycloak:24.0 - container_name: keycloak - hostname: keycloak - environment: - - KC_HOSTNAME_STRICT=false - - KC_DB=postgres - - KC_DB_URL=jdbc:postgresql://keycloakdb/keycloak - - KC_DB_URL_PORT=5432 - - KC_DB_USERNAME=keycloak - - KC_DB_PASSWORD=password - - KC_DB_SCHEMA=public - - KC_LOG_LEVEL=info - - KC_FEATURES=docker - - KEYCLOAK_ADMIN=admin - - KEYCLOAK_ADMIN_PASSWORD=password - - KC_PROXY=edge - networks: - - mynetwork - depends_on: - - traefik - - keycloakdb - labels: - - "traefik.enable=true" - - "traefik.http.routers.keycloak.rule=Host(`keycloak.dev.yourdomain.org`)" # CHANGE HERE - - "traefik.http.routers.keycloak.entrypoints=websecure" - - "traefik.http.routers.keycloak.tls.certresolver=letsencrypt" - entrypoint: ["/opt/keycloak/bin/kc.sh", "start-dev"] - -networks: - mynetwork: - driver: bridge - -volumes: - keycloakdata: From e72fc219d9beb326c959190737869491ec126771 Mon Sep 17 00:00:00 2001 From: ushcode Date: Fri, 20 Feb 2026 17:25:47 +0000 Subject: [PATCH 63/64] add missing bits to oauth2 proxy template --- forwardauth/auth-app/oauth2-proxy/oauth2-proxy.template.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/forwardauth/auth-app/oauth2-proxy/oauth2-proxy.template.cfg b/forwardauth/auth-app/oauth2-proxy/oauth2-proxy.template.cfg index d7c32dd0..56721085 100644 --- a/forwardauth/auth-app/oauth2-proxy/oauth2-proxy.template.cfg +++ b/forwardauth/auth-app/oauth2-proxy/oauth2-proxy.template.cfg @@ -22,6 +22,9 @@ cookie_secret="xxxxxxxxxx" cookie_secure="false" cookie_samesite="lax" whitelist_domains=["xxxx"] +skip_jwt_bearer_tokens="true" +extra_jwt_issuers=["https://dev.theworldavatar.io/realms/twa-test=bingus"] + # Logging request_logging="true" From 6401cf74cf36c591e750f94e1e271a6911ca3125 Mon Sep 17 00:00:00 2001 From: ushcode Date: Fri, 20 Feb 2026 17:48:10 +0000 Subject: [PATCH 64/64] setup roles thing --- forwardauth/auth-app/oauth2-proxy/oauth2-proxy.template.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/forwardauth/auth-app/oauth2-proxy/oauth2-proxy.template.cfg b/forwardauth/auth-app/oauth2-proxy/oauth2-proxy.template.cfg index 56721085..34754b1a 100644 --- a/forwardauth/auth-app/oauth2-proxy/oauth2-proxy.template.cfg +++ b/forwardauth/auth-app/oauth2-proxy/oauth2-proxy.template.cfg @@ -16,6 +16,7 @@ oidc_issuer_url="xxxxxxxxxxxx" redirect_url="xxxxxxxxxxxxxx" scope="openid email profile groups" code_challenge_method="S256" +allowed_roles="protected" # Cookies cookie_secret="xxxxxxxxxx"