From 13d8e6142dd8781e1c1b7b10e0a9004f3fd18ccf Mon Sep 17 00:00:00 2001 From: Julien Moutinho Date: Mon, 16 Jul 2018 21:43:20 +0200 Subject: [PATCH 1/1] init --- .envrc | 34 ++ Makefile | 75 +++ build/default.nix | 15 + build/modules.nix | 12 + build/modules/gnupg.nix | 23 + build/modules/nix-plugins.nix | 13 + install/logical.nix | 4 + install/logical/machine1.nix | 142 +++++ install/logical/machine1/dovecot.nix | 548 ++++++++++++++++++ install/logical/machine1/nginx.nix | 219 +++++++ install/logical/machine1/nsd.nix | 24 + .../logical/machine1/nsd/millogic.coop.nix | 61 ++ install/logical/machine1/postfix.nix | 437 ++++++++++++++ install/logical/machine1/postgrey.nix | 14 + install/logical/machine1/rmilter.nix | 84 +++ install/logical/machine1/shorewall.nix | 148 +++++ install/physical.nix | 2 + install/physical/nixos.nix | 15 + install/physical/virtualbox.nix | 46 ++ 19 files changed, 1916 insertions(+) create mode 100644 .envrc create mode 100644 Makefile create mode 100644 build/default.nix create mode 100644 build/modules.nix create mode 100644 build/modules/gnupg.nix create mode 100644 build/modules/nix-plugins.nix create mode 100644 install/logical.nix create mode 100644 install/logical/machine1.nix create mode 100644 install/logical/machine1/dovecot.nix create mode 100644 install/logical/machine1/nginx.nix create mode 100644 install/logical/machine1/nsd.nix create mode 100644 install/logical/machine1/nsd/millogic.coop.nix create mode 100644 install/logical/machine1/postfix.nix create mode 100644 install/logical/machine1/postgrey.nix create mode 100644 install/logical/machine1/rmilter.nix create mode 100644 install/logical/machine1/shorewall.nix create mode 100644 install/physical.nix create mode 100644 install/physical/nixos.nix create mode 100644 install/physical/virtualbox.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..ce74ae4 --- /dev/null +++ b/.envrc @@ -0,0 +1,34 @@ +# Let `nix build` put its result into rightly available commands. +export PATH="$PWD/result/bin:$PATH" + +# Use password-store on a local git repository. +export PASSWORD_STORE_DIR="$PWD/sec/var/pass" + +#NIX_PATH="nixpkgs=$PWD/lib/nixpkgs" +NIX_PATH="nixpkgs=/home/julm/src/nixpkgs" +NIX_PATH+=":nixsys=$PWD/lib/nixsys" +NIX_PATH+=":sys=$PWD" +#NIX_PATH+=":nixpkgs-overlays=$PWD/lib/nixsys/build/overlays.nix" +export NIX_PATH + +# Use NixOps as Disnix's provisioning backend +#export DISNIXOS_USE_NIXOPS=1 +#export DISNIX_CLIENT_INTERFACE=disnix-nixops-client +#export DISNIX_PROFILE=default +#export DISNIX_TARGET_PROPERTY=hostname +#export DYSNOMIA_STATEDIR=var/dysnomia + +# Use gpg on a local home. +#export GNUPGHOME=$PWD/sec/var/gnupg + +# Let NixOps use install/physical/${NIXOPS_DEPLOYMENT}.nix +export NIXOPS_DEPLOYMENT="virtualbox" +export NIXOPS_STATE="$PWD/sec/var/nixops/state.nixops" +# Extend the Nix interpreter +# to enable builtins.extraBuiltins, +# which provides an unsafe exec useful to get secrets +# from the local password-store. +NIXOPS_OPTS+=" --show-trace" +NIXOPS_OPTS+=" --option plugin-files $PWD/result/nix/plugins/libnix-extra-builtins.so" +NIXOPS_OPTS+=" --option extra-builtins-file $PWD/result/nix/extra-builtins.nix" +export NIXOPS_OPTS diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1f39dbc --- /dev/null +++ b/Makefile @@ -0,0 +1,75 @@ +# +## init +### +.PHONY: build +build: + nix build -f build \ + --out-link result \ + --show-trace $(NIX_BUILD) + tree result/ + +init: build + result/bin/init + +# +## vmdk +### +vmdk: var/virtualbox/nixops.vmdk +.PHONY: var/virtualbox/nixops.vmdk + +var/virtualbox/nixops.vmdk: + # NOTE: user's TMPDIR may be too small + TMPDIR=/tmp \ + time nix build \ + --out-link var/virtualbox \ + --argstr system x86_64-linux \ + -f "" + +# +## create +### +create: $(dir $(NIXOPS_STATE)) + $(if $(filter $(NIXOPS_DEPLOYMENT),$(deployment)),,\ + nixops create \ + $(NIXOPS_CREATE) \ + install/logical.nix \ + install/physical.nix) +modify: $(NIXOPS_STATE) + nixops modify \ + $(NIXOPS_MODIFY) \ + install/logical.nix \ + install/physical.nix + +$(dir $(NIXOPS_STATE)): + mkdir -p $@ + +deployment = $(shell \ + test ! -f $(NIXOPS_STATE) || \ + sqlite3 $(NIXOPS_STATE) \ + "select value from DeploymentAttrs \ + where name='name' and value='$(NIXOPS_DEPLOYMENT)';") + +# +## deploy +### +deploy: create + time nixops deploy $(NIXOPS_OPTS) $(NIXOPS_DEPLOY) + #TMPDIR=/tmp \ + #time disnixos-env --use-nixops \ + # -s install/disnix/services.nix \ + # -d install/disnix/distribution.nix \ + # -n install/logical.nix \ + # -n install/physical.nix +%/offline: NIXOPS_DEPLOY+=--option substituters "" +%/offline: % + + +stop: + nixops stop + +# +## state +### +state: $(NIXOPS_STATE) + sqlite3 $(NIXOPS_STATE) .dump + diff --git a/build/default.nix b/build/default.nix new file mode 100644 index 0000000..ee1c290 --- /dev/null +++ b/build/default.nix @@ -0,0 +1,15 @@ +{ pkgs ? import { overlays = import ; } +, lib ? pkgs.lib +, modules ? [ (import ./modules.nix ) ] +}: +let config = + (import { + inherit pkgs lib modules; + }).config; in +pkgs.stdenv.mkDerivation { + name = "logic.coop-sys-build"; + preferLocalBuild = true; + allowSubstitutes = false; + inherit (pkgs) coreutils; + builder = pkgs.writeText "builder.sh" config.init.builder; +} diff --git a/build/modules.nix b/build/modules.nix new file mode 100644 index 0000000..d0c3e6c --- /dev/null +++ b/build/modules.nix @@ -0,0 +1,12 @@ +{config, ...}: +{ + imports = [ + modules/gnupg.nix + modules/nix-plugins.nix + ]; + config = { + init = { + enable = true; + }; + }; +} diff --git a/build/modules/gnupg.nix b/build/modules/gnupg.nix new file mode 100644 index 0000000..09c63e8 --- /dev/null +++ b/build/modules/gnupg.nix @@ -0,0 +1,23 @@ +{config, ...}: +{ + config = { + gnupg = { + enable = true; + keys = { + "Millogic, Coop. " = { + uid = "Millogic, Coop. "; + algo = "rsa4096"; + expire = "1y"; + usage = ["cert" "sign"]; + passPath = "millogic.coop/gnupg/contact@"; + subKeys = [ + { algo = "rsa4096"; expire = "1y"; usage = ["sign"];} + { algo = "rsa4096"; expire = "1y"; usage = ["encrypt"];} + { algo = "rsa4096"; expire = "1y"; usage = ["auth"];} + ]; + backupRecipients = ["@julm@autogeree.net"]; + }; + }; + }; + }; +} diff --git a/build/modules/nix-plugins.nix b/build/modules/nix-plugins.nix new file mode 100644 index 0000000..79345e3 --- /dev/null +++ b/build/modules/nix-plugins.nix @@ -0,0 +1,13 @@ +{ config, ... }: +{ + config = { + nix-plugins = { + enable = true; + extra-builtins = '' + pass = path: exec [ "${config.nix-plugins.nix-pass}/bin/nix-pass" path ]; + git = dir: args: exec ([ "${config.nix-plugins.nix-git}/bin/nix-git" (builtins.toPath dir) ] ++ args); + git-time = dir: path: exec [ "${config.nix-plugins.nix-git}/bin/nix-git" (builtins.toPath dir) "log" "-1" "--format=%ct" "--" path ]; + ''; + }; + }; +} diff --git a/install/logical.nix b/install/logical.nix new file mode 100644 index 0000000..1983961 --- /dev/null +++ b/install/logical.nix @@ -0,0 +1,4 @@ +{ +network.description = "Millogic Network"; +machine1 = import logical/machine1.nix; +} diff --git a/install/logical/machine1.nix b/install/logical/machine1.nix new file mode 100644 index 0000000..d601066 --- /dev/null +++ b/install/logical/machine1.nix @@ -0,0 +1,142 @@ +{pkgs, lib, config, system, ...}: +let inherit (lib) types; + inherit (config.services) dovecot2; +in { +imports = [ + + machine1/nginx.nix + machine1/shorewall.nix + machine1/postfix.nix + machine1/postgrey.nix + machine1/dovecot.nix + machine1/rmilter.nix + machine1/nsd.nix +]; +options = { + enable = lib.mkEnableOption "machine1"; + fqdn = lib.mkOption { + type = types.str; + example = "example.coop"; + description = "Fully Qualified Domain Name of the machine."; + }; + networking.fqdn = lib.mkOption { + type = types.str; + example = "some.example.coop"; + default = "${config.networking.hostName}.${config.networking.domain}"; + description = "Fully Qualified Domain Name of the machine."; + }; + networking.net = lib.mkOption { + default = null; + type = types.nullOr (types.submodule { + options = { + iface = lib.mkOption { + type = types.str; + description = "Interface name."; + example = "eth0"; + }; + ipv4 = lib.mkOption { + type = types.str; + description = "Static IPv4 address of the machine."; + example = "1.2.3.4"; + }; + }; + }); + }; + networking.lan = lib.mkOption { + default = null; + type = types.nullOr (types.submodule { + options = { + iface = lib.mkOption { + type = types.str; + description = "Interface name."; + example = "eth0"; + }; + ipv4 = lib.mkOption { + type = types.str; + description = "Static IPv4 address of the machine."; + example = "192.168.1.1"; + }; + }; + }); + }; +}; +config = { + fqdn = "millogic.coop"; + networking = { + domain = "millogic.coop"; + }; + #fqdn = "machine1.logic.coop"; + + services = { + nixosManual = { + enable = false; # NOTE: useless on this machine, and CPU intensive. + }; + disnix = { + enable = false; + }; + openssh = { + enable = true; + }; + openldap = { + enable = true; + }; + dovecot2 = { + #debug = true; + }; + journald = { + extraConfig = '' + SystemMaxUse=50M + ''; + }; + x509 = { + domains = + builtins.attrNames dovecot2.domains; + }; + postfix.aliases = { + "root@${config.networking.domain}" = [ "test@${config.networking.domain}" ]; + "postmaster@${config.networking.domain}" = [ "test@${config.networking.domain}" ]; + "abuse@${config.networking.domain}" = [ "test@${config.networking.domain}" ]; + }; + dovecot2.domains = { + "${config.networking.domain}" = { + accounts = { + test = { + password = builtins.extraBuiltins.pass "${config.networking.domain}/dovecot2/test"; + # "${config.networking.domain}/dovecot2/test"; + # "{SSHA512}uyjL1KYx4z7HpfNvnKzuVxpMLD2KVueGGBvOcj7AF1EZCTVhT++IIKUVOC4xpZtWdqVD0OVmZqgYr2qpn/3t3Aj4oU0="; + aliases = ["test-alias@${config.networking.domain}"]; + quota = "512M"; + }; + }; + }; + }; + }; + environment = { + systemPackages = with pkgs; [ + htop + tree + vim + postgresql + dnsutils + tcpdump + #mysql + #procmail + postgrey + duplicity + pypolicyd-spf + unbound + nsd + dropbear + cryptsetup + openssl + gitolite + postgresql + cgit + openldap + #mail + sympa + multitail + ]; + }; +}; +} diff --git a/install/logical/machine1/dovecot.nix b/install/logical/machine1/dovecot.nix new file mode 100644 index 0000000..6708332 --- /dev/null +++ b/install/logical/machine1/dovecot.nix @@ -0,0 +1,548 @@ +{pkgs, lib, config, system, ...}: +let inherit (builtins) toString toFile attrNames; + inherit (lib) types; + inherit (config.services) dovecot2 postfix x509; + unlines = lib.concatStringsSep "\n"; + when = x: y: if x == null then "" else y; + extSep = postfix.recipientDelimiter; + dirSep = extSep; + libDir = "/var/lib/dovecot"; + mailDir = "${libDir}/mail"; + sieveDir = "${libDir}/sieve"; + authDir = "${libDir}/auth"; + authUser = dovecot2.mailUser; # TODO: php_roundcube + authGroup = dovecot2.mailGroup; # TODO: php_roundcube + escapeGroup = lib.stringAsChars (c: if "a"<=c && c<="z" + || "0"<=c && c<="9" + || c=="-" + then c else "_"); +in +{ +config = { + environment.etc."nginx/site.d/autoconfig.conf".source = + let servers = lib.concatMapStringsSep " " + (dom: "autoconfig.${dom}") + (attrNames dovecot2.domains); + autoconfigSite = pkgs.writeTextFile { + name = "autoconfig"; + destination = "/mail/config-v1.1.xml"; + text = '' + + + + + + %EMAILDOMAIN% + + imap.%EMAILDOMAIN% + 993 + SSL + %EMAILADDRESS% + password-cleartext + + + pop.%EMAILDOMAIN% + 995 + SSL + %EMAILADDRESS% + password-cleartext + + false + true + + + + smtp.%EMAILDOMAIN% + 465 + SSL + %EMAILADDRESS% + password-cleartext + + true + false + + + + + ''; + }; + in + pkgs.writeText "autoconfig.conf" '' + server { + listen 80; + server_name ${servers}; + root ${autoconfigSite}; + access_log off; + log_not_found off; + } + server { + listen 443 ssl http2; + ssl on; + server_name ${servers}; + root ${autoconfigSite}; + access_log off; + log_not_found off; + } + ''; + #services.postfix.mapFiles."transport-dovecot" = + # toFile "transport-dovecot" + # (unlines + # (lib.mapAttrsToList + # (dom: {...}: "${transportSubDomain}.${dom} lmtp:unix:private/dovecot-lmtp") + # dovecot2.domains)); + systemd.services.dovecot2.after = [ "postfix.service" ]; + #users.extraUsers = [ + # { name = "dovecot"; + # uid = config.ids.uids.dovecot2; + # description = "Dovecot user"; + # group = dovecot2.group; + # } + #]; + users.extraGroups = lib.mapAttrs + (domain: {...}: + { name = escapeGroup "${dovecot2.mailGroup}-${domain}"; + }) + dovecot2.domains; + systemd.services.dovecot2.preStart = + let sieveList = + pkgs.writeText "list.sieve" '' + require + [ "date" + , "fileinto" + , "mailbox" + , "variables" + ]; + + if currentdate :matches "year" "*" { set "year" "''${1}"; } + if currentdate :matches "month" "*" { set "month" "''${1}"; } + + if exists "List-ID" { + if header :matches "List-ID" "*<*.*.*.*>*" { + set "list" "''${2}"; + set "domain" "''${4}"; + } + elsif header :matches "List-ID" "*<*.*.*>*" { + set "list" "''${2}"; + set "domain" "''${3}"; + } + fileinto :create "Listes+''${domain}+''${list}+''${year}+''${month}"; + stop; + } + ''; + sieveSpam = + pkgs.writeText "spam.sieve" '' + require + [ "imap4flags" + ]; + + if header :contains "X-Spam-Level" "***" { + addflag "Junk"; + } + ''; + sieveExtension = + pkgs.writeText "extension.sieve" '' + require + [ "envelope" + , "fileinto" + , "mailbox" + , "subaddress" + , "variables" + ]; + if envelope :matches :detail "TO" "*" { + set "extension" "''${1}"; + } + if not string :is "''${extension}" "" { + fileinto :create "Plus+''${extension}"; + stop; + } + ''; + dovecot-virtual = + pkgs.writeText "dovecot-virtual" '' + all + all+* + all + ''; + in '' + # SEE: http://wiki2.dovecot.org/SharedMailboxes/Permissions + # The sticky bit is to allow the acl.db{.lock,} done by dovecot + install -D -d -m 2771 \ + -o ${dovecot2.mailUser} \ + -g ${dovecot2.mailGroup} \ + ${mailDir} + + # Install global sieves + install -D -d -m 0755 \ + -o root \ + -g root \ + ${sieveDir} \ + ${sieveDir}/after.d \ + ${sieveDir}/before.d \ + ${sieveDir}/global.d + ln -fns ${sieveList} ${sieveDir}/global.d/list.sieve + ln -fns ${sieveExtension} ${sieveDir}/global.d/extension.sieve + ln -fns ${sieveSpam} ${sieveDir}/global.d/spam.sieve + for f in ${sieveDir}/*/*.sieve + do ${pkgs.dovecot_pigeonhole}/bin/sievec $f + done + + # Install pop3 Inbox + install -D -m 0644 \ + -o root \ + -g root \ + ${dovecot-virtual} \ + ${libDir}/pop3/INBOX/dovecot-virtual + '' + + '' + # Install domains + new_uid=5000 + '' + + unlines (lib.mapAttrsToList (domain: {accounts, ...}: + let domainGroup = escapeGroup "${dovecot2.mailGroup}-${domain}"; in + '' + install -D -d -m 1770 \ + -o ${dovecot2.mailUser} \ + -g ${domainGroup} \ + ${mailDir}/${domain} \ + ${libDir}/control/${domain} \ + ${libDir}/index/${domain} + install -D -d -m 1770 \ + -o ${dovecot2.mailUser} \ + -g ${authGroup} \ + ${libDir}/auth \ + ${libDir}/auth/${domain} + dir_passwd=${libDir}/auth/${domain} + old_passwd=$dir_passwd/passwd + new_passwd=$(TMPDIR= mktemp --tmpdir=$dir_passwd -t passwd.XXXXXXXX.tmp) + + # Install users + '' + + unlines (lib.mapAttrsToList (user: acct: '' + home=${mailDir}/${domain}/${user} + gecos= + shell=/run/current-system/sw/bin/nologin + if test -e $home + then + uid=$(stat -c %u $home) + gid=$(stat -c %g $home) + fi + [ "''${uid:+set}" ] || { + while test exists = "$(find $(dirname $home) -mindepth 1 -maxdepth 1 -uid $new_uid -printf exists -quit)" + do new_uid=$((new_uid + 1)) + done + uid=$new_uid + gid=$new_uid + } + install -D -d -o $uid -g $gid -m 2770 $home $home/Maildir + install -d -o $uid -g $gid -m 0700 $home/sieve + '' + + unlines (lib.mapAttrsToList + (n: v: '' + install -D -m 640 -o $uid -g $gid \ + ${pkgs.writeText "${n}.sieve" v} \ + $home/sieve/${n}.sieve + ${pkgs.dovecot_pigeonhole}/bin/sievec \ + $home/sieve/${n}.sieve + '') + acct.sieves) + + '' + mail_access_groups=${lib.concatStringsSep "," ([domainGroup] ++ acct.groups)} + quota=${if lib.isString acct.quota + then ''"userdb_quota_rule=*:storage=${acct.quota}"'' + else ""} + extra_fields="userdb_uid=$uid userdb_gid=$gid userdb_mail_access_groups=$mail_access_groups $quota" + #test ! -e $old_passwd || { + # # Preserve password changed by another mechanism, eg. roundcube. + # # But this also does not overwrite any old password set by this config. + # pass="$(sed -ne "s/^${user}:\([^:]*\):.*/\1/p" $old_passwd)" + #} + [ "''${pass:+set}" ] || { + pass=${lib.escapeShellArg acct.password} + } + printf '%s\n' >>$new_passwd \ + "${user}:$pass:$uid:$gid:$gecos:$home:$shell:$extra_fields" + '') accounts) + + '' + install -o ${authUser} -g ${authGroup} -m 0640 $new_passwd $old_passwd + rm $new_passwd + '' + ) dovecot2.domains); + services.dovecot2 = { + enable = true; + mailUser = "dovemail"; + mailGroup = "dovemail"; + modules = [ + #pkgs.dovecot_antispam + pkgs.dovecot_pigeonhole + ]; + # ${lib.concatMapStringsSep "\n" + # (dom: '' + # local_name imap.${dom} { + # #ssl_ca = <''${caPath} + # ssl_cert = <${x509.cert dom} + # ssl_key = <${x509.key dom} + # } + # local_name pop.${dom} { + # #ssl_ca = <''${caPath} + # ssl_cert = <${x509.cert dom} + # ssl_key = <${x509.key dom} + # } + # '') + # dovecot2.domains + # } + + configFile = toString (pkgs.writeText "dovecot.conf" '' + passdb { + driver = passwd-file + args = scheme=crypt username_format=%n ${authDir}/%d/passwd + } + userdb { + driver = prefetch + } + userdb { + # NOTE: this userdb is only used by lda. + driver = passwd-file + args = username_format=%n ${authDir}/%d/passwd + #default_fields = home=${mailDir}/%d/%n + } + mail_home = ${mailDir}/%d/%n + auth_mechanisms = plain login + # postfix does not supply a client cert. + auth_ssl_require_client_cert = no + auth_ssl_username_from_cert = yes + auth_verbose = yes + ${lib.optionalString dovecot2.debug '' + auth_debug = yes + mail_debug = yes + verbose_ssl = yes + ''} + default_internal_user = ${dovecot2.user} + default_internal_group = ${dovecot2.group} + disable_plaintext_auth = yes + first_valid_uid = 1000 + lda_mailbox_autocreate = yes + lda_mailbox_autosubscribe = yes + listen = * + log_timestamp = "%Y-%m-%d %H:%M:%S " + # NOTE: INDEX and CONTROL are on a partition without quota, as explain in the doc. + # SEE: http://wiki2.dovecot.org/Quota/FS + mail_location = maildir:${mailDir}/%d/%n/Maildir:LAYOUT=fs:INDEX=${libDir}/index/%d/%n:CONTROL=${libDir}/control/%d/%n + namespace inbox { + # NOTE: here because protocol sieve {namespace inbox{}} does not seem to work. + inbox = yes + location = + list = yes + prefix = + separator = ${dirSep} + } + namespace { + #list = children + list = yes + location = maildir:${mailDir}/%%d/%%n/Maildir:LAYOUT=fs:INDEX=${libDir}/index/%d/%n/Shared/%%n:CONTROL=${libDir}/control/%d/%n/Shared/%%n + prefix = Partages+%%n+ + separator = ${dirSep} + subscriptions = yes + type = shared + } + mail_plugins = $mail_plugins acl quota virtual + #mail_uid = ${dovecot2.mailUser} + #mail_gid = ${dovecot2.mailGroup} + #mail_privileged_group = mail + #mail_access_groups = + plugin { + acl = vfile:/etc/dovecot/acl/global.d + acl_anyone = allow + acl_shared_dict = file:${mailDir}/%d/acl.db + ##antispam_allow_append_to_spam = yes + # # NOTE: pour offlineimap + #antispam_backend = pipe + ##antispam_crm_args = -u;${mailDir}/%d/.crm114;/usr/share/crm114/mailfilter.crm + #antispam_crm_args = -u;${mailDir}/crm114;/usr/share/crm114/mailfilter.crm + #antispam_crm_binary = /usr/bin/crm + #antispam_debug_target = syslog + ##antispam_crm_env = HOME=%h;USER=%u + #antispam_ham_keywords = NonJunk + #antispam_pipe_program = /usr/bin/crm + #antispam_pipe_program_args = -u;${mailDir}/crm114;/usr/share/crm114/mailfilter.crm;--stats_only;--force + #antispam_pipe_program_notspam_arg = --learnnonspam + #antispam_pipe_program_spam_arg = --learnspam + #antispam_pipe_program_unlearn_spam_args = --unlearn;--learnspam + #antispam_pipe_program_unlearn_notspam_args = --unlearn;--learnnonspam + #antispam_pipe_tmpdir = ${mailDir}/crm114/tmp + #antispam_signature = X-CRM114-CacheID + #antispam_signature_missing = move + #antispam_spam = Junk + #antispam_spam_keywords = Junk + #antispam_trash = Trash + #antispam_unsure = Unsure + #antispam_verbose_debug = 0 + quota = maildir:User quota + quota_rule = *:storage=256M + quota_rule2 = Trash:storage=+64M + recipient_delimiter = ${extSep} + sieve = file:${mailDir}/%d/%n/sieve;active=${mailDir}/%d/%n/sieve/main.sieve + #sieve_default = file:${mailDir}/%u/default.sieve + #sieve_default_name = default + sieve_after = ${sieveDir}/after.d/ + sieve_before = ${sieveDir}/before.d/ + sieve_dir = ${mailDir}/%d/%n/sieve/ + #sieve_extensions = +spamtest +spamtestplus + sieve_global_dir = ${sieveDir}/global.d/ + sieve_max_script_size = 1M + sieve_quota_max_scripts = 0 + sieve_quota_max_storage = 10M + sieve_spamtest_max_value = 10 + sieve_spamtest_status_header = X-Spam-Score + sieve_spamtest_status_type = strlen + sieve_user_log = /var/log/dovecot/%d/sieve.%n.log + } + protocol imap { + #mail_max_userip_connections = 10 + mail_plugins = $mail_plugins imap_acl imap_quota # antispam + namespace inbox { + inbox = yes + location = + list = yes + mailbox Drafts { + special_use = \Drafts + } + mailbox Junk { + special_use = \Junk + } + mailbox Sent { + special_use = \Sent + } + mailbox "Sent Messages" { + special_use = \Sent + } + mailbox Trash { + special_use = \Trash + } + prefix = + separator = ${dirSep} + } + } + protocol lda { + auth_socket_path = /var/run/dovecot/auth-userdb + hostname = ${config.networking.domain} + info_log_path = + log_path = + mail_plugins = $mail_plugins sieve + namespace inbox { + inbox = yes + location = + list = yes + prefix = + separator = ${dirSep} + } + postmaster_address = postmaster${extSep}dovecot${extSep}lda@${config.networking.domain} + syslog_facility = mail + } + protocol lmtp { + #info_log_path = /tmp/dovecot-lmtp.log + mail_plugins = $mail_plugins sieve + namespace inbox { + inbox = yes + location = + list = yes + prefix = + separator = ${dirSep} + } + postmaster_address = postmaster${extSep}dovecot${extSep}lmtp@${config.networking.domain} + } + protocol pop3 { + #mail_max_userip_connections = 10 + # Used by ${libDir}/pop3/INBOX/dovecot-virtual + namespace all { + hidden = yes + list = no + location = + prefix = all+ + separator = ${dirSep} + } + # Virtual namespace for the virtual INBOX. + # Use a global directory for dovecot-virtual files. + namespace inbox { + inbox = yes + hidden = yes + list = no + location = virtual:${libDir}/pop3:INDEX=${libDir}/index/%d/%n/POP3:LAYOUT=fs + prefix = pop3+ + separator = ${dirSep} + } + pop3_client_workarounds = + pop3_fast_size_lookups = yes + pop3_lock_session = yes + pop3_no_flag_updates = yes + # Use GUIDs to avoid accidental POP3 UIDL changes instead of IMAP UIDs. + pop3_uidl_format = %g + } + protocol sieve { + #mail_max_userip_connections = 10 + #managesieve_implementation_string = Dovecot Pigeonhole + managesieve_max_compile_errors = 5 + #managesieve_max_line_length = 65536 + #managesieve_notify_capability = mailto + #managesieve_sieve_capability = fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date ihave + } + protocols = imap lmtp pop3 sieve + service lmtp { + #executable = lmtp -L + process_min_avail = 2 + unix_listener /var/lib/postfix/queue/private/dovecot-lmtp { + user = ${postfix.user} + group = ${postfix.group} + mode = 0600 + } + #user = mail + } + service auth { + user = root + unix_listener auth-userdb { + user = ${dovecot2.user} + group = ${dovecot2.group} + mode = 0660 + } + unix_listener /var/lib/postfix/queue/private/auth { + user = ${postfix.user} + group = ${postfix.group} + mode = 0660 + } + } + service imap { + # Most of the memory goes to mmap()ing files. + # You may need to increase this limit if you have huge mailboxes. + #vsz_limit = + process_limit = 1024 + } + service imap-login { + #inet_listener imap { + # address = 127.0.0.1 + # port = 143 + # ssl = no + # } + inet_listener imaps { + port = 993 + ssl = yes + } + } + service pop3 { + process_limit = 1024 + } + service pop3-login { + inet_listener pop3s { + port = 995 + ssl = yes + } + } + ssl = required + #ssl_ca = <''${caPath} + ssl_cert = <${x509.cert} + # Only with dovecot >= 2.3 + #ssl_dh = <${x509.dir}/dh.pem + ssl_cipher_list = ALL:!LOW:!SSLv2:!EXP:!aNULL + ssl_key = <${x509.key} + #ssl_verify_client_cert = yes + ''); + }; +}; +} diff --git a/install/logical/machine1/nginx.nix b/install/logical/machine1/nginx.nix new file mode 100644 index 0000000..118babc --- /dev/null +++ b/install/logical/machine1/nginx.nix @@ -0,0 +1,219 @@ +{pkgs, lib, config, system, ...}: +let inherit (lib) types; + inherit (config.services) nginx x509; + tempDir = "/dev/shm/nginx"; + logDir = "/var/log/nginx"; + domainDir = dom: lib.concatStringsSep "/" (lib.reverseList (lib.splitString "." dom)); + #customPkgs = import ../../pkgs.nix { inherit pkgs lib config system; }; +in +{ +imports = [ +]; +options.services.nginx.webDir = lib.mkOption { + type = types.str; + default = "/var/www"; # TODO: /var/lib/nginx ? +}; +config = { + systemd.services.nginx-init = { + # NOTE: This service workarounds nixpkgs shortcoming, + # ideally this script should be prepended to nginx.service's preStart + # but since it is a types.lines I would only be able to append to it, + # which would put it after nginx's configuration check instead of before. + description = "Initialize nginx"; + before = [ "nginx.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig.Type = "oneshot"; + script = + '' + install -D -d -m 1700 \ + -o ${nginx.user} \ + -g ${nginx.group} \ + ${nginx.stateDir} \ + ${nginx.stateDir}/fastcgi_cache \ + ${tempDir}/fastcgi_temp \ + ${tempDir}/client_body_temp \ + ${tempDir}/proxy_temp \ + ${tempDir}/scgi_temp \ + ${tempDir}/uwsgi_temp \ + ${logDir} \ + ${nginx.webDir} + ''; + }; + security.dhparams = { + enable = true; + params = { + nginx = 1024; + }; + }; + services.nginx = { + enable = true; + config = '' + worker_processes 2; + pid /run/nginx.pid; + events { + multi_accept on; + use epoll; + worker_connections 1024; + } + http { + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + include ${nginx.package}/conf/mime.types; + access_log ${logDir}/access.log main buffer=32k; + # % getconf PAGESIZE + # 4096 + client_body_buffer_size 4K; + client_body_temp_path ${tempDir}/client_body_temp 1 2; + client_body_timeout 60; + client_header_buffer_size 1k; + client_header_timeout 60; + client_max_body_size 20m; + default_type application/octet-stream; + error_log ${logDir}/error.log warn; + #error_log stderr; + error_page 403 = 404; + # DOC: http://wiki.nginx.org/HttpFastcgiModule + fastcgi_buffer_size 128k; + fastcgi_buffers 256 4k; + fastcgi_busy_buffers_size 256k; + fastcgi_cache_key "$request_method $scheme://$http_host$request_uri"; + fastcgi_cache_path ${nginx.stateDir}/fastcgi_cache + inactive=10m + keys_zone=microcache:2M + levels=1:2 + loader_files=100000 + loader_sleep=1 + loader_threshold=2592000000 + max_size=64M; + fastcgi_connect_timeout 60; + fastcgi_ignore_client_abort off; + fastcgi_intercept_errors on; + fastcgi_max_temp_file_size 2M; + fastcgi_param CONTENT_LENGTH $content_length; + fastcgi_param CONTENT_TYPE $content_type; + fastcgi_param DOCUMENT_ROOT $document_root; + fastcgi_param DOCUMENT_URI $document_uri; + fastcgi_param GATEWAY_INTERFACE CGI/1.1; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_param QUERY_STRING $query_string; + # PHP only, required if PHP was built with --enable-force-cgi-redirect + fastcgi_param REDIRECT_STATUS 200; + fastcgi_param REMOTE_ADDR $remote_addr; + fastcgi_param REMOTE_PORT $remote_port; + fastcgi_param REQUEST_METHOD $request_method; + fastcgi_param REQUEST_SCHEME $scheme; + fastcgi_param REQUEST_URI $request_uri; + #fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param SCRIPT_FILENAME $request_filename; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param SERVER_ADDR $server_addr; + fastcgi_param SERVER_NAME $server_name; + fastcgi_param SERVER_PORT $server_port; + fastcgi_param SERVER_PROTOCOL $server_protocol; + fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; + fastcgi_temp_path ${tempDir}/fastcgi_temp 1 2; + gzip on; + gzip_buffers 16 8k; + gzip_comp_level 6; + gzip_disable "MSIE [1-6]\."; + gzip_http_version 1.1; + gzip_min_length 1024; + gzip_proxied any; + gzip_static on; + gzip_vary on; + gzip_types application/javascript + application/json + application/rss+xml + application/vnd.ms-fontobject + application/x-font-ttf + application/x-javascript + application/xml + application/xml+rss + font/opentype + font/truetype + image/svg+xml + text/css + text/javascript + text/plain + text/x-component + text/xml; + keepalive_timeout 20; + large_client_header_buffers 4 8k; + open_file_cache max=200000 inactive=20s; + open_file_cache_errors on; + open_file_cache_min_uses 2; + open_file_cache_valid 30s; + open_log_file_cache max=1000 inactive=20s min_uses=2 valid=1m; + proxy_cache_use_stale updating; + proxy_temp_path ${tempDir}/proxy_temp 1 2; + reset_timedout_connection on; + root ${nginx.webDir}; + # If the client stops reading data, + # free up the stale client connection after this much time. + send_timeout 60; + sendfile on; + server_names_hash_bucket_size 128; + server_tokens off; + ssl_certificate ${x509.cert}; + ssl_certificate_key ${x509.key}; + ssl_ciphers HIGH:!ADH:!MD5; + #ssl_ciphers EECDH+aRSA+AESGCM:EDH+aRSA:EECDH+aRSA:+AES256:+AES128:+SHA1:!CAMELLIA:!SEED:!3DES:!DES:!RC4:!eNULL; + ssl_prefer_server_ciphers on; + ssl_protocols TLSv1.2; + ssl_session_cache shared:SSL:10m; + # Don't buffer data-sends (disable Nagle algorithm). + # Good for sending frequent small bursts of data in real time. + tcp_nodelay on; + # Causes nginx to attempt to send its HTTP response head in one packet, + # instead of using partial frames. + # This is useful for prepending headers before calling sendfile, + # or for throughput optimization. + tcp_nopush on; + types_hash_max_size 2048; + uwsgi_param CONTENT_LENGTH $content_length; + uwsgi_param CONTENT_TYPE $content_type; + uwsgi_param DOCUMENT_ROOT $document_root; + uwsgi_param HTTPS $https if_not_empty; + uwsgi_param PATH_INFO $document_uri; + uwsgi_param QUERY_STRING $query_string; + uwsgi_param REMOTE_ADDR $remote_addr; + uwsgi_param REMOTE_PORT $remote_port; + uwsgi_param REQUEST_METHOD $request_method; + uwsgi_param REQUEST_SCHEME $scheme; + uwsgi_param REQUEST_URI $request_uri; + uwsgi_param SERVER_NAME $server_name; + uwsgi_param SERVER_PORT $server_port; + uwsgi_param SERVER_PROTOCOL $server_protocol; + # $connection_upgrade is used for websocket proxying + map $http_upgrade $connection_upgrade { + default upgrade; + ''' close; + } + # User agents that are to be blocked. + #map $http_user_agent $bad_bot { + # default 0; + # libwww-perl 1; + # ~(?i)(httrack|htmlparser|libwww) 1; + #} + # Referrers that are to be blocked. + #map $http_referer $bad_referer { + # default 0; + # ~(?i)(babes|casino|click|diamond|forsale|girl|jewelry|love|nudit|organic|poker|porn|poweroversoftware|replica|sex|teen|webcam|zippo) 1; + #} + #geo $not_local { + # default 1; + # 127.0.0.1 0; + #} + include /etc/nginx/site.d/*.conf; + server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + return 301 https://$host$request_uri; + } + } + ''; + }; +}; +} diff --git a/install/logical/machine1/nsd.nix b/install/logical/machine1/nsd.nix new file mode 100644 index 0000000..986109c --- /dev/null +++ b/install/logical/machine1/nsd.nix @@ -0,0 +1,24 @@ +{pkgs, lib, config, ...}: +let inherit (config.services) nsd; +in +{ + config = { + services.nsd = { + enable = true; + ipv4 = true; + ipv6 = true; + verbosity = 5; + # SEE: http://www.nlnetlabs.nl/blog/2012/10/11/nsd-ratelimit/ + ratelimit.size = 10000; + extraConfig = '' + ''; + interfaces = lib.unique [ + "127.0.0.1" + "::1" + config.networking.lan.ipv4 + config.networking.net.ipv4 + ]; + zones = import nsd/millogic.coop.nix { inherit pkgs lib config; }; + }; + }; +} diff --git a/install/logical/machine1/nsd/millogic.coop.nix b/install/logical/machine1/nsd/millogic.coop.nix new file mode 100644 index 0000000..5446e63 --- /dev/null +++ b/install/logical/machine1/nsd/millogic.coop.nix @@ -0,0 +1,61 @@ +{pkgs, lib, config, ...}: +let inherit (builtins) toString toPath readFile; + inherit (config.services) nsd; + serial = zone: toString (builtins.extraBuiltins.git ./. [ "log" "-1" "--format=%ct" "--" (zone + ".nix") ]); + /* + serial = file: lib.removeSuffix "\n" (readFile + (pkgs.runCommand "zone-serial" + { buildInputs = [ pkgs.git ]; + buildDepends = [ (toPath ./. + file) ]; + preferLocalBuild = true; + allowSubstitutes = false; + } '' + cd ${toPath ./.} + ${pkgs.git}/bin/git log -1 --format="%ct" -- ${file} >$out + '')); + */ + ipv4 = config.networking.net.ipv4; +in +{ +"${config.networking.domain}" = { + data = '' + $ORIGIN ${config.networking.domain}. + $TTL 86400 + + ; SOA (Start Of Authority) + @ SOA ns admin ( + ${serial config.networking.domain} ; Serial number + 1d ; Refresh + 15m ; Retry + 2592000 ; Expire + 1d ; TTL (Time To Live) minimum + ) + + ; A (DNS -> IPv4) + @ A ${ipv4} + autoconfig A ${ipv4} + git A ${ipv4} + imap A ${ipv4} + mail A ${ipv4} + ns A ${ipv4} + pop A ${ipv4} + smtp A ${ipv4} + submission A ${ipv4} + www A ${ipv4} + + ; SPF (Sender Policy Framework) + @ 3600 IN SPF "v=spf1 mx ip4:${ipv4} -all" + @ 3600 IN TXT "v=spf1 mx ip4:${ipv4} -all" + + ; NS (Name Server) + @ NS ns + ;@ NS ns6.gandi.net. + + ; MX (Mail eXchange) + @ 180 MX 5 ${ipv4} + + ; SRV (SeRVice) + _git._tcp.git 18000 IN SRV 0 0 9418 git + ''; + }; +} diff --git a/install/logical/machine1/postfix.nix b/install/logical/machine1/postfix.nix new file mode 100644 index 0000000..41aefab --- /dev/null +++ b/install/logical/machine1/postfix.nix @@ -0,0 +1,437 @@ +{pkgs, lib, config, nodes, ...}: +let inherit (builtins) attrNames toFile; + inherit (lib) types; + inherit (config.services) x509 postfix dovecot2 postgrey; + unlines = lib.concatStringsSep "\n"; + unwords = lib.concatStringsSep " "; + when = x: y: if x == null then "" else y; + + submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" '' + # Removes sensitive headers from mails handed in via the submission or smtps port. + # See https://thomas-leister.de/mailserver-debian-stretch/ + # Uses "pcre" style regex. + + /^Received:/ IGNORE + /^User-Agent:/ IGNORE + /^X-Enigmail:/ IGNORE + /^X-Mailer:/ IGNORE + /^X-Originating-IP:/ IGNORE + ''; +in +{ +options.services.postfix.aliases = lib.mkOption { + type = with types; attrsOf (listOf str); + default = {}; + example = { "root@${config.networking.domain}" = [ + "user1@${config.networking.domain}" + "user2@${config.networking.domain}" + ]; + "@example.coop" = ["user1@${config.networking.domain}"]; + }; +}; +config = { + systemd.services.postfix.after = + if x509.scheme == "letsencrypt" + then [ "nginx.service" ] + else []; + services.postfix = { + enable = true; + hostname = "${config.networking.domain}"; + networksStyle = "host"; + #mapFiles."valias" = toFile "valias" (unlines (all_valiases_postfix ++ catchAllPostfix)); + # See https://blog.grimneko.de/2011/12/24/a-bunch-of-tips-for-improving-your-postfix-setup/ + # for details on how this file looks. By using the same file as valias, + # every alias is uniquely owned by its user. + # The user's own address is already in all_valiases_postfix. + #mapFiles."vaccounts" = toFile "vaccounts" (unlines all_valiases_postfix); + mapFiles."virtual_alias_maps" = + toFile "virtual_alias_maps" + (unlines + (lib.mapAttrsToList + (dom: {accounts, ...}: + unlines + (lib.mapAttrsToList + (user: acct: + unlines + (map (from: "${from} ${user}@${dom}") + acct.aliases)) + accounts)) + dovecot2.domains) + + unlines (lib.mapAttrsToList + (from: to: "${from} ${unwords to}") + postfix.aliases)); + sslCert = x509.cert; + sslKey = x509.key; + #enableSubmission = true; + #enableSmtp = true; + destination = [ "localhost" ]; + networks = [ "127.0.0.0/8" "[::1]/128" ]; + recipientDelimiter = "+"; + config = { + # Appending .domain is the MUA's job + append_dot_mydomain = false; + # No console bell on new mail + biff = false; + body_checks = ""; + #content_filter = "amavisfeed:[127.0.0.1]:10024"; + #debug_peer_level = 4; + #debug_peer_list = ".$myhostname"; + default_extra_recipient_limit = "5000"; + # Uncomment the next line to generate "delayed mail" warnings + #delay_warning_time = "4h"; + # Stops some techniques used to harvest email addresses + disable_vrfy_command = true; + duplicate_filter_limit = "5000"; + enable_long_queue_ids = false; + # Pass unexisting $mydestination recipients to dovecot + fallback_transport = "lmtp:unix:private/dovecot-lmtp"; + forward_path = [ + ''$home/.forward''${recipient_delimiter}''${extension}'' + "$home/.forward" + ]; + #header_checks = "regexp:/var/lib/postfix/conf/header_checks"; + #inet_interfaces = "all"; + line_length_limit = "2048"; + # Let $fallback_transport check existence of recipients + local_recipient_maps = ""; + #local_header_rewrite_clients = ""; + mailbox_command = '' + ${pkgs.procmail}/bin/procmail -t -a "$SENDER" -a "$RECIPIENT" -a "$USER" -a "$EXTENSION" -a "$DOMAIN" -a "$ORIGINAL_RECIPIENT" "$HOME/.procmailrc" + ''; + mailbox_size_limit = "0"; + masquerade_classes = [ "envelope_sender" "header_sender" "header_recipient" ]; + masquerade_domains = ""; + masquerade_exceptions = "root"; + maximal_queue_lifetime = "5d"; + message_size_limit = "20480000"; + mime_header_checks = ""; + milter_header_checks = ""; + nested_header_checks = ""; + non_smtpd_milters = ""; + parent_domain_matches_subdomains = [ + #"debug_peer_list" + #"fast_flush_domains" + #"mynetworks" + #"permit_mx_backup_networks" + #"qmqpd_authorized_clients" + #"smtpd_access_maps" + ]; + permit_mx_backup_networks = ""; + #policy-spf_time_limit = "3600s"; + propagate_unmatched_extensions = [ "canonical" "virtual" "alias" ]; + queue_minfree = "0"; + #receive_override_options = "no_address_mappings"; + # no_unknown_recipient_checks + # Do not try to reject unknown recipients (SMTP server only). + # This is typically specified AFTER an external content filter. + # no_address_mappings + # Disable canonical address mapping, virtual alias map expansion, + # address masquerading, and automatic BCC (blind carbon-copy) recipients. + # This is typically specified BEFORE an external content filter (eg. amavis). + # no_header_body_checks + # Disable header/body_checks. This is typically specified AFTER + # an external content filter. + # no_milters + # Disable Milter (mail filter) applications. + # This is typically specified AFTER an external content filter. + # Parse the extension in email address, eg. contact+extension@ + relayhost = ""; + #relay_clientcerts = hash:/var/lib/postfix/conf/relay_clientcerts + # This is where to put backup MX domains + relay_domains = "$mydestination"; + relay_recipient_maps = ""; + smtp_body_checks = ""; + #smtp_cname_overrides_servername = false; + smtp_connect_timeout = "60s"; + #smtp_header_checks = "regexp:/var/lib/postfix/smtp_header_checks"; + smtp_mime_header_checks = ""; + smtp_nested_header_checks = ""; + smtp_tls_exclude_ciphers = [ "RC4" "aNULL" ]; + #smtp_tls_fingerprint_digest = "sha1"; + smtp_tls_loglevel = "1"; + #smtp_tls_note_starttls_offer = true; + #smtp_tls_policy_maps = "hash:/var/lib/postfix/conf/tls_policy"; + # Only allow TLSv* protocols + smtp_tls_protocols = [ "!SSLv2" "!SSLv3" ]; + smtp_tls_scert_verifydepth = "5"; + #smtp_tls_secure_cert_match = [ "nexthop" "dot-nexthop" ]; + smtp_tls_security_level = "may"; + smtp_tls_session_cache_database = "btree:$data_directory/smtp_tls_session_cache"; + #smtp_tls_session_cache_timeout = "3600s"; + #smtp_tls_verify_cert_match = "hostname"; + # Useful to test restrictions + smtpd_authorized_xclient_hosts = "127.0.0.1"; + smtpd_banner = "$myhostname ESMTP $mail_name (NixOS)"; + smtpd_client_connection_count_limit = "50"; + smtpd_client_connection_rate_limit = "0"; + smtpd_client_event_limit_exceptions = "$mynetworks"; + smtpd_client_message_rate_limit = "0"; + smtpd_client_new_tls_session_rate_limit = "0"; + smtpd_client_port_logging = false; + smtpd_client_recipient_rate_limit = "0"; + smtpd_client_restrictions = [ + #"check_client_access hash:/var/lib/postfix/conf/client_blacklist" + ]; + smtpd_data_restrictions = [ + "reject_unauth_pipelining" + # Force the smtp client to wait OK before sending + "permit" + ]; + # Disable opportunistic encryption + smtpd_discard_ehlo_keywords = "starttls"; + #smtpd_end_of_data_restrictions = ""; + # Ban 5 sec on error + smtpd_error_sleep_time = "5"; + smtpd_helo_required = true; + smtpd_helo_restrictions = [ + "reject_invalid_helo_hostname" + "reject_non_fqdn_helo_hostname" + #"reject_unknown_helo_hostname" + # May be useful to fight spam + "permit" + ]; + #smtpd_milters = ""; + # Needed by postgrey + smtpd_peername_lookup = true; + smtpd_recipient_limit = "5000"; + smtpd_recipient_overshoot_limit = "5000"; + smtpd_recipient_restrictions = [ + "reject_non_fqdn_recipient" + #"reject_invalid_hostname" + "reject_unknown_recipient_domain" + #"reject_non_fqdn_sender" + "reject_unauth_pipelining" + #"check_policy_service inet:localhost:12340" + # check quota + "permit_mynetworks" + "permit_tls_clientcerts" + "permit_sasl_authenticated" + "reject_unverified_recipient" + # $fallback_transport is responsible of checking the existence of the recipient + # WARNING: verify(8) has a cache, dumpable if verify(8) is stopped, with: + # postmap -s btree:/var/lib/postfix/data/verify_cache + # Bypass SPF check and postgrey if the recipient is not for us or someone in backup_mx + "reject_unauth_destination" + # Check SPF + #"check_policy_service unix:private/spfcheck" + # Greylisting using postgrey + "check_policy_service unix:${postgrey.socket.path}" + # Once postgrey passed, permit what is for us + "permit_auth_destination" + "reject" + #"reject_unknown_sender_domain" + # Maybe better in smtpd_sender_restrictions + #"reject_rbl_client bl.spamcop.net" + #"reject_rbl_client list.dsbl.org" + #"reject_rbl_client zen.spamhaus.org" + #"reject_rbl_client dnsbl.sorbs.net" + ]; + smtpd_relay_restrictions = [ + "permit_mynetworks" + "permit_sasl_authenticated" + "reject_unauth_destination" + ]; + #smtpd_restriction_classes = ""; + #smtpd_sasl_auth_enable = true; + #smtpd_sasl_path = "private/auth"; + #smtpd_sasl_security_options = "noanonymous"; + #smtpd_sasl_type = "dovecot"; + smtpd_sender_restrictions = [ + "permit_mynetworks" + "permit_tls_clientcerts" + "permit_sasl_authenticated" + #"check_sender_access hash:/var/lib/postfix/conf/sender_access" + "reject_unauth_pipelining" + "reject_non_fqdn_sender" + #"reject_sender_login_mismatch" + #"reject_unknown_sender_domain" + "permit" + ]; + smtpd_starttls_timeout = "300s"; + #smtpd_tls_always_issue_session_ids = true; + # No SASL AUTH without TLS + smtpd_tls_auth_only = true; + #smtpd_tls_CApath = "/etc/postfix/x509/ca/"; + smtpd_tls_ask_ccert = false; + #smtpd_tls_ccert_verifydepth = "5"; + smtpd_tls_ciphers = "high"; + smtpd_tls_eecdh_grade = "ultra"; + # Disable weak ciphers as reported by https://ssl-tools.net + # https://serverfault.com/questions/744168/how-to-disable-rc4-on-postfix + smtpd_tls_exclude_ciphers = [ "RC4" "aNULL" ]; + smtpd_tls_fingerprint_digest = "sha512"; + # Log only a summary message on TLS handshake completion + smtpd_tls_loglevel = "1"; + smtpd_tls_mandatory_ciphers = "high"; + smtpd_tls_mandatory_protocols = "TLSv1"; + # Only allow TLSv* + smtpd_tls_protocols = [ "!SSLv2" "!SSLv3" ]; + #smtpd_tls_received_header = false; + smtpd_tls_req_ccert = false; + # Postfix 2.3 and later + # encrypt + # Mandatory TLS encryption: announce STARTTLS support to SMTP clients, and require that clients use TLS + # encryption. According to [1720]RFC 2487 this MUST NOT be applied in case of a publicly-referenced + # SMTP server. Instead, this option should be used only on dedicated servers. + smtpd_tls_security_level = "may"; + smtpd_tls_session_cache_database = "btree:$data_directory/smtpd_tls_session_cache"; + #smtpd_tls_session_cache_timeout = "3600s"; + # Stops mail from poorly written software + strict_rfc821_envelopes = true; + #sympa_destination_recipient_limit = "1"; + #sympabounce_destination_recipient_limit = "1"; + # postconf(5) discourages to change this + #tls_high_cipherlist = "AES256-SHA"; + #tls_random_bytes = "32"; + # Must not be in a chroot + #tls_random_exchange_name = "$data_directory/prng_exch"; + #tls_random_prng_update_period = "3600s"; + #tls_random_reseed_period = "3600s"; + # Use a non blocking source of randomness + tls_random_source = "dev:/dev/urandom"; + transport_maps = [ + #"hash:/etc/postfix/transport-dovecot" + #"hash:/etc/postfix/$mydomain/transport" + #"hash:/etc/dovecot/transport" + #"regexp:/etc/sympa/transport" + ]; + # Rejects immediately what $fallback_transport rejects + unverified_recipient_reject_code = "550"; + # Do not specify virtual alias domain names in mydestination + # or relay_domains configuration parameters + # + # With a virtual alias domain, the Postfix SMTP server + # accepts mail for known-user@virtual-alias.domain, and + # rejects mail for unknown-user@virtual-alias.domain as + # undeliverable. + virtual_alias_domains = []; + virtual_alias_maps = [ + "hash:/etc/postfix/virtual_alias_maps" + #"hash:/etc/postfix/virtual_alias-dovecot" + #"hash:/var/lib/postfix/conf/valias" + #"regexp:/etc/sympa/virtual_alias" + ]; + #virtual_uid_maps = "static:5000"; + #virtual_gid_maps = "static:5000"; + #virtual_mailbox_base = dovecot2.mailDir; + virtual_mailbox_domains = toFile "virtual_mailbox_domains" (unlines (attrNames dovecot2.domains)); + #virtual_mailbox_maps = "hash:/etc/postfix/virtual_mailbox_maps"; + virtual_transport = "lmtp:unix:private/dovecot-lmtp"; + }; + #submissionOptions = { + # smtpd_tls_security_level = "encrypt"; + # smtpd_sasl_auth_enable = "yes"; + # smtpd_sasl_type = "dovecot"; + # smtpd_sasl_path = "private/auth"; + # smtpd_sasl_security_options = "noanonymous"; + # smtpd_sasl_local_domain = "$myhostname"; + # smtpd_client_restrictions = "permit_sasl_authenticated,reject"; + # smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts"; + # smtpd_sender_restrictions = "reject_sender_login_mismatch"; + # smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject"; + # cleanup_service_name = "submission-header-cleanup"; + #}; + extraMasterConf = '' + spfcheck unix - n n - 0 spawn + user=policyd-spf argv=/usr/sbin/postfix-policyd-spf-perl + 465 inet n - - - - smtpd + -o milter_macro_daemon_name=ORIGINATING + -o smtpd_client_restrictions=permit_sasl_authenticated,reject + -o smtpd_sasl_auth_enable=yes + -o smtpd_tls_ask_ccert=no + -o smtpd_tls_auth_only=yes + -o smtpd_tls_ccert_verifydepth=0 + -o smtpd_tls_loglevel=1 + -o smtpd_tls_req_ccert=no + -o smtpd_tls_security_level=encrypt + -o smtpd_tls_wrappermode=yes + -o smtpd_recipient_restrictions=reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject + -o smtpd_sasl_local_domain=$myhostname + -o smtpd_sasl_security_options=noanonymous + -o smtpd_sasl_type=dovecot + -o smtpd_sasl_path=private/auth + # -o smtpd_sender_restrictions=reject_sender_login_mismatch + # -o smtpd_sender_login_maps=hash:/etc/postfix/vaccounts + # -o cleanup_service_name=submission-header-cleanup + submission-header-cleanup unix n - n - 0 cleanup + -o header_checks=pcre:${submissionHeaderCleanupRules} + #spfcheck unix - n n - 0 spawn + # user=policyd-spf argv=/usr/bin/postfix-policyd-spf-perl + #uucp unix - n n - - pipe + # flags=Fqhu user=uucp argv=uux -r -n -z -a$sender - $nexthop!rmail ($recipient) + #smtp inet n - - - - smtpd + # -o cleanup_service_name=pre-cleanup + # -o content_filter=amavis:[127.0.0.1]:10024 + # -o smtpd_sender_restrictions=reject_unauth_pipelining,reject_non_fqdn_sender,permit + # -o receive_override_options=no_address_mappings + #amavis unix - - n - 2 lmtp + # -o lmtp_data_done_timeout=1200 + # -o lmtp_send_xforward_command=yes + # -o lmtp_tls_note_starttls_offer=no + #127.0.0.1:10025 inet n - n - - smtpd + # -o content_filter= + # -o local_header_rewrite_clients= + # -o local_recipient_maps= + # -o mynetworks=127.0.0.0/8 + # -o receive_override_options=no_header_body_checks,no_milters,no_unknown_recipient_checks + # -o relay_recipient_maps= + # -o smtpd_client_connection_count_limit=0 + # -o smtpd_client_connection_rate_limit=0 + # -o smtpd_client_restrictions=permit_mynetworks,reject + # -o smtpd_data_restrictions=reject_unauth_pipelining + # -o smtpd_delay_reject=no + # -o smtpd_end_of_data_restrictions= + # -o smtpd_error_sleep_time=0 + # -o smtpd_hard_error_limit=1000 + # -o smtpd_helo_restrictions= + # -o smtpd_milters= + # -o smtpd_recipient_restrictions=permit_mynetworks,reject + # -o smtpd_restriction_classes= + # -o smtpd_sender_restrictions= + # -o smtpd_soft_error_limit=1001 + # -o strict_rfc821_envelopes=yes + #submission inet n - - - - smtpd + # -o cleanup_service_name=pre-cleanup + # -o content_filter=amavis:[127.0.0.1]:10024 + # -o milter_macro_daemon_name=ORIGINATING + # -o receive_override_options=no_address_mappings + # -o smtpd_sender_restrictions=permit_tls_clientcerts,reject + # -o smtpd_tls_ask_ccert=yes + # -o smtpd_tls_auth_only=yes + # -o smtpd_tls_ccert_verifydepth=2 + # -o smtpd_tls_loglevel=1 + # -o smtpd_tls_req_ccert=yes + # -o smtpd_tls_security_level=encrypt + #smtps inet n - - - - smtpd + # -o milter_macro_daemon_name=ORIGINATING + # -o smtpd_client_restrictions=permit_sasl_authenticated,reject + # -o smtpd_sasl_auth_enable=yes + # -o smtpd_tls_ask_ccert=yes + # -o smtpd_tls_auth_only=yes + # -o smtpd_tls_ccert_verifydepth=0 + # -o smtpd_tls_loglevel=1 + # -o smtpd_tls_req_ccert=no + # -o smtpd_tls_security_level=encrypt + # -o smtpd_tls_wrappermode=yes + #pickup fifo n - - 60 1 pickup + # -o cleanup_service_name=pre-cleanup + # -o content_filter=amavis:[127.0.0.1]:10024 + #pre-cleanup unix n - - - 0 cleanup + # -o virtual_alias_maps= + #cleanup unix n - - - 0 cleanup + # -o mime_header_checks= + # -o nested_header_checks= + # -o body_checks= + # -o header_checks= + #-- SYMPA begin + #sympa unix - n n - - pipe + # flags=R user=sympa argv=/usr/lib/sympa/bin/queue ''${recipient} + #sympabounce unix - n n - - pipe + # flags=R user=sympa argv=/usr/lib/sympa/bin/bouncequeue ''${recipient} + #-- SYMPA end + ''; + #noclue unix - n n - - pipe + # flags=q user=noclue argv=/usr/local/bin/noclue-delivery ${recipient} ${sender} + }; +}; +} diff --git a/install/logical/machine1/postgrey.nix b/install/logical/machine1/postgrey.nix new file mode 100644 index 0000000..6a20ea1 --- /dev/null +++ b/install/logical/machine1/postgrey.nix @@ -0,0 +1,14 @@ +{pkgs, lib, config, ...}: +let inherit (config.services) postgrey; +in +{ +config = { + services.postgrey = { + enable = true; + autoWhitelist = 5; + maxAge = 35; + delay = 65; + privacy = true; + }; +}; +} diff --git a/install/logical/machine1/rmilter.nix b/install/logical/machine1/rmilter.nix new file mode 100644 index 0000000..ed5b269 --- /dev/null +++ b/install/logical/machine1/rmilter.nix @@ -0,0 +1,84 @@ +{pkgs, lib, config, ...}: +let inherit (builtins) attrNames; + inherit (lib) types; + inherit (config.services) dkim dovecot2 rmilter; + + createDomainDkimCert = dom: + let dkim_key = "${dkim.keyDir}/${dom}.${dkim.selector}.key"; + dkim_txt = "${dkim.keyDir}/${dom}.${dkim.selector}.txt"; + in '' + if [ ! -f "${dkim_key}" ] || [ ! -f "${dkim_txt}" ] + then + ${pkgs.opendkim}/bin/opendkim-genkey \ + -s "${dkim.selector}" \ + -d "${dom}" \ + --directory="${dkim.keyDir}" + mv "${dkim.keyDir}/${dkim.selector}.private" "${dkim_key}" + mv "${dkim.keyDir}/${dkim.selector}.txt" "${dkim_txt}" + fi + ''; +in +{ +options.services.dkim = lib.mkOption { + default = {}; + type = types.submodule { + options = { + keyDir = lib.mkOption { + type = types.path; + default = "/var/dkim"; + description = '' + ''; + }; + selector = lib.mkOption { + type = types.str; + default = "mail"; + description = '' + ''; + }; + }; + }; +}; +config = { + services.rspamd = { + enable = true; + }; + services.rmilter = { + enable = true; + #debug = true; + postfix = { + enable = true; + }; + rspamd = { + enable = true; + extraConfig = "extended_spam_headers = yes;"; + }; + extraConfig = '' + use_redis = true; + max_size = 20M; + #clamav { + # servers = /var/run/clamav/clamd.ctl; + #}; + # NOTE: domain = "*"; causes rmilter to try to search key in the key path + # as keypath/domain.selector.key for any domain. + dkim { + domain { + domain = "*"; + key = "${dkim.keyDir}"; + selector = "${dkim.selector}"; + }; + sign_alg = sha256; + auth_only = yes; + }; + ''; + }; + systemd.services.rmilter = { + requires = [ "rmilter.socket" ]; + after = [ "rmilter.socket" ]; + preStart = '' + install -D -d -o rmilter -g rmilter ${dkim.keyDir} + ${lib.concatStringsSep "\n" (map createDomainDkimCert (attrNames dovecot2.domains))} + chown -R rmilter:rmilter "${dkim.keyDir}" + ''; + }; +}; +} diff --git a/install/logical/machine1/shorewall.nix b/install/logical/machine1/shorewall.nix new file mode 100644 index 0000000..f7f89ba --- /dev/null +++ b/install/logical/machine1/shorewall.nix @@ -0,0 +1,148 @@ +{pkgs, lib, config, ...}: +let inherit (config.services) shorewall shorewall6; + when = x: y: if x == null then "" else y; +in +{ +config = { + services.shorewall = { + enable = true; + configs = { + "shorewall.conf" = '' + ${builtins.readFile "${shorewall.package}/etc/shorewall/shorewall.conf"} + # + ## Custom config + ### + STARTUP_ENABLED=Yes + ZONE2ZONE=2 + ''; + zones = '' + # DOC: shorewall-zones(5) + fw firewall + ${when config.networking.net "net ipv4"} + ${when config.networking.lan "lan ipv4"} + ''; + interfaces = '' + # DOC: shorewall-interfaces(5) + ?FORMAT 2 + ${when config.networking.net "net ${config.networking.net.iface} arp_filter,nosmurfs,routefilter,tcpflags"} + ${when config.networking.lan "lan ${config.networking.lan.iface} arp_filter,nosmurfs,routefilter,tcpflags"} + ''; + policy = '' + # DOC: shorewall-policy(5) + $FW all DROP + ${when config.networking.net "net all DROP none"} + ${when config.networking.lan "lan all DROP none"} + # XXX: the following policy must be last + all all REJECT none + ''; + rules = '' + # DOC: shorewall-rules(5) + #SECTION ALL + #SECTION ESTABLISHED + #SECTION RELATED + ?SECTION NEW + '' + + when config.networking.lan '' + # ---------- + # $FW -> lan + # ---------- + ACCEPT $FW lan:${config.networking.lan.ipv4}/24 + + # ---------- + # lan -> $FW + # ---------- + ACCEPT lan:${config.networking.lan.ipv4}/24 $FW + '' + + when config.networking.net '' + # ---------- + # $FW -> net + # ---------- + + # By protocol + Ping(ACCEPT) $FW net + + # By port + DNS(ACCEPT) $FW net + Git(ACCEPT) $FW net + HTTP(ACCEPT) $FW net + HTTPS(ACCEPT) $FW net + SMTP(ACCEPT) $FW net + SMTPS(ACCEPT) $FW net + SSH(ACCEPT) $FW net + + # ---------- + # net -> $FW + # ---------- + + # By protocol + Ping(ACCEPT) net $FW + + # By port + #HTTPS(ACCEPT) net $FW + DNS(ACCEPT) net $FW + IMAPS(ACCEPT) net $FW + POP3S(ACCEPT) net $FW + SMTP(ACCEPT) net $FW + SMTPS(ACCEPT) net $FW + ''; + "macro.Git" = '' + ?FORMAT 2 + #ACTION SOURCE DEST PROTO DEST SOURCE RATE USER/ + # PORT(S) PORT(S) LIMIT GROUP + PARAM - - tcp 9418 + ''; + }; + }; + services.shorewall6 = { + enable = true; + configs = { + "shorewall6.conf" = '' + ${builtins.readFile "${shorewall6.package}/etc/shorewall6/shorewall6.conf"} + # + ## Custom config + ### + STARTUP_ENABLED=Yes + ZONE2ZONE=2 + ''; + zones = '' + # DOC: shorewall-zones(5) + fw firewall + ${when config.networking.net "net ipv6"} + ${when config.networking.lan "lan ipv6"} + ''; + interfaces = '' + # DOC: shorewall-interfaces(5) + ?FORMAT 2 + ${when config.networking.net "net ${config.networking.net.iface} nosmurfs,tcpflags"} + ${when config.networking.lan "lan ${config.networking.lan.iface} nosmurfs,tcpflags"} + ''; + policy = '' + # DOC: shorewall-policy(5) + $FW all DROP + ${when config.networking.net "net all DROP none"} + ${when config.networking.lan "lan all DROP none"} + # XXX: the following policy must be last + all all REJECT none + ''; + rules = '' + # DOC: shorewall-rules(5) + #SECTION ALL + #SECTION ESTABLISHED + #SECTION RELATED + ?SECTION NEW + '' + + when config.networking.lan '' + # ---------- + # $FW -> lan + # ---------- + Ping(ACCEPT) $FW lan:fe80::/10 + + # ---------- + # lan -> $FW + # ---------- + Ping(ACCEPT) lan:fe80::/10 $FW + ''; + }; + }; +}; +} diff --git a/install/physical.nix b/install/physical.nix new file mode 100644 index 0000000..d6b0d45 --- /dev/null +++ b/install/physical.nix @@ -0,0 +1,2 @@ +with builtins; +import (toPath ./. + "/physical/" + getEnv "NIXOPS_DEPLOYMENT" + ".nix") diff --git a/install/physical/nixos.nix b/install/physical/nixos.nix new file mode 100644 index 0000000..4b56136 --- /dev/null +++ b/install/physical/nixos.nix @@ -0,0 +1,15 @@ +{ +machine1 = {pkgs, config, ...}: { + deployment.targetHost = "1.2.3.4"; + networking = { + net = { + iface = null; + ipv4 = null; + }; + lan = { + iface = null; + ipv4 = null; + }; + }; +}; +} diff --git a/install/physical/virtualbox.nix b/install/physical/virtualbox.nix new file mode 100644 index 0000000..1549abc --- /dev/null +++ b/install/physical/virtualbox.nix @@ -0,0 +1,46 @@ +{ +machine1 = {pkgs, lib, config, options, ...}: +let ipv4 = if options.networking.privateIPv4.isDefined + then config.networking.privateIPv4 + else "X.X.X.X"; +in +{ + + config = { + deployment.targetEnv = "virtualbox"; + deployment.virtualbox.headless = true; + deployment.virtualbox.memorySize = 1024; + deployment.virtualbox.vcpu = 2; + deployment.virtualbox.disks.disk1.baseImage = ; + #deployment.virtualbox.disks.disk1.size = 6024; + # NOTE: resize not yet supported. + + deployment.storeKeysOnMachine = true; + deployment.autoLuks = { + # NOTE: not working on virtualbox deployment + secretdisk = { + device = "/dev/sda"; + passphrase = "foobar"; + autoFormat = true; + cipher = "aes-cbc-essiv:sha256"; + }; + }; + networking = { + interfaces."enp0s8" = { + #macAddress = "00:11:22:33:44:55"; + ipv4.addresses = [ { address = ipv4; prefixLength = 32; } ]; + ipv6.addresses = [ { address = "fe80::1"; prefixLength = 64; } ]; + }; + net = { + iface = "enp0s3"; + ipv4 = ipv4; + }; + lan = { + iface = "enp0s8"; + ipv4 = ipv4; + #ipv6 = "fe80::1"; + }; + }; + }; +}; +} -- 2.44.1