summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--flake.nix3
-rw-r--r--hosts/apocalypse/default.nix6
-rw-r--r--hosts/omega-relay/default.nix12
-rw-r--r--packages/bulwark/0001-use-local-google-fonts.patch29
-rw-r--r--packages/bulwark/package.nix74
-rw-r--r--secrets/omega-relay/bulwark.yaml46
-rw-r--r--services/bulwark.nix152
-rw-r--r--services/email.nix4
8 files changed, 324 insertions, 2 deletions
diff --git a/flake.nix b/flake.nix
index f99e9dd..d581678 100644
--- a/flake.nix
+++ b/flake.nix
@@ -71,6 +71,9 @@
in
{
packages."x86_64-linux".full-iso = self.nixosConfigurations.full-iso.config.system.build.isoImage;
+ packages."x86_64-linux".bulwark =
+ nixpkgs.legacyPackages."x86_64-linux".callPackage ./packages/bulwark/package.nix
+ { };
# for `nix fmt`
formatter = treefmtExtract "formatter";
# for `nix flake check`
diff --git a/hosts/apocalypse/default.nix b/hosts/apocalypse/default.nix
index 179e2d1..f2aac2a 100644
--- a/hosts/apocalypse/default.nix
+++ b/hosts/apocalypse/default.nix
@@ -1,4 +1,4 @@
-{ lib, ... }:
+{ lib, pkgs, ... }:
{
imports = [
./hardware-configuration.nix
@@ -13,6 +13,10 @@
"127.0.0.1" = [ "omega-relay.local" ];
};
+ environment.systemPackages = [
+ (pkgs.callPackage ../../packages/bulwark/package.nix { })
+ ];
+
services.logind.settings.Login = {
HandleLidSwitch = "suspend";
HandleLidSwitchExternalPower = "lock";
diff --git a/hosts/omega-relay/default.nix b/hosts/omega-relay/default.nix
index 9932da2..dbb9874 100644
--- a/hosts/omega-relay/default.nix
+++ b/hosts/omega-relay/default.nix
@@ -17,6 +17,7 @@
../../services/freshrss.nix
../../services/wakapi.nix
../../services/continuwuity.nix
+ ../../services/bulwark.nix
];
addons = {
@@ -54,8 +55,19 @@
corsDomains = [
"qtab.wilkuu.nl"
"qtab-dev.wilkuu.nl"
+ "bulwark.wilkuu.nl"
+ "bulwark.wilkuu.xyz"
];
};
+
+ bulwark = {
+ enable = true;
+ domains = [
+ "bulwark.wilkuu.nl"
+ "bulwark.wilkuu.xyz"
+ ];
+ doACME = !isVM;
+ };
continuwuity = {
enable = true;
allowRegistration = false;
diff --git a/packages/bulwark/0001-use-local-google-fonts.patch b/packages/bulwark/0001-use-local-google-fonts.patch
new file mode 100644
index 0000000..0bae8c0
--- /dev/null
+++ b/packages/bulwark/0001-use-local-google-fonts.patch
@@ -0,0 +1,29 @@
+diff --git a/app/layout.tsx b/app/layout.tsx
+index 573ed32..e5b1edc 100644
+--- a/app/layout.tsx
++++ b/app/layout.tsx
+@@ -1,5 +1,5 @@
+ import type { Metadata } from "next";
+-import { Geist, Geist_Mono } from "next/font/google";
++import localFont from "next/font/local";
+ import { headers } from "next/headers";
+ import { getLocale } from "next-intl/server";
+ import { PWAInstallPrompt } from "@/components/pwa-install-prompt";
+@@ -7,15 +7,8 @@ import { ServiceWorkerRegistration } from "@/components/service-worker-registrat
+ import { configManager } from "@/lib/admin/config-manager";
+ import "./globals.css";
+
+-const geistSans = Geist({
+- variable: "--font-geist-sans",
+- subsets: ["latin"],
+-});
+-
+-const geistMono = Geist_Mono({
+- variable: "--font-geist-mono",
+- subsets: ["latin"],
+-});
++const geistSans = localFont({src: './Geist-Regular.otf', variable: "--font-geist-sans"})
++const geistMono = localFont({src: './GeistMono-Regular.otf', variable: "--font-geist-mono"})
+
+ export async function generateMetadata(): Promise<Metadata> {
+ await configManager.ensureLoaded();
diff --git a/packages/bulwark/package.nix b/packages/bulwark/package.nix
new file mode 100644
index 0000000..14b9844
--- /dev/null
+++ b/packages/bulwark/package.nix
@@ -0,0 +1,74 @@
+{
+ lib,
+ buildNpmPackage,
+ fetchFromGitHub,
+ git,
+ geist-font,
+
+ defaultHostname ? "127.0.0.1",
+ defaultPort ? 3000,
+}:
+buildNpmPackage (finalAttrs: {
+ pname = "bulwark";
+ version = "1.6.5";
+ nativeBuildInputs = [ git ];
+ src = fetchFromGitHub {
+ owner = "bulwarkmail";
+ repo = "webmail";
+ tag = "${finalAttrs.version}";
+ hash = "sha256-U2bXylfyRLh7d0PA8UNO5Jdi0jg2U//rJlohPJwaPUQ=";
+ };
+
+ npmDepsHash = "sha256-DLRXDM/PUqcjwX66hwvzALIKS016E7s3u/VDuYStYXA=";
+ npmPackFlags = [ "--ignore-scripts" ];
+ NODE_OPTIONS = [ "--openssl-legacy-provider" ];
+
+ patches = [
+ ./0001-use-local-google-fonts.patch
+ ];
+
+ preBuild = ''
+ # TODO: BASE_PATH
+ cp ${geist-font}/share/fonts/opentype/Geist{,Mono}-Regular.otf ./app/
+ '';
+
+ postBuild = ''
+ sed -i '1s|^|#!/usr/bin/env node \n|' .next/standalone/server.js
+ patchShebangs .next/standalone/server.js
+ '';
+
+ installPhase = ''
+ runHook preInstall
+
+ mkdir -p $out/{share,bin}
+
+ cp -r .next/standalone $out/share/homepage/
+ cp -r .next/standalone $out/share/homepage/data/
+ # cp -r .env $out/share/homepage/
+ cp -r public $out/share/homepage/public
+
+ mkdir -p $out/share/homepage/.next
+ cp -r .next/static $out/share/homepage/.next/static
+
+ # https://github.com/vercel/next.js/discussions/58864
+ ln -s /var/cache/bulwark $out/share/homepage/.next/cache
+ # also provide a environment variable to override the cache directory
+ substituteInPlace $out/share/homepage/node_modules/next/dist/server/image-optimizer.js \
+ --replace '_path.join)(distDir,' '_path.join)(process.env["NEXT_CACHE_DIR"] || distDir,'
+
+ chmod +x $out/share/homepage/server.js
+
+ # we set a default port to support "nix run ..."
+ makeWrapper $out/share/homepage/server.js $out/bin/bulwark \
+ --set-default PORT ${toString defaultPort} \
+ --set-default HOSTNAME ${defaultHostname} \
+
+ runHook postInstall
+ '';
+
+ meta = {
+ description = "Bulwark JMAP Webmail :3";
+ homepage = "https://bulwarkmail.org";
+ license = lib.licenses.agpl3Only;
+ };
+})
diff --git a/secrets/omega-relay/bulwark.yaml b/secrets/omega-relay/bulwark.yaml
new file mode 100644
index 0000000..9273b7c
--- /dev/null
+++ b/secrets/omega-relay/bulwark.yaml
@@ -0,0 +1,46 @@
+bulwark:
+ admin_password: ENC[AES256_GCM,data:Za8pi+toC9QQ99O7YqHLK6+CyDFsyY6LxcJcXf3FETl/,iv:Vflgx1YGRmBw8nSu4g/ScXreUn/2PPab+d583imYdF8=,tag:yt+P3osRhTS5N+ax1Vf9bA==,type:str]
+ session_secret: ENC[AES256_GCM,data:Yvk/p08xU2IvneIzGx73mGrPy/FwFLRYgPXeEYlTWH4PHwrt1qGNVEZK2KU=,iv:YiSvrIc/dtGCLFG9FfwW1GDhdbpGQqNCtSbH/nASMLk=,tag:boo8+cZOgwRetZkccQlIZw==,type:str]
+ oauth_secret: ENC[AES256_GCM,data:ksA+k6beYB5jhtD0k9CTKwhOmfqWfxAqKPAxm/3r9uWOKHmv87xh9ufEM13VEtyxBKQLoRBCdSpU5XHOVtRxOTzLG2fAB4tzuhv4IehvLaXrFWmsDGwmGg==,iv:gaYvaYK7t9M6JRWtawQoyyktwnTyGkwN2Qs9fZbTA2s=,tag:3Dnb1lXCTjsirFL2u3DZ8A==,type:str]
+sops:
+ age:
+ - enc: |
+ -----BEGIN AGE ENCRYPTED FILE-----
+ YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNSmtBNVdvY2FMQzlmTkZq
+ d3M2QkIyNHordUFPby9jeE9qai82cCtoWG1VCm5CUjlvUExabVlUR21seStLdW9W
+ bnlqbjN6Rm1tTTYzS3lxVjJDRlRhcjQKLS0tIFV3RXpDdmpSdDhWQ1ZQUTBxM1dM
+ alI5OG5xd2lLOG1URE9ZVzZwc1FHSncKbbjc4e4ytLqoElCeHamQZ/DSMujK0/J2
+ at3TsZPKmF7ZSaecYeMraO7HFPQZNRqCSarcqfzomynoGc+NvQ9Wjw==
+ -----END AGE ENCRYPTED FILE-----
+ recipient: age1hsxrd9shau86k7zz9wfdeplldyg5c2al6umnr7rn6tf7ewus64uswufr0a
+ - enc: |
+ -----BEGIN AGE ENCRYPTED FILE-----
+ YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQWHZyRTQySW9nd0FDL0JD
+ MVFLeUtaeG5xWENkZ3liSnA4WGlnVGtBM2t3CklXWTByeUtwVlptYjJUWkUrOVha
+ SHppN1FPZVQzMXFieUZERUhuMUg5c28KLS0tIExVMWl5UDM1YThHbk5mSmNRQi8r
+ WjRvVUhUMGh6N3BNSEFRTDNJeVVaVncK8Z4/RKSsHaZrI5llk1PtLulQ3qmsJRy/
+ riqO0bAfbI/eyu56yg2VHBnL5loh6z5b+s4M6vmU9hcwcV4arj/xoA==
+ -----END AGE ENCRYPTED FILE-----
+ recipient: age1n20lpr5x2aep3l3ta62x2c9cnuz3m495f2dmguqs08ezlqgu4vwsqfskyg
+ - enc: |
+ -----BEGIN AGE ENCRYPTED FILE-----
+ YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKZm5BV1EzUDZUVFF3SFdK
+ bzBmZ2tTV2gxTDhSdXRtZHpBcnowdHdhQWpZCjJnZWZwUVNIcmhINGJrNW5OTXhm
+ bXpIZDN5N3NZNmFKN2RiSWZQdGFjR0UKLS0tIG9JbXVndG1kL050bUk0VHNCanYz
+ bnRwWURXai85a3Z6L1ltT1QrN0lmeDAKNguXlU0pCb3lhB9nTBw/ud3XS9RBmyvz
+ oBK7lKT6NobgxT+i5BFzc6swsz2yKkNNsKpOCcclb5VczqteP9nFSw==
+ -----END AGE ENCRYPTED FILE-----
+ recipient: age13x9grwfpqjgvjfcnwg5snuntp5emze85hqc8qwuz9nfphfe2pg4skl29r4
+ - enc: |
+ -----BEGIN AGE ENCRYPTED FILE-----
+ YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuekpsc1lxQ3V1Zm1lT29h
+ NFFBSmRLQzBRQ1JrQWdHY3NEUEFqVmV6YVJrCnc0VjNzSm4vRlVuY09TdTV5U1VV
+ MGNvNWs3eU52MTgyd0l0UUErSzFJcWsKLS0tIG4rdENyVUNobVQ5U1c2cW53UGZz
+ NE1zTDIxcW1XUWVGdnZWbEJ1L0wxNGsKp+ImRObwkZoLmQ5/1r8mPPICHVFXSIIs
+ yGsDGZIqn7ivYrMfXJ7mQTMP+IvM2nmoGkl02PXWJbHPqzsrRuimkQ==
+ -----END AGE ENCRYPTED FILE-----
+ recipient: age18u22lycxtcasvlm298qwzwuc97z56kkhw8ssy9yx4gzlkskuwpyqdz80g4
+ lastmodified: "2026-05-15T20:46:06Z"
+ mac: ENC[AES256_GCM,data:R6RqOTgmsoPIyGTmJkYo3HdKPCjlUfgs+djVG97kvQKhh+apO97NH2JWlXpwOP4xzmL+5k4fuSalbNGoBmfY112d0syIpXCzy3gys6dhZTT+/dPfFC7BBvg7FICWSA6ea89mkO+5qhtuMiNFoFbG9f38i6O0a//xCQ2b0IdQ8B0=,iv:qv3VFvHf1P9yu+Hiq79NFJOLIHC+tpK+pLgFPXWcf1A=,tag:GovlvRgZ7jtwR7pIz06+xQ==,type:str]
+ unencrypted_suffix: _unencrypted
+ version: 3.13.0
diff --git a/services/bulwark.nix b/services/bulwark.nix
new file mode 100644
index 0000000..b20125e
--- /dev/null
+++ b/services/bulwark.nix
@@ -0,0 +1,152 @@
+{
+ pkgs,
+ lib,
+ config,
+ ...
+}:
+let
+ cfg = config.wilkuu.services.bulwark;
+ stalwart_cfg = config.wilkuu.services.stalwart;
+ hostname = config.networking.hostName;
+
+ # Importing stuff from stalwart and defaults etc.
+ _stalwart_jmap_domains = lib.optional stalwart_cfg.enable (
+ [ stalwart_cfg.domain ] ++ stalwart_cfg.additionalDomains
+ );
+
+ # Secrets handling
+ sopsPath = ../secrets/${hostname}/bulwark.yaml;
+ secrets = [
+ "admin_password"
+ "session_secret"
+ "oauth_secret"
+ ];
+ toSops = (sname: "bulwark/${sname}");
+ toCredfilePath = (name: config.sops.secrets.${toSops name}.path);
+ toSopsPlaceholder = (name: config.sops.placeholder.${toSops name});
+
+ bulwark = pkgs.callPackage ../packages/bulwark/package.nix { };
+in
+{
+ options.wilkuu.services.bulwark = with lib; {
+ enable = mkEnableOption "Enable bulwark client";
+ doACME = mkEnableOption "Enable ACME for bulwark here";
+ package = mkOption {
+ type = types.package;
+ default = bulwark;
+ description = "The bulwark package to be used.";
+ };
+ domains = mkOption {
+ type = (types.listOf types.str);
+ default = [ ];
+ example = [ "bulwark.wilkuu.xyz" ];
+ description = "Domains for HTTP connections";
+ };
+ dataDir = mkOption {
+ type = types.path;
+ description = "Storage localtion for Bulwark user data";
+ default = "/var/lib/bulwark";
+ example = "/srv/data/bulwark";
+ };
+ jmap_servers = mkOption {
+ type = types.listOf types.str;
+ default = _stalwart_jmap_domains; # FIXME: What if someone does not want to use all of their stalwart domains here.
+ example = [ "jmap.example.com" ];
+ description = "A list of JMAP domains avalable for bulwark";
+ };
+ extraEnv = mkOption {
+ type = types.attrs; # TODO Better typing
+ default = { };
+ example = {
+ FAVICON_URL = "static.example.com/favicon.ico";
+ };
+ description = "Additional configuration, see https://bulwarkmail.org/docs/getting-started/configuration/environment-reference#server-listen-address for more info.";
+ };
+ stalwartInterop = mkOption {
+ type = types.bool;
+ default = stalwart_cfg.enable;
+ example = true;
+ description = "Whenever to allow stalwart-specific features.";
+ };
+
+ };
+
+ config.users = lib.mkIf cfg.enable {
+ users.bulwark = {
+ isSystemUser = true;
+ group = "uptimekuma";
+ };
+ groups.bulwark = { };
+ };
+
+ config.wilkuu.services.stalwart.corsDomains = lib.optionals (
+ cfg.enable && stalwart_cfg.enable
+ ) cfg.domains;
+ config.sops.secrets = (
+ lib.genAttrs (map toSops secrets) (_name: {
+ sopsFile = sopsPath;
+ mode = "0440";
+ owner = "bulwark";
+ })
+ );
+ config.sops.templates."bulwark-env" = lib.mkIf cfg.enable {
+ owner = "bulwark";
+ content =
+ pkgs.lib.generators.toKeyValue
+ {
+ mkKeyValue = key: value: "${key}=${lib.escapeShellArg (toString value)}";
+ }
+ {
+ STALWART_FEATURES = "${cfg.stalwartInterop}";
+ JMAP_SERVER_URL = lib.concatStringSep cfg.jmap_domains;
+ ADMIN_CONFIG_DIR = "${cfg.dataDir}/settings";
+ ADMIN_STATE_DIR = "${cfg.dataDir}/admin-state";
+
+ SETTINGS_SYNC_ENABLED = true;
+ SETTINGS_DATA_DIR = "${cfg.dataDir}/settings";
+
+ BULWARK_TELEMETRY = "off";
+ TELEMETRY_DATA_DIR = "${cfg.dataDir}/telemetry";
+ VERSION_CHECK_DATA_DIR = "${cfg.dataDir}/version-check";
+
+ SESSION_SECRET_FILE = toCredfilePath "session_secret";
+ ADMIN_PASSWORD = toSopsPlaceholder "admin_password";
+ EXTENSION_DIRECTORY_URL = "https://extensions.bulwarkmail.org";
+
+ # Put into a option module
+ OAUTH_ENABLED = true;
+ OAUTH_CLIENT_ID = "${hostname}-bulwark-webmail";
+ OAUTH_ONLY = false;
+ # OAUTH_CLIENT_SECRET_FILE = ...
+ # TODO: OAUTH_[EXTRA_]SCOPES
+
+ PORT = 3081;
+ }
+ // cfg.extraEnv;
+ };
+ config.services.nginx.virtualHosts = lib.mkIf cfg.enable (
+ lib.genAttrs cfg.domains (_vhost: {
+ addSSL = cfg.doACME;
+ enableACME = cfg.doACME;
+ locations."/" = {
+ proxyPass = "http://localhost:3081";
+ recommendedProxySettings = true;
+ };
+ })
+ );
+
+ config.systemd.services.bulwark = lib.mkIf cfg.enable ({
+ enable = cfg.enable;
+ serviceConfig = {
+ User = "bulwark";
+ AllowedPorts = 3081;
+ SocketBindAllow = "tcp:8080";
+ ExecStart = "${cfg.package}";
+ WorkingDirectory = "${cfg.dataDir}";
+ StateDirectory = "bulwark";
+ Restart = "always";
+ RestartSec = 5;
+ EnvironmentFile = config.sops.templates."bulwark-env".path;
+ };
+ });
+}
diff --git a/services/email.nix b/services/email.nix
index c05840d..7afc2b5 100644
--- a/services/email.nix
+++ b/services/email.nix
@@ -136,7 +136,7 @@ in
lib.genAttrs (map toSops secrets) (_name: {
sopsFile = sopsPath;
mode = "0440";
- owner = "stalwart-mail";
+ owner = "stalwart";
})
);
@@ -147,6 +147,8 @@ in
"mta-sts.txt"
"mail-v1.xml"
"autoconfig/mail"
+ "openid-configuration"
+ "/.well-known/oauth-authorization-server"
])
// (lib.genAttrs [ "/.well-known/caldav/" "/.well-known/webdav/" "/.well-known/jmap" ] (uri: {
extraConfig = ''