Add keycloak

Keycloak is a login provider that we can host to give us SSO. This is preferable
to all of our services having different authentication capabilities, logins etc.
(e.g. mailu doesn't support 2fa: <https://github.com/Mailu/Mailu/issues/2222>!)

Change-Id: Ic0a5238a03d4d0b8a270c29a270c579b00aea799
diff --git a/flake.nix b/flake.nix
index 7d0d732..838808d 100644
--- a/flake.nix
+++ b/flake.nix
@@ -61,6 +61,7 @@
               ./modules/git.nix
               ./modules/grafana.nix
               ./modules/home-manager-users.nix
+              ./modules/keycloak.nix
               ./modules/kitty.nix
               ./modules/loginctl-linger.nix
               ./modules/matrix.nix
diff --git a/modules/caddy/caddyfile.nix b/modules/caddy/caddyfile.nix
index 243e721..0b2acf4 100644
--- a/modules/caddy/caddyfile.nix
+++ b/modules/caddy/caddyfile.nix
@@ -370,6 +370,7 @@
             }
           ))
           (HTTPReverseProxyRoute [ "passwords.clicks.codes" ] [ "localhost:8452" ])
+          (HTTPReverseProxyRoute [ "login.clicks.codes" ] [ "localhost:9083" ])
           (HTTPReverseProxyRoute [
             "syncthing.clicks.codes"
             "syncthing.coded.codes"
diff --git a/modules/keycloak.nix b/modules/keycloak.nix
new file mode 100644
index 0000000..d196ac9
--- /dev/null
+++ b/modules/keycloak.nix
@@ -0,0 +1,25 @@
+{ config, ... }: {
+  services.keycloak = {
+    enable = true;
+    settings = {
+      http-host = "127.0.0.1";
+      http-port = 9083;
+      https-port = 9084;
+      http-enabled = true;
+
+      proxy = "edge";
+
+      # https-port = 9084;
+      hostname = "login.clicks.codes";
+      hostname-strict = false;
+
+      https-certificate-file = "/var/keycloak/login.clicks.codes.rsa.cert.pem";
+      https-certificate-key-file = "/var/keycloak/login.clicks.codes.rsa.private.pem";
+    };
+    database = {
+      createLocally = false;
+      port = config.services.postgresql.port;
+      passwordFile = config.sops.secrets.clicks_keycloak_db_password.path;
+    };
+  };
+}
diff --git a/modules/postgres.nix b/modules/postgres.nix
index 7a5074a..d2844c1 100644
--- a/modules/postgres.nix
+++ b/modules/postgres.nix
@@ -13,6 +13,7 @@
     ensureDatabases = [
       "vaultwarden"
       "privatebin"
+      "keycloak"
     ];
 
     ensureUsers = [
@@ -30,6 +31,12 @@
         };
       }
       {
+        name = "keycloak";
+        ensurePermissions = {
+          "DATABASE keycloak" = "ALL PRIVILEGES";
+        };
+      }
+      {
         name = "vaultwarden";
         ensurePermissions = {
           "DATABASE vaultwarden" = "ALL PRIVILEGES";
@@ -72,6 +79,7 @@
     )
     (lib.mkAfter (lib.pipe [
       { user = "clicks_grafana"; passwordFile = config.sops.secrets.clicks_grafana_db_password.path; }
+      { user = "keycloak"; passwordFile = config.sops.secrets.clicks_keycloak_db_password.path; }
       { user = "vaultwarden"; passwordFile = config.sops.secrets.clicks_bitwarden_db_password.path; }
       { user = "privatebin"; passwordFile = config.sops.secrets.clicks_privatebin_db_password.path; }
     ] [
@@ -84,6 +92,7 @@
 
   sops.secrets = lib.pipe [
     "clicks_grafana_db_password"
+    "clicks_keycloak_db_password"
     "clicks_bitwarden_db_password"
     "clicks_privatebin_db_password"
   ] [
diff --git a/secrets/postgres.json b/secrets/postgres.json
index 5bb202b..2b057d8 100644
--- a/secrets/postgres.json
+++ b/secrets/postgres.json
@@ -1,7 +1,10 @@
 {
-	"clicks_grafana_db_password": "ENC[AES256_GCM,data:tFByC3OyhRLkDlfjwq3Kmc7PnTHWmkXpXuqOGb2AzA9dkAijPggPhgvCrbkY8/oL8QwQDaI24+XV3U/8A2UwLbzu0L5oaWV/E4EJbyvi8UKp8Wg8Au25E0nD5tJZm7QQ3FVERgoUefcB8AEPJ4Z8Rgx1PuoBeun9toT1GkJtmuYNNHpOcFrbmaI/Qf1MP+yFZLYjvB1jz07V04RGTv4jow61lWFknS2aPJyat43Ogp64lIkfjen7zCvj3CWghfJx87uxeXsnFHMrRwfONozUdw19Bq1uLUJ7xvPqDtr/1WKi1xvBe5ez7/PkPslNJlIToIlL89xN/lOm2iQR2BNeXg==,iv:ruC4PzKpWYsz2qe0KImUo0YhRt2cisYx306yfPtzi6c=,tag:U8vg7w1zyqXAWH3WzNAHFA==,type:str]",
-	"clicks_bitwarden_db_password": "ENC[AES256_GCM,data:Xr9h/QVazxn1tzSNJgPH9uk6RE2bk0fNyOVNxSlflQ14wnna0xA9Uw4CcN9DbPyvBCvYsKrxTJOAzLOn/K4rc/G1C7dPKpGADiTH5O+1wX+PifrDHtQyuqvaarwO3QA20ZW48A==,iv:yKYDF3X9fNO+rbnguo5DAiXfCkjH06VkpGBjB9NGv/s=,tag:W8fAVyJGh6jQxwHMN+RAPg==,type:str]",
-	"clicks_privatebin_db_password": "ENC[AES256_GCM,data:FqTQzRxzv1Jelr7uPgQO0g9n653ilAriacvfj0ZQ7mnD8PEpE5SsZ4z8pOZJxCyIHFGE1n1iFudTJ3RXdSdsQ3Rxw7/eoz7IlblnQFQQKdTbpApNAkKyCKr5tUhUPqjTFixxGwNO8yxNUCTtdrBBbVZoxyAwl8lWXhI78fiFGA8=,iv:lqCfX0P+xHtIJfRW1DUjfwNlmvRCkg+y+qogsETL0RM=,tag:H855oMdkflQP6Drlefv6dA==,type:str]",
+	"clicks_grafana_db_password": "ENC[AES256_GCM,data:sw/zrpFiCe2rLn73M8GE/uWbMsaGmVcHM93mEb4ZEFXTJEPN1Zx47/VyKmMmadMQDWHECCsqmXyjqCy2IG3T7ox5A8bVHYIyAhWz8hi+Ao9d7M76RJl6HhrhJF89+64/F+PTN0z0+5KgC4nMjGbYaJj8A83Fh2bpUGgow+eDghUQf+GDhXJGIsYqXpyxZdl1YjmD0Gj0AK4Em+t4gPlY82CbiyRhEag+BOkSxSirBHYlqiKrFisrKtzHt/gYZQZZMt3tNRqY2S71pJospxJ+ZFhgVzyt2euxZYftKj7rehvLHdvfr7VUsJdwLK2hC7RRzMlEojnEqPXRQ8Uee2XiTg==,iv:5sdPFVdo1MBMF2CN7UtfKYf83WnnW5Hpdy0yoGyPAC0=,tag:SxC4DuVYiNcvJrGvzYk0Fw==,type:str]",
+	"clicks_keycloak_db_password": "ENC[AES256_GCM,data:duwBc+bJnfn1Erzi1FijzOtrruTsdOZDFmVA6OWMwrN/YBE8Dy5PEfMJ0acZ2Wc544426Dp70fHppIMqdoJPFbOSsmgysLoxfQLxk7iJaO56N6ZUMMk4qQT7Y/u2m5uJS43R1O5wRz+C6IYCu0ixXOj/+6dDigk2Ur2i3OjrS/4=,iv:X1DSnWSO1js9bQVgMHX1wN1NkfFylnGHAluoNQ7ztpw=,tag:cKPfZU0j4NYTIYlp6UXzGA==,type:str]",
+	"clicks_bitwarden_db_password": "ENC[AES256_GCM,data:NR5ezR022u5MJmMq1veRbx89gRUS07pJq6d4i8lfgSM3uhrq1LtgP7KT5T9my1koBynGYTPvA2TMUQbePrVQYiPVMWzFRaYHzV29xKJqbTC6s7KdWLs6VxeRQMzCfmakqYf2mw==,iv:wEjSkr7NJvE8ZcEgNe6zL5h3UFwBidKySUCD6elpmeA=,tag:B0bujxD6F+OyytMCRrwltw==,type:str]",
+	"clicks_privatebin_db_password": "ENC[AES256_GCM,data:T+NIe961xTXO/B9RCr/KlhlOLHcz8RfVnCn/+PexGUSeQ9suQ1wdILt14GvEuAUczN3bTT1sy9wRM656lAwWA/nsF3yML+5VwQo/aKo2R66Ga9Lnslg8tquQuwEpWb2tRg6BDEwUl0iLrvGKODAKuu3ClXJEJTASeTCZMv0jUQY=,iv:NFsZbKKCfji9DGDRQHFfH+insWGxbS6xqsng40ckC4M=,tag:LR5Ay8ZowkD7s3pEHjws2w==,type:str]",
+	"clicks_gerrit_db_password": "ENC[AES256_GCM,data:0QJH5KEi25NG3EvN9HF7Y7DeSemp6imouqoytwLZldpD9XlCa1wt14Re3ykADfIUzp6OjGIHY6XHdglN1/pqUOzmxb+3rG82FyNgUI9O91F8+1g8U7TjaQkfjlWudxhlDkwnQNSu5FcBuJ04BEtBWJ7l/POJ5BVi4bX9N9hGsdc=,iv:564WVx1tkYHvebqXKVaBFBmu9NYbDgiQ8YVmMMQFqPQ=,tag:zfWgB3Fje4sogYKGiiu/lg==,type:str]",
+	"clicks_nextcloud_db_password": "ENC[AES256_GCM,data:Tu4BRo0qkpp+RXYlQO7PIfZM40tquvQUt9hbtZdKRotrOg81CGjZLISjNELr8pLCQK4AAfCJ7UPdR0ZztJfhrj5vPnaQM/2nHO4aMuhfnkOX00MDJhum/j1I0Adx/Au9zAaIONaKMBXLmX/g3FU2s6Yp7OtZ7/4FoWAYbG4zSbY=,iv:LjlkKkVNybg9EU9pytsmyYJrFMym0RmSvIFI/KKcpyc=,tag:rPyOh+KtAmo9OeY0Wm1sCQ==,type:str]",
 	"sops": {
 		"kms": null,
 		"gcp_kms": null,
@@ -10,19 +13,19 @@
 		"age": [
 			{
 				"recipient": "age15mv77dpnh5762gk5rsw2u79uza4tg8cu6r3nlwjudlzmdqqck3ss6mg9dy",
-				"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRa01sZHdFalJFTFVxQ25K\nMms0c1pIeUpHK1MvcGZWN1hwS094OC9RMEZ3CnR0a2xzbmxScUhOcE5CRVdjZFha\nV2h4TVV3TGxFV0tUK3lQSDl4OWpEQlEKLS0tIGVkRFp4N25maW83TGlxTy9WRGxQ\nQ3NESWw5VFNGRlFKRnRlemNsK1NRSHcK34seVyvvseCzn3abwcCAu3rhOUQYMUMC\nLepvm8ahPRcI5Hl3cbX6c1Td6QQ5kPC3QYyngKX4M+jhWMfpuDbwHQ==\n-----END AGE ENCRYPTED FILE-----\n"
+				"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByRlE1TCtxTTd4RTFtVG9l\na2ZKeEZZMC9YZDBEc0FCMXQ3ekMwczdhZUFvCkcvSGRsVmVDSjdNMW4zNVhKdFhv\nQjdnZXBJMmdORG0wdzVKQ1ZDZ0xVbUEKLS0tIERtK1J6UWQzVTZsUGVrcFFYelBB\nWnFQNGI3Y0JzbHNlcjNxL2FjaVh2Z00KMkxXtxMB656xgwFDd3SI1HeTsyFQ18Rj\nPQZYwbVHgQ/KUo/t6zRFN6RwNQ+eqcgl+x/eSilUlFf8x3sg96OKhg==\n-----END AGE ENCRYPTED FILE-----\n"
 			},
 			{
 				"recipient": "age1m7k864feyuezllp2hj4edkccn36rthrvfw969j6f0l3c0mhh5emsnfx6pd",
-				"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMMThlZzkzUW1hWUp0djZl\nMmVsRzZNdnA0QmRXaU5JeXBVcDB2Lytya3hNCitSVi8rdkp3UmVXWTV0MGV3QWJF\nZHJxMk1UaDF0b01qTm1sL3ozNjJpZmsKLS0tIGtDcFF2cTFmS3d2UFNYamFCOFdm\nUnk1dUhvR1R6cTlzNVVqdU5CdG9YS2MK0yfOBwjloIjtaWtJf6hRsjm9LCOHbB96\nzRl95IazEXhBUwB9WC2RsH6l8/Ja4AHvTnCmznItbvQL/LassZsMGQ==\n-----END AGE ENCRYPTED FILE-----\n"
+				"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpMjNRUVp1b0R1OWwybGVI\nbjVvWS9CblRQdW9yMldZVmhNUU4yWU9QSGxzCjl0SWlSbHBkektucTJzUUgrcEhp\nMm5weFp0WGJJdUU3ZHZUaXBxUEl1WEkKLS0tIFliZXJDS0V4TlJkdVpMYks5UTNa\nT2hhVzl2cDdUWUVYSXhYbVpET2k0SWcKmqiJMB2N77WenKzx18ADkg56YEW+PNk2\nZX3vvcuU2eLZ1u6O0y7melm/CG2hgYi/oXV+c7Xddva8LN3tbo77MA==\n-----END AGE ENCRYPTED FILE-----\n"
 			},
 			{
 				"recipient": "age1fxxnmkeuqhhct93c43pwkzhuzzq8857s5hye6pgfpku70kjn4ecqtamfqr",
-				"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByN0hMaXF2S2xiTTNqbm9h\nSGNieGNjNThJTk0wZXNQejBXaW9MOEp2akNFCm8rWVc3WG9pVndERFZzUnFOZnVG\naFBxTENlQ0x0ZjdqUHF6SFBPKzQ4bUEKLS0tIGVJTmdveTZrNTRiSTl0cHRQeWhl\nY2RmUTVQVTNoMFhLdkc3WFZEcHAycnMKqr42TSx7Pqcu62XgX4gj/iq2tbkZjFxg\nOcWBsLzqOsu/r0w5cK2Ple6JFGIJwmT2SqVqZh1pPbPwYHHXHbEphg==\n-----END AGE ENCRYPTED FILE-----\n"
+				"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuN2k4QnR0UnhXTHFTUUlz\ndlJpYVBaeHB5eitocktmV1hPN2Vxa0pjYUhBClc1REh1UndRM1F3L082ZElrZkJZ\nWk56Z1Vrd0xla1FSb1k2eGN4UXlud2MKLS0tIEczMWVuVkIwOGNRamE2OW1VNWlT\nZXdBTUxMNnRUSlhHbGtndzZISm1jM1kKvg5s5u68gW9PeQ8cYRqBwqHu352bv1jQ\nQUSPxQpGZilZz95BUMEniAa75ljAD7b9v9zmxLDRreC+4L/thCdMFQ==\n-----END AGE ENCRYPTED FILE-----\n"
 			}
 		],
-		"lastmodified": "2023-07-30T14:59:54Z",
-		"mac": "ENC[AES256_GCM,data:Miimq3cBEfHhtF+zUWzYUs0pGfEEvA0ZXJzgJabH19FfIMSR6dXpdN807i7Of1AGYNYlGpxS3aPbDrpB5wsKkDFwzTCIZDsaKeq0G/YVzNVxA7BVdw/I3048f/VqrGSenwPhbpElADrGsL+waXGbQ/GA5F4387yihi6ZenxeQIs=,iv:Tx4bRqLVaaNACumwumZrCPcfTg4i19TSpwd1Ifu1PmE=,tag:nFg7zNFiR9vah4bgId0jjA==,type:str]",
+		"lastmodified": "2023-10-07T21:56:51Z",
+		"mac": "ENC[AES256_GCM,data:L4f1WW6Wjh/2qEHyqLR16kpM2dmxft9He9E7bADwGk40CKPq2pdh+3MvHsHIPHDoo87f3UO7yJUWsES80vKwjaLIqkeRfVJRFP4Ci/m8KZtWJBtEdaHfU2nKegBTmb797CvMZN8rAvn/AeFl53sNK0QtYAnJhIctZ72rh2kGepQ=,iv:phi5SwKZ8ESWKZntKUkZWfl8NdTvH3Ax7rnuT2bz4vM=,tag:fFQOutSnTO9no1atbravLA==,type:str]",
 		"pgp": null,
 		"unencrypted_suffix": "_unencrypted",
 		"version": "3.7.3"