diff --git a/flake.lock b/flake.lock
index a3923c5..2fdef4b 100644
--- a/flake.lock
+++ b/flake.lock
@@ -83,6 +83,22 @@
"type": "github"
}
},
+ "blobs": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1604995301,
+ "narHash": "sha256-wcLzgLec6SGJA8fx1OEN1yV/Py5b+U5iyYpksUY/yLw=",
+ "owner": "simple-nixos-mailserver",
+ "repo": "blobs",
+ "rev": "2cccdf1ca48316f2cfd1c9a0017e8de5a7156265",
+ "type": "gitlab"
+ },
+ "original": {
+ "owner": "simple-nixos-mailserver",
+ "repo": "blobs",
+ "type": "gitlab"
+ }
+ },
"cosmic-manager": {
"inputs": {
"flake-parts": [
@@ -200,6 +216,22 @@
"type": "github"
}
},
+ "flake-compat_2": {
+ "flake": false,
+ "locked": {
+ "lastModified": 1747046372,
+ "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
+ "type": "github"
+ },
+ "original": {
+ "owner": "edolstra",
+ "repo": "flake-compat",
+ "type": "github"
+ }
+ },
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
@@ -291,6 +323,54 @@
"type": "github"
}
},
+ "git-hooks": {
+ "inputs": {
+ "flake-compat": [
+ "simple-nixos-mailserver",
+ "flake-compat"
+ ],
+ "gitignore": "gitignore",
+ "nixpkgs": [
+ "simple-nixos-mailserver",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1742649964,
+ "narHash": "sha256-DwOTp7nvfi8mRfuL1escHDXabVXFGT1VlPD1JHrtrco=",
+ "owner": "cachix",
+ "repo": "git-hooks.nix",
+ "rev": "dcf5072734cb576d2b0c59b2ac44f5050b5eac82",
+ "type": "github"
+ },
+ "original": {
+ "owner": "cachix",
+ "repo": "git-hooks.nix",
+ "type": "github"
+ }
+ },
+ "gitignore": {
+ "inputs": {
+ "nixpkgs": [
+ "simple-nixos-mailserver",
+ "git-hooks",
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1709087332,
+ "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
+ "owner": "hercules-ci",
+ "repo": "gitignore.nix",
+ "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
+ "type": "github"
+ },
+ "original": {
+ "owner": "hercules-ci",
+ "repo": "gitignore.nix",
+ "type": "github"
+ }
+ },
"gnome-shell": {
"flake": false,
"locked": {
@@ -440,6 +520,22 @@
"type": "github"
}
},
+ "nixpkgs-25_05": {
+ "locked": {
+ "lastModified": 1747610100,
+ "narHash": "sha256-rpR5ZPMkWzcnCcYYo3lScqfuzEw5Uyfh+R0EKZfroAc=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "ca49c4304acf0973078db0a9d200fd2bae75676d",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-25.05",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
"nixpkgs-lib": {
"locked": {
"lastModified": 1751159883,
@@ -494,6 +590,7 @@
"nixos-anywhere": "nixos-anywhere",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay",
+ "simple-nixos-mailserver": "simple-nixos-mailserver",
"stylix": "stylix",
"systems": "systems_2",
"terranix": "terranix",
@@ -521,6 +618,31 @@
"type": "github"
}
},
+ "simple-nixos-mailserver": {
+ "inputs": {
+ "blobs": "blobs",
+ "flake-compat": "flake-compat_2",
+ "git-hooks": "git-hooks",
+ "nixpkgs": [
+ "nixpkgs"
+ ],
+ "nixpkgs-25_05": "nixpkgs-25_05"
+ },
+ "locked": {
+ "lastModified": 1747965231,
+ "narHash": "sha256-BW3ktviEhfCN/z3+kEyzpDKAI8qFTwO7+S0NVA0C90o=",
+ "owner": "simple-nixos-mailserver",
+ "repo": "nixos-mailserver",
+ "rev": "53007af63fade28853408370c4c600a63dd97f41",
+ "type": "gitlab"
+ },
+ "original": {
+ "owner": "simple-nixos-mailserver",
+ "ref": "nixos-25.05",
+ "repo": "nixos-mailserver",
+ "type": "gitlab"
+ }
+ },
"snowfall-lib": {
"inputs": {
"flake-compat": "flake-compat",
diff --git a/flake.nix b/flake.nix
index 3204855..e9c73aa 100644
--- a/flake.nix
+++ b/flake.nix
@@ -91,10 +91,16 @@
flake-parts.follows = "flake-parts";
};
};
+ simple-nixos-mailserver = {
+ url = "gitlab:simple-nixos-mailserver/nixos-mailserver/nixos-25.05";
+ inputs = {
+ nixpkgs.follows = "nixpkgs";
+ };
+ };
};
outputs =
- inputs@{ self, ... }:
+ inputs:
let
inputNixosModules = [
inputs.disko.nixosModules.disko
diff --git a/nix/modules/nixos/infrastructure/hetzner-instance/default.nix b/nix/modules/nixos/infrastructure/hetzner-instance/default.nix
index 88cd2c8..c9d9546 100644
--- a/nix/modules/nixos/infrastructure/hetzner-instance/default.nix
+++ b/nix/modules/nixos/infrastructure/hetzner-instance/default.nix
@@ -77,6 +77,7 @@ in
description = "The server type to create";
default = null;
};
+
extraFirewallRules = lib.mkOption {
type = lib.types.listOf lib.types.attrs;
description = "Extra firewall rules added to the instance";
@@ -153,7 +154,7 @@ in
initial_image = "debian-12";
rdns = lib.mkIf cfg.mapRdns fqdn;
ssh_keys = [ config.khscodes.hcloud.output.data.ssh_key.khs.id ];
- user_data = provisioningUserData;
+ user_data = builtins.toJSON provisioningUserData;
};
khscodes.cloudflare = {
enable = true;
@@ -199,8 +200,16 @@ in
message = "Must set config.khscodes.networking.fqdn when using opentofu";
}
];
-
- khscodes.services.read-vault-auth-from-userdata.url = "http://169.254.169.254/latest/user-data";
+ khscodes.services.openssh = {
+ enable = true;
+ hostCertificate = {
+ enable = true;
+ };
+ };
+ khscodes.services.read-vault-auth-from-userdata = {
+ url = "http://169.254.169.254/latest/user-data";
+ doubleDecodeJsonData = true;
+ };
khscodes.infrastructure.provisioning.pre = {
modules = modules;
};
diff --git a/nix/modules/nixos/infrastructure/khs-openstack-instance/default.nix b/nix/modules/nixos/infrastructure/khs-openstack-instance/default.nix
index 39fd74b..5f81a88 100644
--- a/nix/modules/nixos/infrastructure/khs-openstack-instance/default.nix
+++ b/nix/modules/nixos/infrastructure/khs-openstack-instance/default.nix
@@ -135,7 +135,7 @@ in
flavor = cfg.flavor;
ssh_public_key = cfg.ssh_key;
firewall_rules = firewallRules;
- user_data = provisioningUserData;
+ user_data = builtins.toJSON provisioningUserData;
};
khscodes.unifi.enable = true;
khscodes.unifi.static_route.compute = {
diff --git a/nix/modules/nixos/infrastructure/provisioning/default.nix b/nix/modules/nixos/infrastructure/provisioning/default.nix
index 5fcb305..e48846f 100644
--- a/nix/modules/nixos/infrastructure/provisioning/default.nix
+++ b/nix/modules/nixos/infrastructure/provisioning/default.nix
@@ -30,6 +30,10 @@ let
search = "cloudflare/cloudflare";
endpoint = "cloudflare";
}
+ {
+ search = "hetznercloud/hcloud";
+ endpoint = "hcloud";
+ }
{
search = "terraform-provider-openstack/openstack";
endpoint = "openstack";
@@ -79,7 +83,7 @@ in
pre = provisioning;
post = provisioning;
instanceUserData = lib.mkOption {
- type = lib.types.str;
+ type = (pkgs.formats.json { }).type;
description = "User data that should be added to the instance during provisioning";
default = "";
};
diff --git a/nix/modules/nixos/infrastructure/vault-loki-sender/default.nix b/nix/modules/nixos/infrastructure/vault-loki-sender/default.nix
index 5b95b21..e727124 100644
--- a/nix/modules/nixos/infrastructure/vault-loki-sender/default.nix
+++ b/nix/modules/nixos/infrastructure/vault-loki-sender/default.nix
@@ -66,7 +66,9 @@ in
owner = "alloy";
group = "alloy";
perms = "0600";
- reloadOrRestartUnits = [ "alloy.service" ];
+ # Alloy doesn't seem to reload the certificates when using just reload
+ # so restart the unit.
+ restartUnits = [ "alloy.service" ];
}
];
khscodes.services.alloy = {
diff --git a/nix/modules/nixos/infrastructure/vault-prometheus-sender/default.nix b/nix/modules/nixos/infrastructure/vault-prometheus-sender/default.nix
index 61fe177..1b76ee3 100644
--- a/nix/modules/nixos/infrastructure/vault-prometheus-sender/default.nix
+++ b/nix/modules/nixos/infrastructure/vault-prometheus-sender/default.nix
@@ -66,7 +66,7 @@ in
owner = "alloy";
group = "alloy";
perms = "0600";
- reloadOrRestartUnits = [ "alloy.service" ];
+ restartUnits = [ "alloy.service" ];
}
];
khscodes.services.alloy = {
diff --git a/nix/modules/nixos/infrastructure/vault-server-approle/default.nix b/nix/modules/nixos/infrastructure/vault-server-approle/default.nix
index e2f21bf..b9fa4a5 100644
--- a/nix/modules/nixos/infrastructure/vault-server-approle/default.nix
+++ b/nix/modules/nixos/infrastructure/vault-server-approle/default.nix
@@ -131,11 +131,9 @@ in
};
# I can only provide the user data if the stage is pre (along with the instance creation)
# Also I should probably find a way of injecting this in a nicer way than this mess.
- khscodes.infrastructure.provisioning.instanceUserData = lib.mkIf (cfg.stage == "pre") ''
- {
- "VAULT_ROLE_ID": "''${ vault_approle_auth_backend_role.${lib.khscodes.sanitize-terraform-name cfg.role_name}.role_id }",
- "VAULT_SECRET_ID_WRAPPED": "''${ vault_approle_auth_backend_role_secret_id.${lib.khscodes.sanitize-terraform-name cfg.role_name}.wrapping_token }"
- }
- '';
+ khscodes.infrastructure.provisioning.instanceUserData = lib.mkIf (cfg.stage == "pre") {
+ VAULT_ROLE_ID = "\${ vault_approle_auth_backend_role.${lib.khscodes.sanitize-terraform-name cfg.role_name}.role_id }";
+ VAULT_SECRET_ID_WRAPPED = "\${ vault_approle_auth_backend_role_secret_id.${lib.khscodes.sanitize-terraform-name cfg.role_name}.wrapping_token }";
+ };
};
}
diff --git a/nix/modules/nixos/os/auto-update/default.nix b/nix/modules/nixos/os/auto-update/default.nix
index a0dcb4c..cae36f0 100644
--- a/nix/modules/nixos/os/auto-update/default.nix
+++ b/nix/modules/nixos/os/auto-update/default.nix
@@ -14,7 +14,7 @@ let
pkgs.uutils-coreutils-noprefix
pkgs.nix
];
- name = "nixos-prepare-upgrade";
+ name = "nixos-upgrade-prepare-flake";
text = ''
@@ -28,7 +28,7 @@ let
echo -n ${inputs.self.outPath} > ${upgradeVersion}
fi
cd ${upgradePath}
- NIX_CONFIG="extra-experimental-features=flake nix-command" nix flake update
+ nix --extra-experimental-features "nix-command flakes" flake update
'';
};
in
diff --git a/nix/modules/nixos/security/acme/default.nix b/nix/modules/nixos/security/acme/default.nix
index d4955d0..e8f568a 100644
--- a/nix/modules/nixos/security/acme/default.nix
+++ b/nix/modules/nixos/security/acme/default.nix
@@ -29,7 +29,7 @@ in
credentialsFile = vaultAgentCredentialsFile;
};
};
- khscodes.infrastructure.vault-server-approle = {
+ khscodes.infrastructure.vault-server-approle = lib.mkIf cfg.dns01Enabled {
enable = true;
policy = {
"${cloudflareSecret}" = {
diff --git a/nix/modules/nixos/services/read-vault-auth-from-userdata/default.nix b/nix/modules/nixos/services/read-vault-auth-from-userdata/default.nix
index e150534..a5c4553 100644
--- a/nix/modules/nixos/services/read-vault-auth-from-userdata/default.nix
+++ b/nix/modules/nixos/services/read-vault-auth-from-userdata/default.nix
@@ -10,6 +10,7 @@ in
{
options.khscodes.services.read-vault-auth-from-userdata = {
enable = lib.mkEnableOption "Enables reading vault auth information from instance userdata";
+ doubleDecodeJsonData = lib.mkEnableOption "Enables double decoding the JSON data. Used by the hcloud provider as hetzner encodes the entire user data as a string";
url = lib.mkOption {
type = lib.types.str;
description = "URL to retrieve instance metadata from";
@@ -29,6 +30,9 @@ in
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
+ environment = {
+ DOUBLE_DECODE = if cfg.doubleDecodeJsonData then "yes" else "no";
+ };
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
@@ -43,7 +47,11 @@ in
pkgs.systemd
];
text = ''
+ DOUBLE_DECODE="''${DOUBLE_DECODE:-no}"
userdata="$(curl ${lib.escapeShellArg cfg.url})"
+ if [[ "$DOUBLE_DECODE" == "yes" ]]; then
+ userdata="$(echo "$userdata" | jq --raw-output '.')"
+ fi
role_id="$(echo "$userdata" | jq --raw-output '.VAULT_ROLE_ID')"
secret_id_wrapped="$(echo "$userdata" | jq --raw-output '.VAULT_SECRET_ID_WRAPPED')"
if [[ -f ${cacheFilePath} ]]; then
diff --git a/nix/modules/terranix/hcloud/default.nix b/nix/modules/terranix/hcloud/default.nix
index af515a6..1121dc7 100644
--- a/nix/modules/terranix/hcloud/default.nix
+++ b/nix/modules/terranix/hcloud/default.nix
@@ -120,6 +120,7 @@ in
ignore_changes = [
"ssh_keys"
"public_net"
+ "user_data"
"image"
];
};
diff --git a/nix/systems/aarch64-linux/kas.codes/default.nix b/nix/systems/aarch64-linux/kas.codes/default.nix
index d1fa974..c0082e9 100644
--- a/nix/systems/aarch64-linux/kas.codes/default.nix
+++ b/nix/systems/aarch64-linux/kas.codes/default.nix
@@ -3,7 +3,11 @@
...
}:
{
- imports = [ "${inputs.self}/nix/profiles/nixos/hetzner-server.nix" ];
+ imports = [
+ "${inputs.self}/nix/profiles/nixos/hetzner-server.nix"
+ ./mailserver
+ ./forgejo
+ ];
khscodes.infrastructure.hetzner-instance = {
enable = true;
mapRdns = true;
diff --git a/nix/systems/aarch64-linux/kas.codes/dkim.nix b/nix/systems/aarch64-linux/kas.codes/dkim.nix
deleted file mode 100644
index 6a849d9..0000000
--- a/nix/systems/aarch64-linux/kas.codes/dkim.nix
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- khscodes.services.vault-agent.templates = [
- {
- contents = ''
- {{- with secret "forgejo/data/mailserver/dkim" -}}
- {{ .Data.data.dkim_private_key }}
- {{- end -}}
- '';
- destination = "/var/lib/vault-agent/mailserver/dkim/private.key";
- perms = "0600";
- owner = "rspamd";
- group = "rspamd";
- restartUnits = [
- "rspamd.service"
- "postfix.service"
- ];
- }
- {
- contents = ''
- {{- with secret "forgejo/data/mailserver/forgejo-user" -}}
- {{ .Data.data.hashed_password }}
- {{- end -}}
- '';
- destination = "/var/lib/vault-agent/mailserver/users/forgejo.passwd.hash";
- perms = "0600";
- owner = "rspamd";
- group = "rspamd";
- restartUnits = [
- "rspamd.service"
- "postfix.service"
- ];
- }
- ];
-}
diff --git a/nix/systems/aarch64-linux/kas.codes/forgejo/default.nix b/nix/systems/aarch64-linux/kas.codes/forgejo/default.nix
new file mode 100644
index 0000000..37743a0
--- /dev/null
+++ b/nix/systems/aarch64-linux/kas.codes/forgejo/default.nix
@@ -0,0 +1,202 @@
+{ config, pkgs, ... }:
+let
+ home_forgejo = pkgs.writeText "home_forgejo.tmpl" ''
+
+
+
+
+ This is just a personal self hosted software forge for my projects. I might publish a few things here for public consumption.
+
+
+
+
+
+ This server is running Forgejo. Click the link to learn more.
+
+
+
+ '';
+ # This simply has the tag removed.
+ home = pkgs.writeText "home.tmpl" ''
+ {{template "base/head" .}}
+
+
+
+

+
+
+
+
+
+ {{template "home_forgejo" .}}
+
+ {{template "base/footer" .}}
+ '';
+in
+{
+ imports = [ ./oauth.nix ];
+ khscodes.services.vault-agent.templates = [
+ {
+ contents = ''
+ {{- with secret "forgejo/data/mailserver/users/forgejo" -}}
+ {{ .Data.data.password }}
+ {{- end -}}
+ '';
+ destination = "/var/lib/vault-agent/forgejo/mailserver/forgejo.passwd";
+ perms = "0600";
+ owner = "git";
+ group = "git";
+ restartUnits = [
+ "forgejo.service"
+ ];
+ }
+ ];
+ systemd.services.forgejo = {
+ unitConfig = {
+ ConditionPathExists = [ "/var/lib/vault-agent/forgejo/mailserver/forgejo.passwd" ];
+ };
+ };
+ services.forgejo = {
+ enable = true;
+ user = "git";
+ group = "git";
+ settings = {
+ DEFAULT = {
+ APP_NAME = "KAS: Codes";
+ };
+ server = rec {
+ DOMAIN = "kas.codes";
+ ROOT_URL = "https://${DOMAIN}";
+ };
+ session = {
+ COOKIE_SECURE = true;
+ };
+ service = {
+ DISABLE_REGISTRATION = true;
+ ENABLE_INTERNAL_SIGNIN = false;
+ };
+ repository = {
+ DEFAULT_REPO_UNITS = "repo.code,repo.releases,repo.issues,repo.packages,repo.actions";
+ };
+ mailer = {
+ ENABLED = true;
+ SMTP_ADDR = "kas.codes";
+ FROM = "forgejo@khs.codes";
+ USER = "forgejo@khs.codes";
+ };
+ "ui.meta" = {
+ AUTHOR = "Kaare Hoff Skovgaard ";
+ DESCRIPTION = "A self-hosted software forge for KAS/KHS";
+ KEYWORDS = "khs,kas,kastermester,code";
+ };
+ actions = {
+ DEFAULT_ACTIONS_URL = "https://kas.codes";
+ };
+ oauth2_client = {
+ ENABLE_AUTO_REGISTRATION = true;
+ USERNAME = "nickname";
+ ACCOUNT_LINKING = "disabled";
+ REGISTER_EMAIL_CONFIRM = false;
+ };
+ };
+ secrets.mailer.PASSWD = "/var/lib/vault-agent/forgejo/mailserver/forgejo.passwd";
+ lfs = {
+ enable = true;
+ };
+ database = {
+ type = "postgres";
+ user = "git";
+ name = "git";
+ };
+ dump = {
+ enable = true;
+ file = "forgejo-dump";
+ };
+ };
+
+ systemd.services.write-forgejo-templates = {
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" ];
+ before = [ "forgejo.service" ];
+ serviceConfig = {
+ Type = "oneshot";
+ ExecStart = pkgs.lib.getExe (
+ pkgs.writeShellApplication {
+ name = "write-forgejo-templates";
+ runtimeInputs = [ pkgs.uutils-coreutils-noprefix ];
+ text = ''
+ if [ ! -d /var/lib/forgejo/custom/templates ]; then
+ mkdir /var/lib/forgejo/custom/templates
+ fi
+ ln -sf ${home_forgejo} /var/lib/forgejo/custom/templates/home_forgejo.tmpl
+ ln -sf ${home} /var/lib/forgejo/custom/templates/home.tmpl
+ '';
+ }
+ );
+ };
+ };
+
+ users.users.forgejo-backup = {
+ isNormalUser = true;
+ home = "/home/forgejo-backup";
+ group = "forgejo-backup";
+ createHome = true;
+ openssh.authorizedKeys.keys = [
+ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ/hn4Q1+5KpViol+Kk7bUvWrka2hhKEXqUJVY0quQLu forgejo-backup@truenas.kaareskovgaard.net"
+ ];
+ };
+ users.groups.forgejo-backup = { };
+
+ systemd.timers.forgejo-dump = {
+ timerConfig = {
+ Unit = "forgejo-copy-dump.service";
+ };
+ };
+ systemd.services.forgejo-copy-dump = {
+ requires = [ "forgejo-dump.service" ];
+ after = [ "forgejo-dump.service" ];
+ wantedBy = [ "timers.target" ];
+
+ serviceConfig = {
+ Type = "oneshot";
+ ExecStart = pkgs.lib.getExe (
+ pkgs.writeShellApplication {
+ name = "forgejo-copy-dump";
+ runtimeInputs = [ pkgs.uutils-coreutils-noprefix ];
+ text = ''
+ mv /var/lib/forgejo/dump/forgejo-dump.zip /home/forgejo-backup/dump.zip
+ chown forgejo-backup:forgejo-backup /home/forgejo-backup/dump.zip
+ chmod 0640 /home/forgejo-backup/dump.zip
+ '';
+ }
+ );
+ };
+ };
+
+ khscodes.services.nginx = {
+ enable = true;
+ virtualHosts = {
+ "kas.codes" = {
+ extraConfig = ''
+ client_max_body_size 32M;
+ '';
+ locations."/" = {
+ proxyPass = "http://localhost:3000";
+ };
+ };
+ };
+ };
+ users.users.git = {
+ isSystemUser = true;
+ group = "git";
+ home = config.services.forgejo.stateDir;
+ useDefaultShell = true;
+ };
+ users.groups.git = { };
+}
diff --git a/nix/systems/aarch64-linux/kas.codes/forgejo/oauth.nix b/nix/systems/aarch64-linux/kas.codes/forgejo/oauth.nix
new file mode 100644
index 0000000..c8ab23c
--- /dev/null
+++ b/nix/systems/aarch64-linux/kas.codes/forgejo/oauth.nix
@@ -0,0 +1,79 @@
+{
+ pkgs,
+ lib,
+ config,
+ ...
+}:
+let
+ oauthSecretIdFile = "/run/forgejo/oauth_secret_id";
+ setApp = pkgs.writeShellApplication {
+ name = "forgejo-set-oauth-app";
+ runtimeInputs = [
+ pkgs.forgejo
+ pkgs.gnugrep
+ ];
+ text = ''
+ config="${config.services.forgejo.stateDir}/custom/conf/app.ini"
+ secret="$(cat ${oauthSecretIdFile})"
+
+ if gitea "--config=$config" admin auth list | grep -q "Kanidm" 2> /dev/null; then
+ echo "Oauth2 app already exists, updating not yet implemented"
+ exit 0
+ else
+ gitea "--config=$config" admin auth add-oauth \
+ --name "Kanidm" \
+ --key "forgejo" \
+ --secret "$secret" \
+ --auto-discover-url https://login.kaareskovgaard.net/oauth2/openid/forgejo/.well-known/openid-configuration \
+ --scopes "email profile" \
+ --skip-local-2fa \
+ --provider openidConnect \
+ --group-claim-name groups \
+ --admin-group admin \
+ --group-team-map-removal \
+ --group-team-map '{"nix": ["nix"], "actions": ["actons"]}'
+ fi
+ '';
+ };
+in
+{
+ khscodes.services.vault-agent.templates = [
+ {
+ contents = ''
+ {{- with secret "kanidm/data/apps/forgejo" -}}
+ {{ .Data.data.basic_secret }}
+ {{- end -}}
+ '';
+ destination = oauthSecretIdFile;
+ perms = "0600";
+ owner = "git";
+ group = "git";
+ restartUnits = [
+ "forgejo-setup-oauth.service"
+ ];
+ }
+ ];
+ khscodes.infrastructure.vault-server-approle.policy = {
+ "kanidm/data/apps/forgejo" = {
+ capabilities = [ "read" ];
+ };
+ };
+ systemd.services.forgejo-setup-oauth = {
+ enable = true;
+ wants = [ "vault-agent-openbao.service" ];
+ requires = [ "forgejo.service" ];
+ after = [
+ "forgejo.service"
+ "vault-agent-openbao.service"
+ ];
+ unitConfig = {
+ ConditionPathExists = [ oauthSecretIdFile ];
+ };
+ serviceConfig = {
+ Type = "oneshot";
+ User = "git";
+ Group = "git";
+ ExecStart = lib.getExe setApp;
+ };
+ };
+}
diff --git a/nix/systems/aarch64-linux/kas.codes/mailserver/default.nix b/nix/systems/aarch64-linux/kas.codes/mailserver/default.nix
new file mode 100644
index 0000000..88099df
--- /dev/null
+++ b/nix/systems/aarch64-linux/kas.codes/mailserver/default.nix
@@ -0,0 +1,44 @@
+{ inputs, ... }:
+{
+ imports = [
+ ./dkim.nix
+ ./forgejo-user.nix
+ inputs.simple-nixos-mailserver.nixosModules.mailserver
+ ];
+ khscodes.infrastructure.vault-server-approle.policy = {
+ "forgejo/data/mailserver/*" = {
+ capabilities = [ "read" ];
+ };
+ };
+ khscodes.infrastructure.provisioning.pre.modules = [
+ {
+ khscodes.vault = {
+ enable = true;
+ mount.forgejo = {
+ path = "forgejo";
+ type = "kv";
+ options = {
+ version = "2";
+ };
+ description = "Secrets used for forgejo";
+ };
+ };
+ }
+ ];
+ mailserver = {
+ enable = true;
+ fqdn = "kas.codes";
+ domains = [ "kas.codes" ];
+ loginAccounts = {
+ "forgejo@kas.codes" = {
+ hashedPasswordFile = "/var/lib/vault-agent/mailserver/users/forgejo.passwd.hash";
+ sendOnly = true;
+ };
+ };
+ certificateScheme = "acme";
+ dkimKeyDirectory = "/var/lib/vault-agent/mailserver/dkim/";
+ dkimSelector = "dkim_rsa";
+ # Not sure we need to set this at all.
+ dkimKeyBits = 2048;
+ };
+}
diff --git a/nix/systems/aarch64-linux/kas.codes/mailserver/dkim.nix b/nix/systems/aarch64-linux/kas.codes/mailserver/dkim.nix
new file mode 100644
index 0000000..41f182d
--- /dev/null
+++ b/nix/systems/aarch64-linux/kas.codes/mailserver/dkim.nix
@@ -0,0 +1,112 @@
+let
+ publicKeyBegin = ''"-----BEGIN PUBLIC KEY-----\n"'';
+ publicKeyEnd = ''"-----END PUBLIC KEY-----\n"'';
+ dkimPublicKey =
+ tls_key:
+ ''''${ replace(trimprefix(trimsuffix(${tls_key}.public_key_pem, ${publicKeyEnd}), ${publicKeyBegin}), "\n", "") }'';
+in
+{
+ khscodes.services.vault-agent.templates = [
+ {
+ contents = ''
+ {{- with secret "forgejo/data/mailserver/dkim/rsa" -}}
+ {{ .Data.data.dkim_private_key }}
+ {{- end -}}
+ '';
+ destination = "/var/lib/vault-agent/mailserver/dkim/rsa_private.key";
+ perms = "0600";
+ owner = "rspamd";
+ group = "rspamd";
+ restartUnits = [
+ "rspamd.service"
+ "postfix.service"
+ ];
+ }
+ {
+ contents = ''
+ {{- with secret "forgejo/data/mailserver/dkim/ed25519" -}}
+ {{ .Data.data.dkim_private_key }}
+ {{- end -}}
+ '';
+ destination = "/var/lib/vault-agent/mailserver/dkim/ed25519_private.key";
+ perms = "0600";
+ owner = "rspamd";
+ group = "rspamd";
+ restartUnits = [
+ "rspamd.service"
+ "postfix.service"
+ ];
+ }
+ ];
+ khscodes.infrastructure.provisioning.pre.modules = [
+ (
+ { config, ... }:
+ {
+ terraform.required_providers.tls = {
+ source = "hashicorp/tls";
+ version = "4.1.0";
+ };
+ provider.tls = { };
+
+ resource.tls_private_key.dkim_rsa = {
+ algorithm = "RSA";
+ rsa_bits = 2048;
+ };
+ resource.tls_private_key.dkim_ed25519 = {
+ algorithm = "ED25519";
+ };
+
+ resource.cloudflare_record.dkim_rsa = {
+ name = "snm_rsa._domainkey";
+ zone_id = "\${ data.cloudflare_zone.dns_zone.id }";
+ type = "TXT";
+ content = ''"v=DKIM1;k=rsa;p=${dkimPublicKey "tls_private_key.dkim_rsa"}"'';
+ comment = "app=kas.codes";
+ ttl = 600;
+ };
+
+ resource.cloudflare_record.dkim_ed25519 = {
+ name = "snm_ed25519._domainkey";
+ zone_id = "\${ data.cloudflare_zone.dns_zone.id }";
+ type = "TXT";
+ content = ''"v=DKIM1;k=ed25519;p=${dkimPublicKey "tls_private_key.dkim_ed25519"}"'';
+ comment = "app=kas.codes";
+ ttl = 600;
+ };
+
+ resource.cloudflare_record.spf = {
+ name = "@";
+ zone_id = "\${ data.cloudflare_zone.dns_zone.id }";
+ type = "TXT";
+ content = ''"v=spf1 ip4:${config.khscodes.hcloud.output.server.compute.ipv4_address} ip6:${config.khscodes.hcloud.output.server.compute.ipv6_address} -all"'';
+ comment = "app=kas.codes";
+ ttl = 600;
+ };
+ resource.cloudflare_record.dmarc = {
+ name = "_dmarc";
+ zone_id = "\${ data.cloudflare_zone.dns_zone.id }";
+ type = "TXT";
+ content = ''"v=DMARC1; p=reject; adkim=s; aspf=s;"'';
+ comment = "app=kas.codes";
+ ttl = 600;
+ };
+
+ resource.vault_kv_secret_v2.dkim_rsa_secret = {
+ mount = config.khscodes.vault.output.mount.forgejo.path;
+ name = "mailserver/dkim/rsa";
+ data_json = ''
+ { "dkim_private_key": ''${ jsonencode(resource.tls_private_key.dkim_rsa.private_key_pem) } }
+ '';
+ };
+
+ resource.vault_kv_secret_v2.dkim_ed25519_secret = {
+ mount = config.khscodes.vault.output.mount.forgejo.path;
+ name = "mailserver/dkim/ed25519";
+ data_json = ''
+ { "dkim_private_key": ''${ jsonencode(resource.tls_private_key.dkim_ed25519.private_key_pem) } }
+ '';
+ };
+ }
+ )
+ ];
+}
diff --git a/nix/systems/aarch64-linux/kas.codes/mailserver/forgejo-user.nix b/nix/systems/aarch64-linux/kas.codes/mailserver/forgejo-user.nix
new file mode 100644
index 0000000..3cd8c3d
--- /dev/null
+++ b/nix/systems/aarch64-linux/kas.codes/mailserver/forgejo-user.nix
@@ -0,0 +1,53 @@
+let
+ bcrypt = expr: "\${ jsonencode(bcrypt(${expr})) }";
+in
+{
+ khscodes.services.vault-agent.templates = [
+ {
+ contents = ''
+ {{- with secret "forgejo/data/mailserver/users/forgejo" -}}
+ {{ .Data.data.hashed_password }}
+ {{- end -}}
+ '';
+ destination = "/var/lib/vault-agent/mailserver/users/forgejo.passwd.hash";
+ perms = "0600";
+ owner = "rspamd";
+ group = "rspamd";
+ restartUnits = [
+ "rspamd.service"
+ "postfix.service"
+ ];
+ }
+ ];
+ khscodes.infrastructure.provisioning.pre.modules = [
+ (
+ { config, ... }:
+ {
+ terraform.required_providers.random = {
+ source = "hashicorp/random";
+ version = "3.7.2";
+ };
+ provider.random = { };
+
+ resource.random_password.forgejo_mail_passwd = {
+ length = 48;
+ numeric = true;
+ lower = true;
+ upper = true;
+ special = false;
+ };
+
+ resource.vault_kv_secret_v2.forgejo_email_user_password = {
+ mount = config.khscodes.vault.output.mount.forgejo.path;
+ name = "mailserver/users/forgejo";
+ data_json = ''
+ {
+ "hashed_password": ${bcrypt "resource.random_password.forgejo_mail_passwd.result"},
+ "password": ''${ jsonencode(resource.random_password.forgejo_mail_passwd.result) }
+ }
+ '';
+ };
+ }
+ )
+ ];
+}
diff --git a/nix/systems/aarch64-linux/security.kaareskovgaard.net/kanidm.nix b/nix/systems/aarch64-linux/security.kaareskovgaard.net/kanidm.nix
index 8a09af7..631db97 100644
--- a/nix/systems/aarch64-linux/security.kaareskovgaard.net/kanidm.nix
+++ b/nix/systems/aarch64-linux/security.kaareskovgaard.net/kanidm.nix
@@ -10,6 +10,7 @@ let
openbaoAppBasicSecretFile = "/var/lib/vault-agent/kanidm/openbao_basic_secret";
openbaoCliAppBasicSecretFile = "/var/lib/vault-agent/kanidm/openbao_cli_basic_secret";
monitoringAppBasicSecretFile = "/var/lib/vault-agent/kanidm/monitoring_basic_secret";
+ forgejoAppBasicSecretFile = "/var/lib/vault-agent/kanidm/monitoring_basic_secret";
openbaoDomain = config.khscodes.infrastructure.openbao.domain;
openbaoAllowedRedirectUrls = [
"https://${openbaoDomain}/ui/vault/auth/kanidm/oidc/callback"
@@ -70,6 +71,14 @@ in
present = true;
members = [ "khs" ];
};
+ groups.forgejo_user = {
+ present = true;
+ members = [ "khs" ];
+ };
+ groups.forgejo_comitter = {
+ present = true;
+ members = [ "khs" ];
+ };
# We cannot add oauth2 apps before the secrets for them are generated.
systems.oauth2 = lib.mkIf (!bootstrapping) {
openbao = {
@@ -137,6 +146,30 @@ in
];
};
};
+ forgejo = {
+ present = true;
+ public = false;
+ preferShortUsername = true;
+ basicSecretFile = lib.mkIf (!bootstrapping) forgejoAppBasicSecretFile;
+ originUrl = [ "https://kas.codes/user/oauth2/Kanidm/callback" ];
+ originLanding = "http://kas.codes";
+ displayName = "KAS: Codes";
+ scopeMaps = {
+ "forgejo_user" = [
+ "profile"
+ "email"
+ "openid"
+ ];
+ };
+ claimMaps.groups = {
+ joinType = "array";
+ valuesByGroup = {
+ "forgejo_comitter" = [
+ "comitter"
+ ];
+ };
+ };
+ };
};
};
};
@@ -218,6 +251,20 @@ in
{ "basic_secret": "''${ resource.random_password.monitoring.result }" }
'';
};
+ resource.random_password.forgejo = {
+ length = 48;
+ numeric = true;
+ lower = true;
+ upper = true;
+ special = false;
+ };
+ resource.vault_kv_secret_v2.forgejo_secret = {
+ mount = config.khscodes.vault.output.mount.kanidm.path;
+ name = "apps/forgejo";
+ data_json = ''
+ { "basic_secret": "''${ resource.random_password.forgejo.result }" }
+ '';
+ };
}
)
# Sets up OIDC authentication within OpenBAO.
@@ -381,6 +428,18 @@ in
group = "kanidm";
reloadOrRestartUnits = [ "kanidm.service" ];
}
+ {
+ contents = ''
+ {{- with secret "kanidm/data/apps/forgejo" -}}
+ {{ .Data.data.basic_secret }}
+ {{- end -}}
+ '';
+ destination = forgejoAppBasicSecretFile;
+ perms = "0600";
+ owner = "kanidm";
+ group = "kanidm";
+ reloadOrRestartUnits = [ "kanidm.service" ];
+ }
];
security.acme.certs.${domain}.reloadServices = [ "kanidm.service" ];