summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--hosts/sif/ruleset.nft4
-rw-r--r--hosts/surtr/default.nix3
-rw-r--r--hosts/surtr/email/ca/index.txt3
-rw-r--r--hosts/surtr/email/ca/serial2
-rw-r--r--hosts/surtr/email/default.nix90
-rw-r--r--hosts/surtr/email/spm-keys.json26
-rw-r--r--hosts/surtr/email/spm/default.nix24
-rw-r--r--hosts/surtr/email/spm/lib/Data/CaseInsensitive/Instances.hs19
-rw-r--r--hosts/surtr/email/spm/lib/Data/UUID/Instances.hs18
-rw-r--r--hosts/surtr/email/spm/lib/Spm.hs5
-rw-r--r--hosts/surtr/email/spm/lib/Spm/Api.hs40
-rw-r--r--hosts/surtr/email/spm/package.yaml92
-rw-r--r--hosts/surtr/email/spm/provision/Spm/Provision.hs46
-rw-r--r--hosts/surtr/email/spm/server/Crypto/JOSE/JWK/Instances.hs9
-rw-r--r--hosts/surtr/email/spm/server/Data/CaseInsensitive/Instances.hs30
-rw-r--r--hosts/surtr/email/spm/server/Data/UUID/Instances.hs31
-rw-r--r--hosts/surtr/email/spm/server/Spm/Server.hs194
-rw-r--r--hosts/surtr/email/spm/server/Spm/Server/Database.hs72
-rw-r--r--hosts/surtr/email/spm/spm.nix28
-rw-r--r--hosts/surtr/postgresql.nix62
-rw-r--r--hosts/surtr/ruleset.nft4
-rw-r--r--hosts/vidhar/network/ruleset.nft4
-rw-r--r--shell.nix2
23 files changed, 771 insertions, 37 deletions
diff --git a/hosts/sif/ruleset.nft b/hosts/sif/ruleset.nft
index c453cc7b..363ffbdc 100644
--- a/hosts/sif/ruleset.nft
+++ b/hosts/sif/ruleset.nft
@@ -118,7 +118,7 @@ table inet filter {
118 meta l4proto $icmp_protos counter name icmp-rx accept 118 meta l4proto $icmp_protos counter name icmp-rx accept
119 119
120 tcp dport 22 counter name ssh-rx accept 120 tcp dport 22 counter name ssh-rx accept
121 udp dport 60001-61000 counter name mosh-rx accept 121 udp dport 60000-61000 counter name mosh-rx accept
122 122
123 tcp dport 8000 counter name quickserve-rx accept 123 tcp dport 8000 counter name quickserve-rx accept
124 124
@@ -146,7 +146,7 @@ table inet filter {
146 146
147 147
148 tcp sport 22 counter name ssh-tx 148 tcp sport 22 counter name ssh-tx
149 udp sport 60001-61000 counter name mosh-tx 149 udp sport 60000-61000 counter name mosh-tx
150 150
151 udp sport 51820-51822 counter name wg-tx 151 udp sport 51820-51822 counter name wg-tx
152 iifname "yggdrasil-wg-*" meta l4proto gre counter name yggdrasil-gre-tx 152 iifname "yggdrasil-wg-*" meta l4proto gre counter name yggdrasil-gre-tx
diff --git a/hosts/surtr/default.nix b/hosts/surtr/default.nix
index ff93e0e5..c9ecc945 100644
--- a/hosts/surtr/default.nix
+++ b/hosts/surtr/default.nix
@@ -71,12 +71,13 @@
71 systemd.network.networks."40-ens3".networkConfig = { 71 systemd.network.networks."40-ens3".networkConfig = {
72 Domains = lib.mkForce "~."; 72 Domains = lib.mkForce "~.";
73 DNS = [ "127.0.0.1:5353" "[::1]:5353" ]; 73 DNS = [ "127.0.0.1:5353" "[::1]:5353" ];
74 DNSSEC = true; 74 # DNSSEC = true;
75 # DNS = [ "46.38.225.230" "46.38.252.230" "2a03:4000:0:1::e1e6" "2a03:4000:8000::fce6" ]; 75 # DNS = [ "46.38.225.230" "46.38.252.230" "2a03:4000:0:1::e1e6" "2a03:4000:8000::fce6" ];
76 }; 76 };
77 77
78 services.resolved = { 78 services.resolved = {
79 llmnr = "false"; 79 llmnr = "false";
80 dnssec = "false"; # unbound does dnssec validation for us
80 }; 81 };
81 82
82 services.ndppd = { 83 services.ndppd = {
diff --git a/hosts/surtr/email/ca/index.txt b/hosts/surtr/email/ca/index.txt
index 711193b2..40c9605a 100644
--- a/hosts/surtr/email/ca/index.txt
+++ b/hosts/surtr/email/ca/index.txt
@@ -1 +1,2 @@
1V 320502142416Z 02 unknown /CN=gkleen 1V 320513204402Z 03 unknown /CN=gkleen
2V 320515063648Z 04 unknown /CN=nmuehlbauer
diff --git a/hosts/surtr/email/ca/serial b/hosts/surtr/email/ca/serial
index 75016ea3..eeee65ec 100644
--- a/hosts/surtr/email/ca/serial
+++ b/hosts/surtr/email/ca/serial
@@ -1 +1 @@
03 05
diff --git a/hosts/surtr/email/default.nix b/hosts/surtr/email/default.nix
index 404e9e4b..947aa328 100644
--- a/hosts/surtr/email/default.nix
+++ b/hosts/surtr/email/default.nix
@@ -21,6 +21,8 @@ let
21 }; 21 };
22 22
23 spmDomains = ["bouncy.email"]; 23 spmDomains = ["bouncy.email"];
24
25 spm = pkgs.callPackage ./spm {};
24in { 26in {
25 config = { 27 config = {
26 nixpkgs.overlays = [ 28 nixpkgs.overlays = [
@@ -115,6 +117,8 @@ in {
115 "reject_unknown_recipient_domain" 117 "reject_unknown_recipient_domain"
116 "reject_unverified_recipient" 118 "reject_unverified_recipient"
117 ]; 119 ];
120 unverified_recipient_reject_code = "550";
121 unverified_recipient_reject_reason = "Recipient address rejected: undeliverable address";
118 122
119 smtpd_relay_restrictions = [ 123 smtpd_relay_restrictions = [
120 "permit_mynetworks" 124 "permit_mynetworks"
@@ -177,6 +181,9 @@ in {
177 "-o" "smtpd_tls_req_ccert=yes" 181 "-o" "smtpd_tls_req_ccert=yes"
178 "-o" "smtpd_client_restrictions=permit_tls_all_clientcerts,reject" 182 "-o" "smtpd_client_restrictions=permit_tls_all_clientcerts,reject"
179 "-o" "smtpd_relay_restrictions=permit_tls_all_clientcerts,reject" 183 "-o" "smtpd_relay_restrictions=permit_tls_all_clientcerts,reject"
184 "-o" "smtpd_sender_restrictions=reject_unknown_sender_domain,reject_unverified_sender"
185 "-o" "unverified_sender_reject_code=550"
186 "-o" "unverified_sender_reject_reason={Sender address rejected: undeliverable address}"
180 "-o" "smtpd_recipient_restrictions=reject_unauth_pipelining,reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_tls_all_clientcerts,reject" 187 "-o" "smtpd_recipient_restrictions=reject_unauth_pipelining,reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_tls_all_clientcerts,reject"
181 "-o" "milter_macro_daemon_name=surtr.yggdrasil.li" 188 "-o" "milter_macro_daemon_name=surtr.yggdrasil.li"
182 "-o" ''smtpd_milters=${config.services.opendkim.socket}'' 189 "-o" ''smtpd_milters=${config.services.opendkim.socket}''
@@ -600,24 +607,79 @@ in {
600 }; 607 };
601 }; 608 };
602 609
603 services.nginx.virtualHosts = listToAttrs (map (domain: nameValuePair "spm.${domain}" { 610 services.nginx = {
604 forceSSL = true; 611 upstreams.spm = {
605 sslCertificate = "/run/credentials/nginx.service/spm.${domain}.pem"; 612 servers = {
606 sslCertificateKey = "/run/credentials/nginx.service/spm.${domain}.key.pem"; 613 "unix:/run/spm/server.sock" = {};
607 extraConfig = '' 614 };
608 ssl_stapling off; 615 };
609 ssl_verify_client on; 616
610 ssl_client_certificate ${toString ./ca/ca.crt}; 617 virtualHosts = listToAttrs (map (domain: nameValuePair "spm.${domain}" {
611 ''; 618 forceSSL = true;
612 locations."/".extraConfig = '' 619 sslCertificate = "/run/credentials/nginx.service/spm.${domain}.pem";
613 default_type text/plain; 620 sslCertificateKey = "/run/credentials/nginx.service/spm.${domain}.key.pem";
614 return 200 "$ssl_client_verify $ssl_client_s_dn ${domain}"; 621 extraConfig = ''
615 ''; 622 ssl_stapling off;
616 }) spmDomains); 623 ssl_verify_client on;
624 ssl_client_certificate ${toString ./ca/ca.crt};
625 '';
626 locations."/" = {
627 proxyPass = "http://spm";
628
629 extraConfig = ''
630 proxy_set_header SSL-CLIENT-VERIFY $ssl_client_verify;
631 proxy_set_header SSL-CLIENT-S-DN $ssl_client_s_dn;
632 proxy_set_header SPM-DOMAIN "${domain}";
633 '';
634 };
635 }) spmDomains);
636 };
617 637
618 systemd.services.nginx.serviceConfig.LoadCredential = concatMap (domain: [ 638 systemd.services.nginx.serviceConfig.LoadCredential = concatMap (domain: [
619 "spm.${domain}.key.pem:${config.security.acme.certs."spm.${domain}".directory}/key.pem" 639 "spm.${domain}.key.pem:${config.security.acme.certs."spm.${domain}".directory}/key.pem"
620 "spm.${domain}.pem:${config.security.acme.certs."spm.${domain}".directory}/fullchain.pem" 640 "spm.${domain}.pem:${config.security.acme.certs."spm.${domain}".directory}/fullchain.pem"
621 ]) spmDomains; 641 ]) spmDomains;
642
643 systemd.services.spm = {
644 serviceConfig = {
645 Type = "notify";
646 ExecStart = "${spm}/bin/spm-server";
647 User = "spm";
648 Group = "spm";
649
650 Environment = [
651 "SPM_INSTANCE=ed1c0e1d-7be4-4dd5-b51a-291bad3ac9c9"
652 "PGCONNSTR=dbname=email"
653 ];
654
655 LoadCredential = [
656 "spm-keys.json:${config.sops.secrets."spm-keys.json".path}"
657 ];
658 };
659 };
660 systemd.sockets.spm = {
661 wantedBy = [ "nginx.service" ];
662
663 socketConfig = {
664 ListenStream = "/run/spm/server.sock";
665 SocketUser = "spm";
666 SocketGroup = "spm";
667 SocketMode = 0660;
668 };
669 };
670
671 users.users.spm = {
672 isSystemUser = true;
673 group = "spm";
674 };
675
676 users.groups.spm = {
677 members = [ config.services.nginx.user ];
678 };
679
680 sops.secrets."spm-keys.json" = {
681 format = "binary";
682 sopsFile = ./spm-keys.json;
683 };
622 }; 684 };
623} 685}
diff --git a/hosts/surtr/email/spm-keys.json b/hosts/surtr/email/spm-keys.json
new file mode 100644
index 00000000..cefe27b1
--- /dev/null
+++ b/hosts/surtr/email/spm-keys.json
@@ -0,0 +1,26 @@
1{
2 "data": "ENC[AES256_GCM,data:CC4g1CDj61PeSk9w9OAiKaQkkXS51H+IcodzsZLYTNfLgAkqbuRSLpE7g2Km7vUb5L0/Yb8Ew1C+JMN3AKudJf3YOJRr1A8M1z3hTJVzW4qSEkj3aPIek3DuqE2Wl8KrDNxyQUylPRFkwMCmcAySlLCmztCU4qu/ZdcCg0O1EE9E0AdxhbKtBZXQUX6KrTyRLLek7h1prfre6w8Gi+3Y8N5yMmFBF8XEcC6xOWglVMOR1eBQlS9iPu4rhFbWqM6YQ7ptI08xZcpCyM4sz+S3fqf0Nysm+19KBzw8g3JikQzqnA==,iv:gNH2tGj8VRxCAJgYuFcrznP7/K4tpwD09Wg/o7McpyE=,tag:Bh3JTL1MkKwLG0d120nnig==,type:str]",
3 "sops": {
4 "kms": null,
5 "gcp_kms": null,
6 "azure_kv": null,
7 "hc_vault": null,
8 "age": null,
9 "lastmodified": "2022-05-19T18:42:23Z",
10 "mac": "ENC[AES256_GCM,data:dQAeiVPBGotOd3dnD9P3o1dlDIrOom369SAlzY9VHe4y/Bck8brrx4fUjjxfFB9/Oew83Pdpl1WXbVp6RVrsdY/xTmVD+1bgZJJRJ5KYe0QcoWl4Sv1E6Y1b5jKZVYbeiCU7NI6gITmM5sLNBzEm2WYsYBtRCxWMh3iGV7ZqmAk=,iv:loxamarLwR6NCHaH/K8tq8XQj7Xl+Onbgu3hEYZycKQ=,tag:WojOpPzi/ajmzBAKKJ7g1Q==,type:str]",
11 "pgp": [
12 {
13 "created_at": "2022-05-19T18:42:23Z",
14 "enc": "-----BEGIN PGP MESSAGE-----\n\nhF4DXxoViZlp6dISAQdAy74slNS/OZAJ2BczfZtCWNdIfrCpT9qg3K17zaam930w\nWRVJeL/4JLyaCvDybqNjyoi7TkCxMtKNu5LzWv+c7iTQgAwyH/aRdaLx4HmEnwqW\n0l4BsKAIB+GNBAO/HUrjrxc16euyNPP0zbguiEUxhzNGb3xwngixbcDBIe8d4yXa\nHQ+mhjG35wQbjcPrQFUvZ5YWkwthL3pY1Jx8l/9V8ajTC3SbHlI2akbun6EMuoZo\n=LKNF\n-----END PGP MESSAGE-----\n",
15 "fp": "30D3453B8CD02FE2A3E7C78C0FB536FB87AE8F51"
16 },
17 {
18 "created_at": "2022-05-19T18:42:23Z",
19 "enc": "-----BEGIN PGP MESSAGE-----\n\nhF4DyFKFNkTVG5oSAQdAT8dopGD88h4G6EBdFbDWizpUreWer6d7U+ii48YYe2Aw\nh8NZe+WplrMmjIWalVylf/MqQKlAwbOZBj5PpFIxFXKvtRxGGYKZ7mBj7kkFaDKG\n0l4BkYVQRhouZdVFcpTtTPlG7ATVpJQAi8UiBuO0HhQBmxQUGLl5vM9bvb9cY5mH\nBnBOWYzff/f0Jl8gn3tGMr9Sxeg7VRcCm+YGMPMQSimKbEZnXUjGEYuflXzopY09\n=6n0A\n-----END PGP MESSAGE-----\n",
20 "fp": "7ED22F4AA7BB55728B643DC5471B7D88E4EF66F8"
21 }
22 ],
23 "unencrypted_suffix": "_unencrypted",
24 "version": "3.7.2"
25 }
26} \ No newline at end of file
diff --git a/hosts/surtr/email/spm/default.nix b/hosts/surtr/email/spm/default.nix
new file mode 100644
index 00000000..75f99d8d
--- /dev/null
+++ b/hosts/surtr/email/spm/default.nix
@@ -0,0 +1,24 @@
1{ haskell, fetchFromGitHub }:
2
3let
4 # defaultPackages = (import ./stackage.nix {});
5 # haskellPackages = defaultPackages // argumentPackages;
6 # haskellPackages = argumentPackages;
7 haskellPackages = haskell.packages.ghc922.override {
8 overrides = self: super: {
9 warp-systemd = haskell.lib.doJailbreak (super.warp-systemd.overrideAttrs (oldAttrs: { meta = oldAttrs.meta // { broken = false; }; }));
10 servant-server = super.servant-server.overrideAttrs (oldAttrs: {
11 patches = [];
12 });
13 hpack = super.hpack.overrideAttrs (oldAttrs: rec {
14 version = "0.35.0";
15 src = fetchFromGitHub {
16 owner = "sol";
17 repo = "hpack";
18 rev = "0.35.0";
19 hash = "sha256-DMxCHwz9x2e4kSOIk1/qeW3aDFHw88LNW+4vXxDV9EI=";
20 };
21 });
22 };
23 };
24in haskellPackages.callPackage ./spm.nix {}
diff --git a/hosts/surtr/email/spm/lib/Data/CaseInsensitive/Instances.hs b/hosts/surtr/email/spm/lib/Data/CaseInsensitive/Instances.hs
new file mode 100644
index 00000000..56cba98a
--- /dev/null
+++ b/hosts/surtr/email/spm/lib/Data/CaseInsensitive/Instances.hs
@@ -0,0 +1,19 @@
1{-# OPTIONS_GHC -fno-warn-orphans #-}
2
3module Data.CaseInsensitive.Instances () where
4
5import Prelude
6
7import Data.CaseInsensitive (CI)
8import qualified Data.CaseInsensitive as CI
9
10import Servant.API.ContentTypes
11
12import Data.Aeson
13
14
15instance MimeRender PlainText a => MimeRender PlainText (CI a) where
16 mimeRender p = mimeRender p . CI.original
17
18instance ToJSON a => ToJSON (CI a) where
19 toJSON = toJSON . CI.original
diff --git a/hosts/surtr/email/spm/lib/Data/UUID/Instances.hs b/hosts/surtr/email/spm/lib/Data/UUID/Instances.hs
new file mode 100644
index 00000000..335937d8
--- /dev/null
+++ b/hosts/surtr/email/spm/lib/Data/UUID/Instances.hs
@@ -0,0 +1,18 @@
1{-# OPTIONS_GHC -fno-warn-orphans #-}
2
3module Data.UUID.Instances () where
4
5import Prelude
6import Data.UUID (UUID)
7import qualified Data.UUID as UUID
8import Servant.API.ContentTypes
9
10
11instance MimeRender PlainText UUID where
12 mimeRender p = mimeRender p . UUID.toText
13
14instance MimeRender JSON UUID where
15 mimeRender p = mimeRender p . UUID.toText
16
17instance MimeRender OctetStream UUID where
18 mimeRender p = mimeRender p . UUID.toByteString
diff --git a/hosts/surtr/email/spm/lib/Spm.hs b/hosts/surtr/email/spm/lib/Spm.hs
new file mode 100644
index 00000000..c7f7dfe5
--- /dev/null
+++ b/hosts/surtr/email/spm/lib/Spm.hs
@@ -0,0 +1,5 @@
1module Spm
2 ( module Spm.Api
3 ) where
4
5import Spm.Api
diff --git a/hosts/surtr/email/spm/lib/Spm/Api.hs b/hosts/surtr/email/spm/lib/Spm/Api.hs
new file mode 100644
index 00000000..d9644222
--- /dev/null
+++ b/hosts/surtr/email/spm/lib/Spm/Api.hs
@@ -0,0 +1,40 @@
1{-# LANGUAGE TemplateHaskell #-}
2
3module Spm.Api
4 ( SpmMailbox
5 , SpmApi, spmApi
6 ) where
7
8import Prelude
9
10import Servant.API
11
12import Data.Proxy (Proxy(..))
13
14import Data.Text (Text)
15
16import GHC.Generics (Generic)
17import Type.Reflection (Typeable)
18
19import Control.Lens.TH
20
21import Data.CaseInsensitive (CI)
22import Data.CaseInsensitive.Instances ()
23
24import Crypto.JOSE.JWK (JWKSet)
25
26import Data.UUID (UUID)
27import Data.UUID.Instances ()
28
29
30newtype SpmMailbox = SpmMailbox { unSpmMailbox :: CI Text }
31 deriving stock (Eq, Ord, Read, Show, Generic, Typeable)
32 deriving newtype (MimeRender JSON, MimeRender PlainText)
33makeWrapped ''SpmMailbox
34
35type SpmApi = "whoami" :> Get '[PlainText, JSON] SpmMailbox
36 :<|> ".well-known" :> "jwks.json" :> Get '[JSON] JWKSet
37 :<|> "instance-id" :> Get '[PlainText, JSON, OctetStream] UUID
38
39spmApi :: Proxy SpmApi
40spmApi = Proxy
diff --git a/hosts/surtr/email/spm/package.yaml b/hosts/surtr/email/spm/package.yaml
new file mode 100644
index 00000000..4859e38c
--- /dev/null
+++ b/hosts/surtr/email/spm/package.yaml
@@ -0,0 +1,92 @@
1name: spm
2version: 0.1.0
3
4default-extensions:
5 - NoImplicitPrelude
6 - DerivingStrategies
7 - DeriveAnyClass
8 - DataKinds
9 - RecordWildCards
10 - TypeFamilies
11 - LambdaCase
12other-extensions:
13 - OverloadedStrings
14 - TemplateHaskell
15 - QuasiQuotes
16 - UndecidableInstances
17language: GHC2021
18license: AGPL-3.0-or-later
19ghc-options:
20 - -Wall
21 - -Wmissing-home-modules
22 - -Wredundant-constraints
23 - -Widentities
24 - -Wincomplete-uni-patterns
25 - -Werror
26 - -fwarn-tabs
27 - -j -O
28
29library:
30 dependencies:
31 - base
32 - servant
33 - text
34 - lens
35 - case-insensitive
36 - aeson
37 - jose
38 - uuid
39 source-dirs:
40 - lib
41
42executables:
43 spm-server:
44 dependencies:
45 - spm
46 - base
47 - servant-server
48 - warp-systemd
49 - warp
50 - attoparsec
51 - text
52 - bytestring
53 - wai
54 - wai-extra
55 - lens
56 - case-insensitive
57 - http-types
58 - persistent
59 - persistent-postgresql
60 - uuid
61 - path-pieces
62 - transformers
63 - mtl
64 - resource-pool
65 - monad-logger
66 - mmorph
67 - unliftio-core
68 - http-api-data
69 - exceptions
70 - aeson
71 - filepath
72 - jose
73
74 source-dirs:
75 - server
76
77 main: Spm.Server
78 spm-provision:
79 dependencies:
80 - base
81 - jose
82 - uuid
83 - optparse-applicative
84 - text
85 - aeson
86 - bytestring
87 - lens
88
89 source-dirs:
90 - provision
91
92 main: Spm.Provision
diff --git a/hosts/surtr/email/spm/provision/Spm/Provision.hs b/hosts/surtr/email/spm/provision/Spm/Provision.hs
new file mode 100644
index 00000000..ff18baa0
--- /dev/null
+++ b/hosts/surtr/email/spm/provision/Spm/Provision.hs
@@ -0,0 +1,46 @@
1module Spm.Provision
2 ( main
3 ) where
4
5import Prelude
6import Options.Applicative
7import Control.Monad
8
9import qualified Data.Text.IO as Text
10
11import qualified Data.UUID as UUID
12import qualified Data.UUID.V4 as UUID
13
14import Crypto.JOSE.JWK
15
16import qualified Data.ByteString.Lazy.Char8 as CLBS
17import qualified Data.Aeson as JSON
18
19import Control.Lens
20
21
22data Command
23 = InstanceId
24 | JwkSet
25 deriving stock (Eq, Ord, Read, Show)
26
27cmdInstanceId :: IO ()
28cmdInstanceId = Text.putStrLn . UUID.toText =<< UUID.nextRandom
29
30cmdJwkSet :: IO ()
31cmdJwkSet = do
32 k' <- genJWK (OKPGenParam Ed25519)
33 kid <- UUID.nextRandom
34 let k = k' & jwkKid ?~ UUID.toText kid
35 & jwkUse ?~ Sig
36 & jwkKeyOps ?~ [Sign, Verify]
37 CLBS.putStrLn . JSON.encode . JWKSet $ pure k
38
39opts :: Parser (IO ())
40opts = subparser $
41 command "instance-id" (info (pure cmdInstanceId) idm)
42 <> command "jwk-set" (info (pure cmdJwkSet) idm)
43
44
45main :: IO ()
46main = join $ execParser (info opts idm)
diff --git a/hosts/surtr/email/spm/server/Crypto/JOSE/JWK/Instances.hs b/hosts/surtr/email/spm/server/Crypto/JOSE/JWK/Instances.hs
new file mode 100644
index 00000000..44a5cfe0
--- /dev/null
+++ b/hosts/surtr/email/spm/server/Crypto/JOSE/JWK/Instances.hs
@@ -0,0 +1,9 @@
1{-# LANGUAGE TemplateHaskell #-}
2{-# OPTIONS_GHC -fno-warn-orphans #-}
3
4module Crypto.JOSE.JWK.Instances () where
5
6import Control.Lens.TH
7import Crypto.JOSE.JWK
8
9makeWrapped ''JWKSet
diff --git a/hosts/surtr/email/spm/server/Data/CaseInsensitive/Instances.hs b/hosts/surtr/email/spm/server/Data/CaseInsensitive/Instances.hs
new file mode 100644
index 00000000..1f3f7a11
--- /dev/null
+++ b/hosts/surtr/email/spm/server/Data/CaseInsensitive/Instances.hs
@@ -0,0 +1,30 @@
1{-# OPTIONS_GHC -fno-warn-orphans #-}
2{-# LANGUAGE OverloadedStrings #-}
3
4module Data.CaseInsensitive.Instances () where
5
6import Prelude
7import Database.Persist
8import Database.Persist.Sql
9
10import Data.CaseInsensitive (CI)
11import qualified Data.CaseInsensitive as CI
12
13import Data.Text (Text)
14import qualified Data.Text as Text
15import qualified Data.Text.Encoding as Text
16
17import Control.Exception
18
19
20instance PersistField (CI Text) where
21 toPersistValue = PersistLiteralEscaped . Text.encodeUtf8 . CI.original
22 fromPersistValue = \case
23 PersistText t -> Right $ CI.mk t
24 PersistLiteralEscaped bs -> case Text.decodeUtf8' bs of
25 Right t -> Right $ CI.mk t
26 Left err -> Left $ "Could not decode PersistLiteral as UTF-8: " <> Text.pack (displayException err)
27 o -> Left $ "Expected PersistText or PersistLiteral but got ‘" <> Text.pack (show o) <> "’"
28
29instance PersistFieldSql (CI Text) where
30 sqlType _ = SqlOther "citext"
diff --git a/hosts/surtr/email/spm/server/Data/UUID/Instances.hs b/hosts/surtr/email/spm/server/Data/UUID/Instances.hs
new file mode 100644
index 00000000..b2268c96
--- /dev/null
+++ b/hosts/surtr/email/spm/server/Data/UUID/Instances.hs
@@ -0,0 +1,31 @@
1{-# OPTIONS_GHC -fno-warn-orphans #-}
2{-# LANGUAGE OverloadedStrings #-}
3
4module Data.UUID.Instances () where
5
6import Prelude
7import Database.Persist
8import Database.Persist.Sql
9import Data.UUID (UUID)
10import qualified Data.UUID as UUID
11
12import qualified Data.ByteString.Char8 as CBS
13import qualified Data.Text as Text
14
15import Web.PathPieces
16
17
18instance PersistField UUID where
19 toPersistValue = PersistLiteralEscaped . CBS.pack . UUID.toString
20 fromPersistValue (PersistLiteralEscaped uuidB8) =
21 case UUID.fromString $ CBS.unpack uuidB8 of
22 Just uuid -> Right uuid
23 Nothing -> Left "Invalid UUID"
24 fromPersistValue v = Left $ "Expected PersistLiteral but got ‘" <> Text.pack (show v) <> "’"
25
26instance PersistFieldSql UUID where
27 sqlType _ = SqlOther "uuid"
28
29instance PathPiece UUID where
30 toPathPiece = Text.pack . UUID.toString
31 fromPathPiece = UUID.fromString . Text.unpack
diff --git a/hosts/surtr/email/spm/server/Spm/Server.hs b/hosts/surtr/email/spm/server/Spm/Server.hs
new file mode 100644
index 00000000..7690f51a
--- /dev/null
+++ b/hosts/surtr/email/spm/server/Spm/Server.hs
@@ -0,0 +1,194 @@
1{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}
2
3module Spm.Server
4 ( main
5 ) where
6
7import Prelude
8import Spm.Api
9import Servant
10import Servant.Server.Experimental.Auth
11
12import Network.Wai
13import Network.Wai.Handler.Warp
14import Network.Wai.Handler.Warp.Systemd
15import Network.Wai.Middleware.RequestLogger
16
17import Network.HTTP.Types
18
19import Data.Text (Text)
20import qualified Data.Text as Text
21import qualified Data.Text.Encoding as Text
22import Data.Attoparsec.Text
23
24import qualified Data.ByteString.Lazy as LBS
25
26import GHC.Generics (Generic)
27import Type.Reflection (Typeable)
28
29import Control.Applicative
30import Control.Monad
31import Control.Arrow
32import Control.Monad.IO.Class
33import Control.Monad.IO.Unlift
34
35import Control.Lens hiding (Context)
36
37import qualified Data.CaseInsensitive as CI
38
39import System.IO
40
41import Spm.Server.Database
42
43import Database.Persist
44import Database.Persist.Postgresql
45import Data.Pool
46
47import Control.Monad.Trans.Reader (ReaderT, runReaderT)
48
49import Control.Monad.Logger
50
51import Control.Monad.Morph
52
53import System.Environment
54
55import Control.Monad.Catch (Exception, MonadThrow(..))
56
57import Data.UUID (UUID)
58import qualified Data.UUID as UUID
59
60import qualified Data.Aeson as JSON
61
62import System.FilePath ((</>), isRelative)
63
64import Crypto.JOSE.JWK hiding (Context)
65import Crypto.JOSE.JWK.Instances ()
66
67import Data.Maybe
68
69
70hSslClientVerify, hSslClientSDn :: HeaderName
71hSslClientVerify = "SSL-Client-Verify"
72hSslClientSDn = "SSL-Client-S-DN"
73
74
75data SSLClientVerify
76 = SSLClientVerifySuccess
77 | SSLClientVerifyOther Text
78 deriving (Eq, Ord, Read, Show, Generic, Typeable)
79instance FromHttpApiData SSLClientVerify where
80 parseUrlPiece = (left Text.pack .) . parseOnly $ p <* endOfInput
81 where
82 p :: Parser SSLClientVerify
83 p = (SSLClientVerifySuccess <$ asciiCI "success")
84 <|> (SSLClientVerifyOther <$> takeText)
85
86type instance AuthServerData (AuthProtect "spm_mailbox") = MailMailbox
87
88type SpmServerApi = Header' '[Required, Strict] "SPM-Domain" MailDomain
89 :> AuthProtect "spm_mailbox"
90 :> SpmApi
91
92spmServerApi :: Proxy SpmServerApi
93spmServerApi = Proxy
94
95
96requestMailMailbox :: Request -> Either Text MailMailbox
97requestMailMailbox req = do
98 clientVerify <- getHeader hSslClientVerify
99 clientSDN <- getHeader hSslClientSDn
100
101 case clientVerify of
102 SSLClientVerifySuccess -> return ()
103 o@(SSLClientVerifyOther _) -> Left $ "Expected “SSLClientVerifySuccess”, but got “" <> Text.pack (show o) <> "”"
104 spmMailbox <- left Text.pack $ parseOnly (asciiCI "CN=" *> (CI.mk <$> takeText) <* endOfInput) clientSDN
105
106 return $ _Wrapped # spmMailbox
107 where
108 getHeader :: forall a. FromHttpApiData a => HeaderName -> Either Text a
109 getHeader hdrName = parseHeader <=< maybeToEither ("Missing “" <> Text.decodeUtf8 (CI.original hdrName) <> "”") . lookup hdrName $ requestHeaders req
110
111 maybeToEither e = maybe (Left e) Right
112
113mailboxAuthHandler :: AuthHandler Request MailMailbox
114mailboxAuthHandler = mkAuthHandler handler
115 where
116 throw401 msg = throwError $ err401 { errBody = LBS.fromStrict $ Text.encodeUtf8 msg }
117 handler = either throw401 return . requestMailMailbox
118
119mkSpmRequestLogger :: MonadIO m => m Middleware
120mkSpmRequestLogger = liftIO $ mkRequestLogger loggerSettings
121 where
122 loggerSettings = defaultRequestLoggerSettings
123 { destination = Handle stderr
124 , outputFormat = ApacheWithSettings $ defaultApacheSettings
125 & setApacheUserGetter (preview (_Right . _Wrapped . to (Text.encodeUtf8. CI.original)) . requestMailMailbox)
126 & setApacheIPAddrSource FromFallback
127 }
128
129data ServerCtx = ServerCtx
130 { _sctxSqlPool :: Pool SqlBackend
131 , _sctxInstanceId :: UUID
132 , _sctxJwkSet :: JWKSet
133 } deriving (Generic, Typeable)
134makeLenses ''ServerCtx
135
136type Handler' = ReaderT ServerCtx (LoggingT Handler)
137type Server' api = ServerT api Handler'
138
139data ServerCtxError
140 = ServerCtxNoInstanceId | ServerCtxInvalidInstanceId
141 | ServerCtxJwkSetCredentialFileNotRelative
142 | ServerCtxNoCredentialsDirectory
143 | ServerCtxJwkSetDecodeError String
144 | ServerCtxJwkSetEmpty
145 deriving stock (Eq, Ord, Read, Show, Generic, Typeable)
146 deriving anyclass (Exception)
147
148mkSpmApp :: (MonadUnliftIO m, MonadThrow m) => m Application
149mkSpmApp = do
150 requestLogger <- mkSpmRequestLogger
151
152 connStr <- liftIO $ maybe mempty (Text.encodeUtf8 . Text.pack) <$> lookupEnv "PGCONNSTR"
153 _sctxInstanceId <- maybe (throwM ServerCtxInvalidInstanceId) return . UUID.fromString =<< maybe (throwM ServerCtxNoInstanceId) return =<< liftIO (lookupEnv "SPM_INSTANCE")
154 jwksetCredentialFile <- liftIO $ fromMaybe "spm-keys.json" <$> lookupEnv "SPM_KEYS_CREDENTIAL"
155 unless (isRelative jwksetCredentialFile) $ throwM ServerCtxJwkSetCredentialFileNotRelative
156 credentialsDir <- maybe (throwM ServerCtxNoCredentialsDirectory) return =<< liftIO (lookupEnv "CREDENTIALS_DIRECTORY")
157 _sctxJwkSet@(JWKSet jwks) <- either (throwM . ServerCtxJwkSetDecodeError) return =<< liftIO (JSON.eitherDecodeFileStrict' $ credentialsDir </> jwksetCredentialFile)
158 when (null jwks) $ throwM ServerCtxJwkSetEmpty
159
160 runStderrLoggingT . withPostgresqlPool connStr 1 $ \_sctxSqlPool -> do
161 let
162 spmServerContext :: Context (AuthHandler Request MailMailbox ': '[])
163 spmServerContext = mailboxAuthHandler :. EmptyContext
164
165 spmServer' = spmServer
166
167 logger <- askLoggerIO
168 return $ serveWithContextT spmServerApi spmServerContext ((runReaderT ?? ServerCtx{..}) . hoist (runLoggingT ?? logger)) spmServer'
169 & requestLogger
170
171spmSql :: ReaderT SqlBackend Handler' a -> Handler' a
172spmSql act = do
173 sqlPool <- view sctxSqlPool
174 withResource sqlPool $ runReaderT act
175
176spmServer :: MailDomain -> MailMailbox -> Server' SpmApi
177spmServer _dom mbox = whoami
178 :<|> jwkSet
179 :<|> instanceId
180 where
181 whoami = do
182 Entity _ Mailbox{mailboxIdent} <- maybe (throwError err404) return <=< spmSql . getBy $ UniqueMailbox mbox
183 return $ mailboxIdent ^. _Wrapped . re _Wrapped
184
185 jwkSet = views sctxJwkSet $ over _Wrapped (^.. folded . asPublicKey . _Just)
186
187 instanceId = view sctxInstanceId
188
189main :: IO ()
190main = runSystemdWarp systemdSettings warpSettings =<< mkSpmApp
191 where
192 systemdSettings = defaultSystemdSettings
193 & requireSocketActivation .~ True
194 warpSettings = defaultSettings
diff --git a/hosts/surtr/email/spm/server/Spm/Server/Database.hs b/hosts/surtr/email/spm/server/Spm/Server/Database.hs
new file mode 100644
index 00000000..09b4c67b
--- /dev/null
+++ b/hosts/surtr/email/spm/server/Spm/Server/Database.hs
@@ -0,0 +1,72 @@
1{-# LANGUAGE OverloadedStrings, TemplateHaskell, QuasiQuotes, UndecidableInstances #-}
2
3module Spm.Server.Database
4 ( MailMailbox, MailLocal, MailExtension, MailDomain
5 , Mailbox(..), MailboxMapping(..)
6 , Unique(..)
7 ) where
8
9import Prelude
10
11import Database.Persist
12import Database.Persist.Sql
13import Database.Persist.TH
14
15import GHC.Generics (Generic)
16import Type.Reflection (Typeable)
17
18import Data.Text (Text)
19
20import Data.CaseInsensitive (CI)
21import qualified Data.CaseInsensitive as CI
22import Data.CaseInsensitive.Instances ()
23
24import Data.UUID (UUID)
25import Data.UUID.Instances ()
26
27import Data.Int (Int64)
28
29import Control.Lens
30
31import Web.HttpApiData
32
33
34newtype MailMailbox = MailMailbox
35 { unMailMailbox :: CI Text
36 } deriving stock (Eq, Ord, Read, Show, Generic, Typeable)
37 deriving newtype (PersistField, PersistFieldSql)
38makeWrapped ''MailMailbox
39newtype MailLocal = MailLocal
40 { unMailLocal :: CI Text
41 } deriving stock (Eq, Ord, Read, Show, Generic, Typeable)
42 deriving newtype (PersistField, PersistFieldSql)
43makeWrapped ''MailLocal
44newtype MailExtension = MailExtension
45 { unMailExtension :: CI Text
46 } deriving stock (Eq, Ord, Read, Show, Generic, Typeable)
47 deriving newtype (PersistField, PersistFieldSql)
48makeWrapped ''MailExtension
49newtype MailDomain = MailDomain
50 { unMailDomain :: CI Text
51 } deriving stock (Eq, Ord, Read, Show, Generic, Typeable)
52 deriving newtype (PersistField, PersistFieldSql)
53makeWrapped ''MailDomain
54
55instance FromHttpApiData MailDomain where
56 parseUrlPiece = fmap (review _Wrapped . CI.mk) . parseUrlPiece
57
58
59share [mkPersist sqlSettings] [persistLowerCase|
60 Mailbox
61 Id UUID sqltype=uuid default=gen_random_uuid()
62 ident MailMailbox sql=mailbox
63 quota Int64 Maybe sql=quota_bytes MigrationOnly
64 UniqueMailbox ident
65 deriving Show
66 MailboxMapping
67 Id UUID sqltype=uuid default=gen_random_uuid()
68 local MailLocal Maybe
69 extension MailExtension Maybe
70 domain MailDomain
71 mailbox MailboxId
72|]
diff --git a/hosts/surtr/email/spm/spm.nix b/hosts/surtr/email/spm/spm.nix
new file mode 100644
index 00000000..ba7a5f0b
--- /dev/null
+++ b/hosts/surtr/email/spm/spm.nix
@@ -0,0 +1,28 @@
1{ mkDerivation, aeson, attoparsec, base, bytestring
2, case-insensitive, exceptions, filepath, hpack, http-api-data
3, http-types, jose, lens, lib, mmorph, monad-logger, mtl
4, optparse-applicative, path-pieces, persistent
5, persistent-postgresql, resource-pool, servant, servant-server
6, text, transformers, unliftio-core, uuid, wai, wai-extra, warp
7, warp-systemd
8}:
9mkDerivation {
10 pname = "spm";
11 version = "0.1.0";
12 src = ./.;
13 isLibrary = true;
14 isExecutable = true;
15 libraryHaskellDepends = [
16 aeson base case-insensitive jose lens servant text
17 ];
18 libraryToolDepends = [ hpack ];
19 executableHaskellDepends = [
20 aeson attoparsec base bytestring case-insensitive exceptions
21 filepath http-api-data http-types jose lens mmorph monad-logger mtl
22 optparse-applicative path-pieces persistent persistent-postgresql
23 resource-pool servant-server text transformers unliftio-core uuid
24 wai wai-extra warp warp-systemd
25 ];
26 prePatch = "hpack";
27 license = lib.licenses.agpl3Plus;
28}
diff --git a/hosts/surtr/postgresql.nix b/hosts/surtr/postgresql.nix
index a5e93ecf..66ce60eb 100644
--- a/hosts/surtr/postgresql.nix
+++ b/hosts/surtr/postgresql.nix
@@ -6,20 +6,6 @@ in {
6 services.postgresql = { 6 services.postgresql = {
7 enable = true; 7 enable = true;
8 package = pkgs.postgresql_14; 8 package = pkgs.postgresql_14;
9 initialScript = pkgs.writeText "schema.sql" ''
10 CREATE DATABASE "matrix-synapse" WITH TEMPLATE "template0" ENCODING "UTF8" LOCALE "C";
11 CREATE USER "matrix-synapse";
12 GRANT ALL PRIVILEGES ON DATABASE "matrix-synapse" TO "matrix-synapse";
13 GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "matrix-synapse";
14
15 CREATE DATABASE "email" WITH TEMPLATE "template0" ENCODING "UTF8" LOCALE "C";
16 CREATE USER "postfix";
17 GRANT CONNECT ON DATABASE "email" TO "postfix";
18 ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO "postfix";
19 CREATE USER "dovecot2";
20 GRANT CONNECT ON DATABASE "email" TO "dovecot2";
21 ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO "dovecot2";
22 '';
23 }; 9 };
24 10
25 systemd.services.migrate-postgresql = { 11 systemd.services.migrate-postgresql = {
@@ -35,10 +21,46 @@ in {
35 21
36 path = [ config.services.postgresql.package ]; 22 path = [ config.services.postgresql.package ];
37 script = '' 23 script = ''
24 psql postgres postgres -eXf ${pkgs.writeText "schema.sql" ''
25 CREATE DATABASE "matrix-synapse" WITH TEMPLATE "template0" ENCODING "UTF8" LOCALE "C";
26 CREATE DATABASE "email" WITH TEMPLATE "template0" ENCODING "UTF8" LOCALE "C";
27 ''}
28
29 psql matrix-synapse postgres -eXf ${pkgs.writeText "matrix-synapse.sql" ''
30 \i ${versioning + "/install.versioning.sql"}
31
32 BEGIN;
33 SELECT _v.register_patch('000-matrix-users', null, null);
34
35 CREATE USER "matrix-synapse";
36 GRANT ALL PRIVILEGES ON DATABASE "matrix-synapse" TO "matrix-synapse";
37 GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "matrix-synapse";
38 COMMIT;
39 ''}
40
38 psql email postgres -eXf ${pkgs.writeText "email.sql" '' 41 psql email postgres -eXf ${pkgs.writeText "email.sql" ''
39 \i ${versioning + "/install.versioning.sql"} 42 \i ${versioning + "/install.versioning.sql"}
40 43
41 BEGIN; 44 BEGIN;
45 SELECT _v.register_patch('000-users', null, null);
46
47 CREATE USER "postfix";
48 GRANT CONNECT ON DATABASE "email" TO "postfix";
49 ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO "postfix";
50 CREATE USER "dovecot2";
51 GRANT CONNECT ON DATABASE "email" TO "dovecot2";
52 ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO "dovecot2";
53 COMMIT;
54
55 BEGIN;
56 SELECT _v.register_patch('001-spm', null, null);
57
58 CREATE USER "spm";
59 GRANT CONNECT ON DATABASE "email" TO "spm";
60 ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES to "spm";
61 COMMIT;
62
63 BEGIN;
42 SELECT _v.register_patch('000-base', null, null); 64 SELECT _v.register_patch('000-base', null, null);
43 65
44 CREATE TABLE mailbox ( 66 CREATE TABLE mailbox (
@@ -109,6 +131,18 @@ in {
109 ALTER TABLE mailbox_mapping DROP CONSTRAINT mailbox_mapping_mailbox_fkey; 131 ALTER TABLE mailbox_mapping DROP CONSTRAINT mailbox_mapping_mailbox_fkey;
110 ALTER TABLE mailbox_mapping ADD CONSTRAINT mailbox_mapping_mailbox_fkey FOREIGN KEY (mailbox) REFERENCES mailbox(id) ON DELETE CASCADE ON UPDATE RESTRICT; 132 ALTER TABLE mailbox_mapping ADD CONSTRAINT mailbox_mapping_mailbox_fkey FOREIGN KEY (mailbox) REFERENCES mailbox(id) ON DELETE CASCADE ON UPDATE RESTRICT;
111 COMMIT; 133 COMMIT;
134
135 BEGIN;
136 SELECT _v.register_patch('005-spm', ARRAY['000-base', '002-citext', '003-extensions'], null);
137
138 GRANT INSERT ON "mailbox_mapping" TO "spm";
139 COMMIT;
140
141 BEGIN;
142 SELECT _v.register_patch('006-spm-mailbox', ARRAY['000-base'], null);
143
144 GRANT SELECT ON ALL TABLES IN SCHEMA public TO "spm";
145 COMMIT;
112 ''} 146 ''}
113 ''; 147 '';
114 }; 148 };
diff --git a/hosts/surtr/ruleset.nft b/hosts/surtr/ruleset.nft
index bfa27d41..51fcd498 100644
--- a/hosts/surtr/ruleset.nft
+++ b/hosts/surtr/ruleset.nft
@@ -159,7 +159,7 @@ table inet filter {
159 meta l4proto $icmp_protos counter name icmp-rx accept 159 meta l4proto $icmp_protos counter name icmp-rx accept
160 160
161 tcp dport 22 counter name ssh-rx accept 161 tcp dport 22 counter name ssh-rx accept
162 udp dport 60001-61000 counter name mosh-rx accept 162 udp dport 60000-61000 counter name mosh-rx accept
163 163
164 meta protocol ip udp dport 51820 counter name wg-rx accept 164 meta protocol ip udp dport 51820 counter name wg-rx accept
165 meta protocol ip6 udp dport {51821, 51822} counter name wg-rx accept 165 meta protocol ip6 udp dport {51821, 51822} counter name wg-rx accept
@@ -203,7 +203,7 @@ table inet filter {
203 203
204 204
205 tcp sport 22 counter name ssh-tx 205 tcp sport 22 counter name ssh-tx
206 udp sport 60001-61000 counter name mosh-tx 206 udp sport 60000-61000 counter name mosh-tx
207 207
208 tcp sport 53 counter name dns-tx 208 tcp sport 53 counter name dns-tx
209 udp sport 53 counter name dns-tx 209 udp sport 53 counter name dns-tx
diff --git a/hosts/vidhar/network/ruleset.nft b/hosts/vidhar/network/ruleset.nft
index d956cb74..c0da0fa6 100644
--- a/hosts/vidhar/network/ruleset.nft
+++ b/hosts/vidhar/network/ruleset.nft
@@ -163,7 +163,7 @@ table inet filter {
163 meta l4proto $icmp_protos counter name icmp-rx accept 163 meta l4proto $icmp_protos counter name icmp-rx accept
164 164
165 iifname { lan, mgmt, dsl, yggdrasil, bifrost } tcp dport 22 counter name ssh-rx accept 165 iifname { lan, mgmt, dsl, yggdrasil, bifrost } tcp dport 22 counter name ssh-rx accept
166 iifname { lan, mgmt, dsl, yggdrasil, bifrost } udp dport 60001-61000 counter name mosh-rx accept 166 iifname { lan, mgmt, dsl, yggdrasil, bifrost } udp dport 60000-61000 counter name mosh-rx accept
167 167
168 iifname { lan, mgmt, dmz01, yggdrasil } tcp dport 53 counter name dns-rx accept 168 iifname { lan, mgmt, dmz01, yggdrasil } tcp dport 53 counter name dns-rx accept
169 iifname { lan, mgmt, dmz01, yggdrasil } udp dport 53 counter name dns-rx accept 169 iifname { lan, mgmt, dmz01, yggdrasil } udp dport 53 counter name dns-rx accept
@@ -207,7 +207,7 @@ table inet filter {
207 207
208 208
209 tcp sport 22 counter name ssh-tx 209 tcp sport 22 counter name ssh-tx
210 udp sport 60001-61000 counter name mosh-tx 210 udp sport 60000-61000 counter name mosh-tx
211 211
212 tcp sport 53 counter name dns-tx 212 tcp sport 53 counter name dns-tx
213 udp sport 53 counter name dns-tx 213 udp sport 53 counter name dns-tx
diff --git a/shell.nix b/shell.nix
index 05ba992b..0b874564 100644
--- a/shell.nix
+++ b/shell.nix
@@ -25,5 +25,7 @@ in pkgs.mkShell {
25 knot-dns 25 knot-dns
26 yq 26 yq
27 nvfetcher 27 nvfetcher
28
29 (pkgs.callPackage hosts/surtr/email/spm {})
28 ]; 30 ];
29} 31}