From 9411f87dbc57c29dd566c2922e27c7b8aac7f09b Mon Sep 17 00:00:00 2001 From: Drew Haven Date: Thu, 29 May 2025 17:05:48 -0700 Subject: [PATCH] [timetagger,traefik] Adds timetagger back, and attempts to put it behind an oauth proxy, but the traefik config isn't quite right. --- secrets/mcp.yaml | 13 +- system/hosts/mcp/containers.nix | 4 +- system/hosts/mcp/containers/lib.nix | 185 ++++++++++-------- system/hosts/mcp/containers/oauth2proxy.nix | 50 +++++ system/hosts/mcp/containers/timetagger.nix | 31 +++ system/hosts/mcp/containers/traefik.nix | 82 +++++--- .../{static => dynamic}/dockerRegistry.yaml | 0 .../hosts/mcp/containers/traefik/traefik.yaml | 2 +- 8 files changed, 253 insertions(+), 114 deletions(-) create mode 100644 system/hosts/mcp/containers/oauth2proxy.nix create mode 100644 system/hosts/mcp/containers/timetagger.nix rename system/hosts/mcp/containers/traefik/{static => dynamic}/dockerRegistry.yaml (100%) diff --git a/secrets/mcp.yaml b/secrets/mcp.yaml index 6852f8c..c35f1b8 100644 --- a/secrets/mcp.yaml +++ b/secrets/mcp.yaml @@ -11,11 +11,9 @@ focalboard: offen: secret: ENC[AES256_GCM,data:sH2siPc/QH1O2M7ZlJwqhqlHRIeLIG9r,iv:eD29ALx2ji0rm1t9j6RulTZT3f6VLK7dxpPOze3qDKA=,tag:zqJTgT2UeA/ecBS4VremUw==,type:str] smtp-token: ENC[AES256_GCM,data:ZTfe65g3JykPvG2l0AN8UQ==,iv:GTruGo/vcP+imfJyqB3NX9ic8dz5jvTEh6SF+OeqMDM=,tag:kgwd59pG/WUt8OAaVzi39Q==,type:str] +traefik: + oauth2-client-secret: ENC[AES256_GCM,data:c4zU/oH/buelobBo/47V74Xwm6MU76C+UPCFpuARYU8=,iv:7dl0MIjza6B8FGl3i3O/4CxKxlp4z2S+H82xSLUg5iQ=,tag:63E/RNjiw56evIxfhF8acg==,type:str] sops: - kms: [] - gcp_kms: [] - azure_kv: [] - hc_vault: [] age: - recipient: age1yvdzvuvu5wqztcx6ll2xk6x547uuyqy735tjjdd7zftkz53jsf9qf5ahue enc: | @@ -35,8 +33,7 @@ sops: by9aNFY4dXNxaWxnTXFTQS9reHhuQWMKh5rZ93nFtBV9EpFVRp+E+GXZ6xzVy2Jw vFh4deGcAb60q4odSaeWfk1Dr7L9Ua69oK9omjbCNUt+P7Kwlfca7Q== -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-04-28T23:33:42Z" - mac: ENC[AES256_GCM,data:cZkRcGV5/CPPVUdTDekwC8UjO6K348sBsS7NvR8wnoXS0AmSZsqN594nkvoc0VccM55Hwnm4jZxY56OV+UFMya1IRIkTo6LJRb88/CgZ8bjz30ACe33FKgJfCugimUDKsekbgNX1UFg1DVbqYK9/N4fcEBSxV3Xmzy5QGnQ/8KU=,iv:EprUHNtU5w7569ADMOxw+izDAL22A5OrB12T9iyHxKU=,tag:kRvyUEZwd/RttKdFOY2bJQ==,type:str] - pgp: [] + lastmodified: "2025-05-29T23:38:06Z" + mac: ENC[AES256_GCM,data:SImZMvXfUUt2IznmthXEG2o2cUUn485+D44OFZZc3WGmvrPIJ8WjT50HUu3fCEWmwO4mFv1VVBAr/SkN3s6VDgCh7Nbe4clp3zpgyiWamiXlvJP2Y0k1W+KjTd/AQ7jLig9D9UDXHVRBy5rZ5iLZdjZ6s5WQ8rfztcbMpkdi9yk=,iv:XVHHpwFkkhzbO3wYoerIcDeA5Io0GeLxzR2AT+BQij8=,tag:go/0xd1FhG/5h+TXA4xxHg==,type:str] unencrypted_suffix: _unencrypted - version: 3.9.4 + version: 3.10.2 diff --git a/system/hosts/mcp/containers.nix b/system/hosts/mcp/containers.nix index f68b7a8..fd6517b 100644 --- a/system/hosts/mcp/containers.nix +++ b/system/hosts/mcp/containers.nix @@ -16,6 +16,7 @@ ./containers/searxng.nix ./containers/shared-postgres.nix ./containers/synapse.nix + ./containers/timetagger.nix ./containers/traefik.nix ./containers/users.nix ]; @@ -50,7 +51,8 @@ inherit (import ./containers/lib.nix config) hostRuleHavenisms localHostRuleHavenisms - havenisms; + havenisms + ; in { jellyfin = { diff --git a/system/hosts/mcp/containers/lib.nix b/system/hosts/mcp/containers/lib.nix index 7696a38..6f1fa73 100644 --- a/system/hosts/mcp/containers/lib.nix +++ b/system/hosts/mcp/containers/lib.nix @@ -21,41 +21,62 @@ in havenisms blazestar terakoda - terakoda_net; + terakoda_net + ; - mkContainer = { - image, - hostName, - port, - homepageOpts ? {}, - dependsOn ? [], - domain ? havenisms, - ports ? [], - volumes ? [], - environment ? {}, - environmentFiles ? [], - public ? false, - user ? null, - extraOptions ? [], - }: + mkContainer = + { + image, + hostName, + port, + homepageOpts ? { }, + dependsOn ? [ ], + domain ? havenisms, + ports ? [ ], + volumes ? [ ], + environment ? { }, + environmentFiles ? [ ], + public ? false, + user ? null, + extraOptions ? [ ], + oauthProxy ? false, + }: let routerRule = if public then hostRule hostName domain else localHostRule hostName domain; - homepageLabels = if homepageOpts == {} then {} else { - "homepage.group" = "${homepageOpts.group}"; - "homepage.name" = "${homepageOpts.name}"; - "homepage.icon" = "${homepageOpts.icon}"; - "homepage.href" = "https://${hostName}.${domain}"; - "homepage.description" = "${homepageOpts.description}"; - }; + homepageLabels = + if homepageOpts == { } then + { } + else + { + "homepage.group" = "${homepageOpts.group}"; + "homepage.name" = "${homepageOpts.name}"; + "homepage.icon" = "${homepageOpts.icon}"; + "homepage.href" = "https://${hostName}.${domain}"; + "homepage.description" = "${homepageOpts.description}"; + }; + oauthLabels = + if oauthProxy then { "traefik.http.routers.${hostName}.middlewares" = "oidc-auth@file"; } else { }; in { - inherit image dependsOn volumes environment environmentFiles ports user extraOptions; + inherit + image + dependsOn + volumes + environment + environmentFiles + ports + user + extraOptions + ; autoStart = true; - labels = { - "traefik.enable" = "true"; - "traefik.http.routers.${hostName}.rule" = "${routerRule}"; - "traefik.http.services.${hostName}.loadbalancer.server.port" = "${toString port}"; - } // homepageLabels; + labels = + { + "traefik.enable" = "true"; + "traefik.http.routers.${hostName}.rule" = "${routerRule}"; + "traefik.http.services.${hostName}.loadbalancer.server.port" = "${toString port}"; + } + // oauthLabels + // homepageLabels; }; # Creates a MariaDB container for a specific app. It should be safe to give @@ -64,60 +85,64 @@ in # user. # # Note that this returns a _module_ so that it can be imported and provide many different config values. - mkMariaDbContainer = { - name, - uid, - gid, - passwordSecret, - directory, - }: - { config, ... }: - { - virtualisation.oci-containers.containers."${name}-mariadb" = { - image = "lscr.io/linuxserver/mariadb:latest"; - autoStart = true; - ports = [ "3306:3306" ]; - volumes = [ - "${directory}:/config" - "${config.sops.secrets.mariadb_root_password.path}:/run/secrets/mariadb_root_password" - "${config.sops.secrets."${passwordSecret}".path}:/run/secrets/mariadb_password" - ]; - environment = { - PUID = "${toString uid}"; - PGID = "${toString gid}"; - MYSQL_USER = name; - MYSQL_DATABASE = name; - FILE__MYSQL_ROOT_PASSWORD = "/run/secrets/mariadb_root_password"; - FILE__MYSQL_PASSWORD = "/run/secrets/mariadb_password"; + mkMariaDbContainer = + { + name, + uid, + gid, + passwordSecret, + directory, + }: + { config, ... }: + { + virtualisation.oci-containers.containers."${name}-mariadb" = { + image = "lscr.io/linuxserver/mariadb:latest"; + autoStart = true; + ports = [ "3306:3306" ]; + volumes = [ + "${directory}:/config" + "${config.sops.secrets.mariadb_root_password.path}:/run/secrets/mariadb_root_password" + "${config.sops.secrets."${passwordSecret}".path}:/run/secrets/mariadb_password" + ]; + environment = { + PUID = "${toString uid}"; + PGID = "${toString gid}"; + MYSQL_USER = name; + MYSQL_DATABASE = name; + FILE__MYSQL_ROOT_PASSWORD = "/run/secrets/mariadb_root_password"; + FILE__MYSQL_PASSWORD = "/run/secrets/mariadb_password"; + }; }; }; - }; - mkPostgresContainer = { - name, - uid, - gid, - passwordSecret, - directory, - containerName ? "${name}-postgres", - databaseName ? name, - username ? name, - }: { config, ... }: { - virtualisation.oci-containers.containers."${containerName}" = { - image = "postgres"; - autoStart = true; - volumes = [ - # Note that data must be mounted at this location to persist. - # See https://github.com/docker-library/docs/blob/master/postgres/README.md#pgdata - "${directory}:/var/lib/postgresql/data" - "${config.sops.secrets."${passwordSecret}".path}:/run/secrets/postgres_password" - ]; - user = "${toString uid}:${toString gid}"; - environment = { - POSTGRES_USER = username; - POSTGRES_DB = databaseName; - POSTGRES_PASSWORD_FILE = "/run/secrets/postgres_password"; + mkPostgresContainer = + { + name, + uid, + gid, + passwordSecret, + directory, + containerName ? "${name}-postgres", + databaseName ? name, + username ? name, + }: + { config, ... }: + { + virtualisation.oci-containers.containers."${containerName}" = { + image = "postgres"; + autoStart = true; + volumes = [ + # Note that data must be mounted at this location to persist. + # See https://github.com/docker-library/docs/blob/master/postgres/README.md#pgdata + "${directory}:/var/lib/postgresql/data" + "${config.sops.secrets."${passwordSecret}".path}:/run/secrets/postgres_password" + ]; + user = "${toString uid}:${toString gid}"; + environment = { + POSTGRES_USER = username; + POSTGRES_DB = databaseName; + POSTGRES_PASSWORD_FILE = "/run/secrets/postgres_password"; + }; }; }; - }; } diff --git a/system/hosts/mcp/containers/oauth2proxy.nix b/system/hosts/mcp/containers/oauth2proxy.nix new file mode 100644 index 0000000..32b31c0 --- /dev/null +++ b/system/hosts/mcp/containers/oauth2proxy.nix @@ -0,0 +1,50 @@ +{ config, ... }: +let + inherit (import ./lib.nix config) mkContainer blazestar havenisms; +in +{ + sops.secrets = { + "oauth2-proxy/cookie-secret" = { + restartUnits = [ "podman-oauth2-proxy.service" ]; + mode = "0400"; + }; + "oauth2-proxy/client-secret" = { + restartUnits = [ "podman-oauth2-proxy.service" ]; + mode = "0400"; + }; + }; + + sops.templates."oauth2-proxy.env".content = '' + OAUTH2_PROXY_HTTP_ADDRESS='0.0.0.0:4180' + OAUTH2_PROXY_COOKIE_SECRET='${config.sops.placehoder."oauth2-proxy/cookie-secret"}' + OAUTH2_PROXY_COOKIE_DOMAINS='.${blazestar} .${havenisms}' + OAUTH2_PROXY_WHITELIST_DOMAINS='.${blazestar} .${havenisms}' + OAUTH2_PROXY_PROVIDER='oidc' + OAUTH2_PROXY_CLIENT_ID='oauth2-proxy' + OAUTH2_PROXY_CLIENT_SECRET='${config.sops.placehoder."oauth2-proxy/client-secret"}' + OAUTH2_PROXY_EMAIL_DOMAINS='*' + OAUTH2_PROXY_OIDC_ISSUER_URL='https://auth.${blazestar}/realms/master' + OAUTH2_PROXY_REDIRECT_URL='https://auth.${blazestar}/oauth2/callback' + OAUTH2_PROXY_COOKIE_CSRF_PER_REQUEST=true + OAUTH2_PROXY_COOKIE_CSRF_EXPIRE='5m' + OAUTH2_PROXY_CUSTOM_TEMPLATES_DIR="/templates" + OAUTH2_PROXY_REVERSE_PROXY=true + ''; + + virtualisation.oci-containers.containers.oauth2-proxy = mkContainer { + image = "quay.io/oauth2-proxy/oauth2-proxy"; + hostName = "oauth"; + domain = blazestar; + port = "4180"; + homepageOpts = { + group = "Infra"; + name = "OAuth2-Proxy"; + icon = "oauth2-proxy.png"; + description = "An OAuth2 Reverse Proxy"; + }; + volumes = [ + ]; + environment = { + }; + }; +} diff --git a/system/hosts/mcp/containers/timetagger.nix b/system/hosts/mcp/containers/timetagger.nix new file mode 100644 index 0000000..8daa5a6 --- /dev/null +++ b/system/hosts/mcp/containers/timetagger.nix @@ -0,0 +1,31 @@ +{ config, ... }: +let + inherit (import ./lib.nix config) mkContainer terakoda_net; +in +{ + virtualisation.oci-containers.containers.timetagger = mkContainer { + image = "ghcr.io/almarklein/timetagger:v24.12.2"; + hostName = "time"; + domain = terakoda_net; + port = "80"; + oauthProxy = true; + homepageOpts = { + group = "Apps"; + name = "TimeTagger"; + icon = "timetagger.png"; + description = "Time tracker"; + + }; + volumes = [ + "/tank/config/timetagger:/data" + ]; + environment = { + TIMETAGGER_BIND = "0.0.0.0:80"; + TIMETAGGER_DATADIR = "/data"; + TIMETAGGER_LOG_LEVEL = "debug"; + TIMETAGGER_PROXY_AUTH_ENABLED = "True"; + TIMETAGGER_PROXY_AUTH_TRUSTED = "10.88.0.0/16"; + TIMETAGGER_PROXY_AUTH_HEADER = "X-Remote-User"; + }; + }; +} diff --git a/system/hosts/mcp/containers/traefik.nix b/system/hosts/mcp/containers/traefik.nix index abedadf..d461c5f 100644 --- a/system/hosts/mcp/containers/traefik.nix +++ b/system/hosts/mcp/containers/traefik.nix @@ -5,28 +5,62 @@ let name = "traefik-config"; path = ./traefik; }; -in { - virtualisation.oci-containers.containers.traefik = mkContainer { - image = "traefik"; - hostName = "proxy"; - port = 8080; - domain = blazestar; - public = false; - ports = [ - "80:80" - "443:443" - ]; - volumes = - [ - "/var/run/podman/podman.sock:/var/run/docker.sock:ro" - "${traefikConfigDir}:/etc/traefik" - "/tank/config/traefik/acme:/etc/traefik/acme" - ]; - homepageOpts = { - name = "Traefik"; - icon = "traefik.svg"; - group = "Infra"; - description = "Reverse Proxy"; - }; +in +{ + + sops.secrets = { + "traefik/oauth2-client-secret" = { + restartUnits = [ "podman-traefik.service" ]; + mode = "0400"; }; - } + }; + + sops.templates."traefik/oauth2-config.yaml".content = '' + experimental: + plugins: + traefik-oidc-auth: + moduleName: "github.com/sevensolutions/traefik-oidc-auth" + version: "v0.11.0" + + http: + middlewares: + oidc-auth: + plugin: + traefik-oidc-auth: + Provider: + Url: "https://auth.blazestar.net/" + ClientId: "3e3f7d9a-a684-4412-866c-ea7281954a9f" + ClientSecret: "${config.sops.placeholder."traefik/oauth2-client-secret"}" + TokenValidation: "IdToken" + Scopes: ["openid", "profile", "email"] + ''; + + virtualisation.oci-containers.containers.traefik = mkContainer { + image = "traefik"; + hostName = "proxy"; + port = 8080; + domain = blazestar; + public = false; + ports = [ + "80:80" + "443:443" + ]; + volumes = [ + "/var/run/podman/podman.sock:/var/run/docker.sock:ro" + # All the configs from the config directory + "${traefikConfigDir}:/etc/traefik" + # Oauth2 config containing secrets + "${config.sops.templates."traefik/oauth2-config.yaml".path}:/etc/traefik/dynamic/oauth2-config.yaml" + # Persistent storage for acme certificates + # TODO: It may be possible to just use docker storage because persistence + # is not critical when the cert can just be renewed. + "/tank/config/traefik/acme:/etc/traefik/acme" + ]; + homepageOpts = { + name = "Traefik"; + icon = "traefik.svg"; + group = "Infra"; + description = "Reverse Proxy"; + }; + }; +} diff --git a/system/hosts/mcp/containers/traefik/static/dockerRegistry.yaml b/system/hosts/mcp/containers/traefik/dynamic/dockerRegistry.yaml similarity index 100% rename from system/hosts/mcp/containers/traefik/static/dockerRegistry.yaml rename to system/hosts/mcp/containers/traefik/dynamic/dockerRegistry.yaml diff --git a/system/hosts/mcp/containers/traefik/traefik.yaml b/system/hosts/mcp/containers/traefik/traefik.yaml index 8120d26..a8d6d30 100644 --- a/system/hosts/mcp/containers/traefik/traefik.yaml +++ b/system/hosts/mcp/containers/traefik/traefik.yaml @@ -21,7 +21,7 @@ providers: docker: exposedByDefault: false file: - directory: /etc/traefik/static + directory: /etc/traefik/dynamic watch: true certificatesResolvers: