From 11aea04fe5e568fe23153dbbf907435f40aad094 Mon Sep 17 00:00:00 2001 From: teidesu Date: Sat, 26 Oct 2024 22:44:01 +0300 Subject: [PATCH] chore(koi): authentik -> kanidm --- hosts/koi/configuration.nix | 3 +- hosts/koi/containers/authentik/default.nix | 77 --------------------- hosts/koi/containers/kanidm/default.nix | 51 ++++++++++++++ hosts/koi/containers/kanidm/proxy.nix | 69 ++++++++++++++++++ hosts/koi/containers/kanidm/server.toml | 9 +++ hosts/koi/containers/siyuan/default.nix | 25 +++---- secrets/kanidm-tls-cert.age | Bin 0 -> 2237 bytes secrets/kanidm-tls-key.age | Bin 0 -> 3484 bytes secrets/openid-proxy-env.age | 6 ++ secrets/siyuan-teidesu-authentik-env.age | 6 -- secrets/siyuan-teidesu-proxy-env.age | Bin 0 -> 358 bytes 11 files changed, 147 insertions(+), 99 deletions(-) delete mode 100644 hosts/koi/containers/authentik/default.nix create mode 100644 hosts/koi/containers/kanidm/default.nix create mode 100644 hosts/koi/containers/kanidm/proxy.nix create mode 100644 hosts/koi/containers/kanidm/server.toml create mode 100644 secrets/kanidm-tls-cert.age create mode 100644 secrets/kanidm-tls-key.age create mode 100644 secrets/openid-proxy-env.age delete mode 100644 secrets/siyuan-teidesu-authentik-env.age create mode 100644 secrets/siyuan-teidesu-proxy-env.age diff --git a/hosts/koi/configuration.nix b/hosts/koi/configuration.nix index 951adc4..8813f49 100755 --- a/hosts/koi/configuration.nix +++ b/hosts/koi/configuration.nix @@ -30,8 +30,7 @@ ./containers/navidrome ./containers/conduwuit ./containers/zond - ./containers/authentik - ./containers/outline-wiki + ./containers/kanidm ./containers/siyuan ./containers/teisu.nix ./containers/bots/pcre-sub-bot.nix diff --git a/hosts/koi/containers/authentik/default.nix b/hosts/koi/containers/authentik/default.nix deleted file mode 100644 index b0236e8..0000000 --- a/hosts/koi/containers/authentik/default.nix +++ /dev/null @@ -1,77 +0,0 @@ -{ abs, pkgs, config, ... }@inputs: - -let - secrets = import (abs "lib/secrets.nix"); - - UID = 1110; - sharedConfig = { - image = "ghcr.io/goauthentik/server:2024.8.2"; - dependsOn = [ "authentik-redis" ]; - environment = { - AUTHENTIK_POSTGRESQL__HOST = "172.17.0.1"; - AUTHENTIK_POSTGRESQL__USER = "authentik"; - AUTHENTIK_POSTGRESQL__PASSWORD = "authentik"; - AUTHENTIK_POSTGRESQL__NAME = "authentik"; - AUTHENTIK_REDIS__HOST = "authentik-redis.docker"; - }; - volumes = [ - "/mnt/puffer/authentik/media:/media" - "/mnt/puffer/authentik/templates:/templates" - ]; - user = builtins.toString UID; - environmentFiles = [ - (secrets.file config "authentik-env") - ]; - }; -in { - imports = [ - # email related + AUTHENTIK_SECRET_KEY - (secrets.declare [{ - name = "authentik-env"; - owner = "authentik"; - }]) - ]; - - users.users.authentik = { - isNormalUser = true; - uid = UID; - }; - - services.postgresql.ensureUsers = [ - { name = "authentik"; ensureDBOwnership = true; } - ]; - services.postgresql.ensureDatabases = [ "authentik" ]; - desu.postgresql.ensurePasswords.authentik = "authentik"; - - virtualisation.oci-containers.containers.authentik-redis = { - image = "docker.io/redis:7.0-alpine"; - volumes = [ - "/mnt/puffer/authentik/redis:/data" - ]; - user = builtins.toString UID; - }; - - virtualisation.oci-containers.containers.authentik-server = sharedConfig // { - cmd = [ "server" ]; - }; - systemd.services.docker-authentik-server.after = [ "postgresql.service" ]; - - virtualisation.oci-containers.containers.authentik-worker = sharedConfig // { - cmd = [ "worker" ]; - }; - systemd.services.docker-authentik-worker.after = [ "postgresql.service" ]; - - systemd.tmpfiles.rules = [ - "d /mnt/puffer/authentik 0777 root root -" - ]; - - services.nginx.virtualHosts."id.stupid.fish" = { - forceSSL = true; - useACMEHost = "stupid.fish"; - - locations."/" = { - proxyPass = "http://authentik-server.docker:9000$request_uri"; - proxyWebsockets = true; - }; - }; -} \ No newline at end of file diff --git a/hosts/koi/containers/kanidm/default.nix b/hosts/koi/containers/kanidm/default.nix new file mode 100644 index 0000000..cb899ab --- /dev/null +++ b/hosts/koi/containers/kanidm/default.nix @@ -0,0 +1,51 @@ +{ abs, pkgs, config, ... }@inputs: + +let + secrets = import (abs "lib/secrets.nix"); + + UID = 1111; +in { + imports = [ + (secrets.declare [ + { + name = "kanidm-tls-key"; + owner = "kanidm"; + } + { + name = "kanidm-tls-cert"; + owner = "kanidm"; + } + ]) + ./proxy.nix + ]; + users.users.kanidm = { + isNormalUser = true; + uid = UID; + }; + + virtualisation.oci-containers.containers.kanidm = { + image = "kanidm/server:1.3.3"; + volumes = [ + "/srv/kanidm/data:/data/db" + "${./server.toml}:/data/server.toml" + "${(secrets.file config "kanidm-tls-key")}:/data/key.pem" + "${(secrets.file config "kanidm-tls-cert")}:/data/chain.pem" + ]; + + user = "${builtins.toString UID}:60"; + }; + + systemd.tmpfiles.rules = [ + "d /srv/kanidm/data 0700 ${builtins.toString UID} ${builtins.toString UID} -" + ]; + + services.nginx.virtualHosts."id.stupid.fish" = { + forceSSL = true; + useACMEHost = "stupid.fish"; + + locations."/" = { + proxyPass = "https://kanidm.docker:8443$request_uri"; + proxyWebsockets = true; + }; + }; +} \ No newline at end of file diff --git a/hosts/koi/containers/kanidm/proxy.nix b/hosts/koi/containers/kanidm/proxy.nix new file mode 100644 index 0000000..6f0104c --- /dev/null +++ b/hosts/koi/containers/kanidm/proxy.nix @@ -0,0 +1,69 @@ +{ pkgs, config, lib, ... }: + +let + cfg = config.desu.openid-proxy; +in { + options.desu.openid-proxy = with lib; { + services = mkOption { + type = types.attrsOf (types.submodule ({ ... }: { + options = { + clientId = mkOption { + type = types.str; + description = "oauth2 client id"; + }; + domain = mkOption { + type = types.str; + description = "domain that the service will be hosted on"; + }; + upstream = mkOption { + type = types.str; + description = "upstream address"; + }; + envSecret = mkOption { + type = types.str; + description = "name of the secret that contains the env vars (OAUTH2_PROXY_COOKIE_SECRET, OAUTH2_PROXY_CLIENT_SECRET)"; + }; + extra = mkOption { + type = types.listOf types.str; + description = "extra arguments that will be passed to the service"; + default = []; + }; + uid = mkOption { + type = types.int; + description = "uid of the user that will run the service"; + }; + }; + })); + default = {}; + }; + }; + + config = lib.mkIf (cfg.services != {}) { + virtualisation.oci-containers.containers = builtins.listToAttrs ( + map (name: let + service = cfg.services.${name}; + in { + name = "${name}-oidc"; + value = { + image = "quay.io/oauth2-proxy/oauth2-proxy:v7.7.1-amd64"; + user = "${builtins.toString service.uid}"; + environmentFiles = [ + config.age.secrets.${service.envSecret}.path + ]; + + cmd = [ + "--reverse-proxy=true" + "--http-address=0.0.0.0:80" + "--skip-provider-button=true" + "--provider=oidc" + "--email-domain=*" + "--client-id=${service.clientId}" + "--upstream=${service.upstream}" + "--redirect-url=https://${service.domain}/oauth2/callback" + "--oidc-issuer-url=https://id.stupid.fish/oauth2/openid/${service.clientId}" + ] ++ service.extra; + }; + }) (builtins.attrNames cfg.services) + ); + }; +} \ No newline at end of file diff --git a/hosts/koi/containers/kanidm/server.toml b/hosts/koi/containers/kanidm/server.toml new file mode 100644 index 0000000..6c857e6 --- /dev/null +++ b/hosts/koi/containers/kanidm/server.toml @@ -0,0 +1,9 @@ +bindaddress = "0.0.0.0:8443" +adminbindpath = "/tmp/kanidm.sock" +trust_x_forward_for = true +db_path = "/data/db/kanidm.db" +tls_chain = "/data/chain.pem" +tls_key = "/data/key.pem" + +domain = "id.stupid.fish" +origin = "https://id.stupid.fish" \ No newline at end of file diff --git a/hosts/koi/containers/siyuan/default.nix b/hosts/koi/containers/siyuan/default.nix index d3d3b92..9235f74 100644 --- a/hosts/koi/containers/siyuan/default.nix +++ b/hosts/koi/containers/siyuan/default.nix @@ -9,7 +9,7 @@ let in { imports = [ (secrets.declare [{ - name = "siyuan-teidesu-authentik-env"; + name = "siyuan-teidesu-proxy-env"; owner = "siyuan-teidesu"; }]) ]; @@ -30,33 +30,30 @@ in { ]; cmd = [ "--workspace=/data" ]; environment = { - # we manage auth via authentik + # we manage auth via openid-proxy SIYUAN_ACCESS_AUTH_CODE_BYPASS = "true"; }; user = builtins.toString UID; }; - virtualisation.oci-containers.containers.siyuan-teidesu-authentik = { - image = "ghcr.io/goauthentik/proxy"; - environment = { - AUTHENTIK_HOST = "https://id.stupid.fish"; - }; - user = builtins.toString UID; - environmentFiles = [ - (secrets.file config "siyuan-teidesu-authentik-env") - ]; - }; - systemd.tmpfiles.rules = [ "d /srv/siyuan-teidesu 0700 ${builtins.toString UID} ${builtins.toString UID} -" ]; + desu.openid-proxy.services.siyuan-teidesu = { + clientId = "teidesu-siyuan"; + domain = "siyuan.tei.su"; + upstream = "http://siyuan-teidesu.docker:6806"; + envSecret = "siyuan-teidesu-proxy-env"; + uid = UID; + }; + services.nginx.virtualHosts."siyuan.tei.su" = { forceSSL = true; useACMEHost = "tei.su"; locations."/" = { - proxyPass = "http://siyuan-teidesu-authentik.docker:9000$request_uri"; + proxyPass = "http://siyuan-teidesu-oidc.docker$request_uri"; proxyWebsockets = true; }; }; diff --git a/secrets/kanidm-tls-cert.age b/secrets/kanidm-tls-cert.age new file mode 100644 index 0000000000000000000000000000000000000000..89828f5219cf570f74fb2bd46d7fb3a322fbbacf GIT binary patch literal 2237 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 Lx-8`mrGbsb)B#1q literal 0 HcmV?d00001 diff --git a/secrets/kanidm-tls-key.age b/secrets/kanidm-tls-key.age new file mode 100644 index 0000000000000000000000000000000000000000..9cd0dfd9c85335ec5bfea27a1e0bf412d3d02544 GIT binary patch literal 3484 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 KO@Jhc2O1Vf#hn)b literal 0 HcmV?d00001 diff --git a/secrets/openid-proxy-env.age b/secrets/openid-proxy-env.age new file mode 100644 index 0000000..810e4b5 --- /dev/null +++ b/secrets/openid-proxy-env.age @@ -0,0 +1,6 @@ +age-encryption.org/v1 +-> ssh-ed25519 sj88Xw em5uDRlc3WU8cHrelbBNgb1TY4DQna/GC4MvhRVCJ3U +eqyrs56AvN4+wVjH58meq8milx1wnXRhF6bd122tlmQ +--- U9jh49an0uX5qumssc7TXc9n+yO7b2dtZ3Y7NmjsaIE +f܎ceu7<P_{bTbԋN ssh-ed25519 sj88Xw K4t0UbmCo9hVvB3k0ut17zjnN/SrqjiCRokNy4CSvi4 -b067KvwE3J3NrXY5ZANkoUdS0UTTbkWWrCpsWtS0eP8 ---- 7cx4kHSwSvsAlAMvfM/lGr3B2QhmD6vhNdFSzLAnUuo -frrx/E!~ZMTj@'EcnVsqyqt zQb~DiVPtD_PHbr_IdN}xGcihJHAQrJLQq+3Qb<~CYC>#Ic27ceL~{yvV@pdkOJaFw zY-dj}IeKDAIe2$fOjlw`Ni}nCT3AA9LU3blIdgV4ZFdSSEiE8wPeUtEVKGK`I9F{$ zXh~#gbW%lZST9#^H8OEmbZjtUWNK<@M`L$&S$7J?FR~b1BeyTflh?%gxJkq6D$^)% zD4d#a>EgcdNh(9ZQOjWZJhMv!ZI+GcOATyko%91>s+HYCkixciDn@MY8EaYR+TChX|hP zvOQ4y`HfZ}#^Qr{9M2M#c_1QyEU_>1(hT`1c(SOX0ib^LL3iO)?~Eb%*`sh=ngHhA EqSc3&?EnA( literal 0 HcmV?d00001