diff options
| -rw-r--r-- | flake.nix | 3 | ||||
| -rw-r--r-- | hosts/apocalypse/default.nix | 6 | ||||
| -rw-r--r-- | hosts/omega-relay/default.nix | 12 | ||||
| -rw-r--r-- | packages/bulwark/0001-use-local-google-fonts.patch | 29 | ||||
| -rw-r--r-- | packages/bulwark/package.nix | 74 | ||||
| -rw-r--r-- | secrets/omega-relay/bulwark.yaml | 46 | ||||
| -rw-r--r-- | services/bulwark.nix | 152 | ||||
| -rw-r--r-- | services/email.nix | 4 |
8 files changed, 324 insertions, 2 deletions
@@ -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 = '' |
