Self-host your own digital island

Compare changes

Choose any two refs to compare.

+1 -1
README.md
···
# Eilean
<div align="center">
-
<img src="./eilean-donan.jpg" alt="Eilean Donan" width="400"/>
+
<img src="./eilean-donan.jpg" alt="Eilean Donan"/>
<!-- Photo by DAVID ILIFF. License: CC BY-SA 3.0 -->
</div>
+2 -1
docs/getting_started.md
···
## Further Information
-
See [docs](../docs/).
+
For a list of options, use `man eilean-configuration.nix`.
+
eilean-donan.jpg

This is a binary file and will not be displayed.

+283 -5
flake.lock
···
{
"nodes": {
+
"blobs": {
+
"flake": false,
+
"locked": {
+
"lastModified": 1604995301,
+
"narHash": "sha256-wcLzgLec6SGJA8fx1OEN1yV/Py5b+U5iyYpksUY/yLw=",
+
"owner": "simple-nixos-mailserver",
+
"repo": "blobs",
+
"rev": "2cccdf1ca48316f2cfd1c9a0017e8de5a7156265",
+
"type": "gitlab"
+
},
+
"original": {
+
"owner": "simple-nixos-mailserver",
+
"repo": "blobs",
+
"type": "gitlab"
+
}
+
},
+
"eon": {
+
"inputs": {
+
"flake-utils": "flake-utils",
+
"nixpkgs": [
+
"nixpkgs"
+
],
+
"opam-nix": "opam-nix"
+
},
+
"locked": {
+
"lastModified": 1738666931,
+
"narHash": "sha256-dTF+etN5ZDPVwK8XV/huQByY6JohiVgpCfzVJWAZY1I=",
+
"owner": "RyanGibb",
+
"repo": "eon",
+
"rev": "42523d1d8f720215ab5108a1b42e9c5b7d17d4bf",
+
"type": "github"
+
},
+
"original": {
+
"owner": "RyanGibb",
+
"repo": "eon",
+
"type": "github"
+
}
+
},
+
"flake-compat": {
+
"flake": false,
+
"locked": {
+
"lastModified": 1696426674,
+
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
+
"owner": "edolstra",
+
"repo": "flake-compat",
+
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
+
"type": "github"
+
},
+
"original": {
+
"owner": "edolstra",
+
"repo": "flake-compat",
+
"type": "github"
+
}
+
},
+
"flake-compat_2": {
+
"flake": false,
+
"locked": {
+
"lastModified": 1696426674,
+
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
+
"owner": "edolstra",
+
"repo": "flake-compat",
+
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
+
"type": "github"
+
},
+
"original": {
+
"owner": "edolstra",
+
"repo": "flake-compat",
+
"type": "github"
+
}
+
},
+
"flake-utils": {
+
"inputs": {
+
"systems": "systems"
+
},
+
"locked": {
+
"lastModified": 1731533236,
+
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+
"owner": "numtide",
+
"repo": "flake-utils",
+
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+
"type": "github"
+
},
+
"original": {
+
"owner": "numtide",
+
"repo": "flake-utils",
+
"type": "github"
+
}
+
},
+
"mirage-opam-overlays": {
+
"flake": false,
+
"locked": {
+
"lastModified": 1710922379,
+
"narHash": "sha256-j4QREQDUf8oHOX7qg6wAOupgsNQoYlufxoPrgagD+pY=",
+
"owner": "dune-universe",
+
"repo": "mirage-opam-overlays",
+
"rev": "797cb363df3ff763c43c8fbec5cd44de2878757e",
+
"type": "github"
+
},
+
"original": {
+
"owner": "dune-universe",
+
"repo": "mirage-opam-overlays",
+
"type": "github"
+
}
+
},
+
"nixos-mailserver": {
+
"inputs": {
+
"blobs": "blobs",
+
"flake-compat": "flake-compat_2",
+
"nixpkgs": [
+
"nixpkgs"
+
],
+
"nixpkgs-24_05": "nixpkgs-24_05",
+
"utils": "utils"
+
},
+
"locked": {
+
"lastModified": 1718183756,
+
"narHash": "sha256-m5JQT/RIegSLZJx41Cv7d8Xoa2KKq+5uLkgB5KJR5D0=",
+
"owner": "RyanGibb",
+
"repo": "nixos-mailserver",
+
"rev": "9dc7a8d40232f600e6ca1e78356cd4398665b46b",
+
"type": "gitlab"
+
},
+
"original": {
+
"owner": "RyanGibb",
+
"ref": "fork-24.05",
+
"repo": "nixos-mailserver",
+
"type": "gitlab"
+
}
+
},
"nixpkgs": {
"locked": {
-
"lastModified": 1703068421,
-
"narHash": "sha256-WSw5Faqlw75McIflnl5v7qVD/B3S2sLh+968bpOGrWA=",
-
"owner": "NixOS",
+
"lastModified": 1732981179,
+
"narHash": "sha256-F7thesZPvAMSwjRu0K8uFshTk3ZZSNAsXTIFvXBT+34=",
+
"owner": "nixos",
"repo": "nixpkgs",
-
"rev": "d65bceaee0fb1e64363f7871bc43dc1c6ecad99f",
+
"rev": "62c435d93bf046a5396f3016472e8f7c8e2aed65",
"type": "github"
},
"original": {
"owner": "nixos",
-
"ref": "nixos-unstable",
+
"ref": "nixos-24.11",
"repo": "nixpkgs",
"type": "github"
}
},
+
"nixpkgs-24_05": {
+
"locked": {
+
"lastModified": 1718086528,
+
"narHash": "sha256-hoB7B7oPgypePz16cKWawPfhVvMSXj4G/qLsfFuhFjw=",
+
"owner": "NixOS",
+
"repo": "nixpkgs",
+
"rev": "47b604b07d1e8146d5398b42d3306fdebd343986",
+
"type": "github"
+
},
+
"original": {
+
"id": "nixpkgs",
+
"ref": "nixos-24.05",
+
"type": "indirect"
+
}
+
},
+
"opam-nix": {
+
"inputs": {
+
"flake-compat": "flake-compat",
+
"flake-utils": [
+
"eon",
+
"flake-utils"
+
],
+
"mirage-opam-overlays": "mirage-opam-overlays",
+
"nixpkgs": [
+
"eon",
+
"nixpkgs"
+
],
+
"opam-overlays": "opam-overlays",
+
"opam-repository": "opam-repository",
+
"opam2json": "opam2json"
+
},
+
"locked": {
+
"lastModified": 1732617437,
+
"narHash": "sha256-jj25fziYrES8Ix6HkfSiLzrN6MZjiwlHUxFSIuLRjgE=",
+
"owner": "tweag",
+
"repo": "opam-nix",
+
"rev": "ea8b9cb81fe94e1fc45c6376fcff15f17319c445",
+
"type": "github"
+
},
+
"original": {
+
"owner": "tweag",
+
"repo": "opam-nix",
+
"type": "github"
+
}
+
},
+
"opam-overlays": {
+
"flake": false,
+
"locked": {
+
"lastModified": 1726822209,
+
"narHash": "sha256-bwM18ydNT9fYq91xfn4gmS21q322NYrKwfq0ldG9GYw=",
+
"owner": "dune-universe",
+
"repo": "opam-overlays",
+
"rev": "f2bec38beca4aea9e481f2fd3ee319c519124649",
+
"type": "github"
+
},
+
"original": {
+
"owner": "dune-universe",
+
"repo": "opam-overlays",
+
"type": "github"
+
}
+
},
+
"opam-repository": {
+
"flake": false,
+
"locked": {
+
"lastModified": 1732612513,
+
"narHash": "sha256-kju4NWEQo4xTxnKeBIsmqnyxIcCg6sNZYJ1FmG/gCDw=",
+
"owner": "ocaml",
+
"repo": "opam-repository",
+
"rev": "3d52b66b04788999a23f22f0d59c2dfc831c4f32",
+
"type": "github"
+
},
+
"original": {
+
"owner": "ocaml",
+
"repo": "opam-repository",
+
"type": "github"
+
}
+
},
+
"opam2json": {
+
"inputs": {
+
"nixpkgs": [
+
"eon",
+
"opam-nix",
+
"nixpkgs"
+
]
+
},
+
"locked": {
+
"lastModified": 1671540003,
+
"narHash": "sha256-5pXfbUfpVABtKbii6aaI2EdAZTjHJ2QntEf0QD2O5AM=",
+
"owner": "tweag",
+
"repo": "opam2json",
+
"rev": "819d291ea95e271b0e6027679de6abb4d4f7f680",
+
"type": "github"
+
},
+
"original": {
+
"owner": "tweag",
+
"repo": "opam2json",
+
"type": "github"
+
}
+
},
"root": {
"inputs": {
+
"eon": "eon",
+
"nixos-mailserver": "nixos-mailserver",
"nixpkgs": "nixpkgs"
+
}
+
},
+
"systems": {
+
"locked": {
+
"lastModified": 1681028828,
+
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+
"owner": "nix-systems",
+
"repo": "default",
+
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+
"type": "github"
+
},
+
"original": {
+
"owner": "nix-systems",
+
"repo": "default",
+
"type": "github"
+
}
+
},
+
"systems_2": {
+
"locked": {
+
"lastModified": 1681028828,
+
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+
"owner": "nix-systems",
+
"repo": "default",
+
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+
"type": "github"
+
},
+
"original": {
+
"owner": "nix-systems",
+
"repo": "default",
+
"type": "github"
+
}
+
},
+
"utils": {
+
"inputs": {
+
"systems": "systems_2"
+
},
+
"locked": {
+
"lastModified": 1709126324,
+
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
+
"owner": "numtide",
+
"repo": "flake-utils",
+
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
+
"type": "github"
+
},
+
"original": {
+
"owner": "numtide",
+
"repo": "flake-utils",
+
"type": "github"
}
}
},
+29 -7
flake.nix
···
{
-
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
+
inputs = {
+
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
+
nixos-mailserver.url = "gitlab:RyanGibb/nixos-mailserver/fork-24.05";
+
eon.url = "github:RyanGibb/eon";
-
outputs = { self, nixpkgs, ... }@inputs: {
+
eon.inputs.nixpkgs.follows = "nixpkgs";
+
nixos-mailserver.inputs.nixpkgs.follows = "nixpkgs";
+
};
+
+
outputs = { nixpkgs, nixos-mailserver, eon, ... }: {
+
packages = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed (system:
+
let pkgs = nixpkgs.legacyPackages.${system};
+
in {
+
manpage = import ./man { inherit pkgs system nixos-mailserver; };
+
packages.mautrix-meta = (pkgs.callPackage ./pkgs/mautrix-meta.nix { });
+
});
+
nixosModules.default = {
imports = [
./modules/default.nix
-
({ config, ... }: {
-
nixpkgs.overlays = [ (final: prev: {
-
mautrix-meta = (prev.callPackage ./pkgs/mautrix-meta.nix { });
-
}) ];
-
})
+
nixos-mailserver.nixosModule
+
eon.nixosModules.default
+
eon.nixosModules.acme
+
{
+
nixpkgs.overlays = [
+
(final: prev: {
+
mautrix-meta = (prev.callPackage ./pkgs/mautrix-meta.nix { });
+
})
+
];
+
}
];
};
defaultTemplate.path = ./template;
+
+
formatter = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed
+
(system: nixpkgs.legacyPackages.${system}.nixfmt);
};
}
+35
man/default.nix
···
+
{ pkgs, system, nixos-mailserver, ... }:
+
+
with pkgs;
+
let
+
optionsDoc = let
+
eval = import (pkgs.path + "/nixos/lib/eval-config.nix") {
+
inherit system;
+
modules = [ ../modules/default.nix nixos-mailserver ];
+
};
+
in pkgs.nixosOptionsDoc {
+
options = eval.options;
+
# TODO make sure all options have descriptions
+
warningsAreErrors = false;
+
};
+
+
# Generate the `man eliean.nix` package
+
eilean-configuration-manual = runCommand "eilean-reference-manpage" {
+
nativeBuildInputs =
+
[ buildPackages.installShellFiles buildPackages.nixos-render-docs ];
+
allowedReferences = [ "out" ];
+
} ''
+
# Generate manpages.
+
mkdir -p $out/share/man/man5
+
# filter to only eilean options
+
cat ${optionsDoc.optionsJSON}/share/doc/nixos/options.json \
+
| ${pkgs.jq}/bin/jq 'with_entries(select(.key | test("^eilean")))' \
+
> eilean-options.json
+
nixos-render-docs -j $NIX_BUILD_CORES options manpage \
+
--revision dev \
+
--header ${./eilean-configuration-nix-header.5} \
+
--footer ${./eilean-configuration-nix-footer.5} \
+
eilean-options.json \
+
$out/share/man/man5/eilean-configuration.nix.5
+
'';
+
in eilean-configuration-manual
+3
man/eilean-configuration-nix-footer.5
···
+
.SH "AUTHORS"
+
.PP
+
Eilean contributors
+17
man/eilean-configuration-nix-header.5
···
+
.TH "EILEAN-CONFIGURATION\&.NIX" "5" "01/01/1980" "Home Manager"
+
.\" disable hyphenation
+
.nh
+
.\" disable justification (adjust text to left margin only)
+
.ad l
+
.\" enable line breaks after slashes
+
.cflags 4 /
+
.SH "NAME"
+
\fIeilean\-configuration\&.nix\fP \- Eilean configuration specification
+
.SH "DESCRIPTION"
+
.sp
+
Self-host your own digital island.
+
.sp
+
Eilean extends the NixOS module system with the following options.
+
.sp
+
.SH "OPTIONS"
+
.PP
+16
modules/acme-eon.nix
···
+
{ pkgs, config, lib, ... }:
+
+
with lib;
+
let cfg = config.eilean;
+
in {
+
options.eilean.acme-eon = mkEnableOption "acme-eon";
+
+
config = mkIf cfg.acme-eon {
+
assertions = [{
+
assertion = cfg.services.dns.server == "eon";
+
message = ''
+
If config.eilean.acme-eon is enabled config.eilean.services.dns.server must be "eon".
+
'';
+
}];
+
};
+
}
+17 -16
modules/default.nix
···
-
{ lib, config, ... }:
+
{ pkgs, lib, config, ... }:
with lib;
{
imports = [
+
./acme-eon.nix
./services/dns/default.nix
-
./mailserver/default.nix
./mastodon.nix
./mailserver.nix
./gitea.nix
./dns.nix
+
./fail2ban.nix
./matrix/synapse.nix
-
./matrix/mautrix-signal.nix
./matrix/mautrix-instagram.nix
./matrix/mautrix-messenger.nix
./turn.nix
./headscale.nix
-
./wireguard/server.nix
./wireguard/default.nix
+
./radicale.nix
];
options.eilean = with types; {
-
username = mkOption {
-
type = str;
-
};
-
serverIpv4 = mkOption {
-
type = str;
-
};
-
serverIpv6 = mkOption {
-
type = str;
-
};
-
publicInterface = mkOption {
-
type = str;
+
username = mkOption { type = str; };
+
serverIpv4 = mkOption { type = str; };
+
serverIpv6 = mkOption { type = str; };
+
publicInterface = mkOption { type = str; };
+
domainName = mkOption {
+
type = types.str;
+
default = "vps";
};
};
config = {
-
security.acme.defaults.email = "${config.eilean.username}@${config.networking.domain}";
+
# TODO install manpage
+
environment.systemPackages = [ ];
+
security.acme.defaults.email = lib.mkIf (!config.eilean.acme-eon)
+
"${config.eilean.username}@${config.networking.domain}";
+
security.acme-eon.defaults.email = lib.mkIf config.eilean.acme-eon
+
"${config.eilean.username}@${config.networking.domain}";
networking.firewall.allowedTCPPorts = mkIf config.services.nginx.enable [
80 # HTTP
443 # HTTPS
+16 -22
modules/dns.nix
···
{ config, lib, ... }:
with lib;
-
let cfg = config.eilean; in
-
{
-
+
let cfg = config.eilean;
+
in {
+
options.eilean.dns = {
enable = mkEnableOption "dns";
nameservers = mkOption {
···
default = [ "ns1" "ns2" ];
};
};
-
+
config.eilean.services.dns = mkIf cfg.dns.enable {
enable = true;
zones.${config.networking.domain} = {
···
{
name = "@";
type = "NS";
-
data = ns;
+
value = ns;
}
{
name = ns;
type = "A";
-
data = cfg.serverIpv4;
-
}
-
{
-
name = "@";
-
type = "NS";
-
data = ns;
+
value = cfg.serverIpv4;
}
{
name = ns;
type = "AAAA";
-
data = cfg.serverIpv6;
+
value = cfg.serverIpv6;
}
-
]) cfg.dns.nameservers ++
-
[
+
]) cfg.dns.nameservers ++ [
{
name = "@";
type = "A";
-
data = cfg.serverIpv4;
+
value = cfg.serverIpv4;
}
{
name = "@";
type = "AAAA";
-
data = cfg.serverIpv6;
+
value = cfg.serverIpv6;
}
{
-
name = "vps";
+
name = cfg.domainName;
type = "A";
-
data = cfg.serverIpv4;
+
value = cfg.serverIpv4;
}
{
-
name = "vps";
+
name = cfg.domainName;
type = "AAAA";
-
data = cfg.serverIpv6;
+
value = cfg.serverIpv6;
}
-
+
{
name = "@";
type = "LOC";
-
data = "52 12 40.4 N 0 5 31.9 E 22m 10m 10m 10m";
+
value = "52 12 40.4 N 0 5 31.9 E 22m 10m 10m 10m";
}
];
};
+42
modules/fail2ban.nix
···
+
{ config, pkgs, lib, ... }:
+
+
with lib;
+
let cfg = config.eilean;
+
in {
+
options.eilean.fail2ban = {
+
enable = mkEnableOption "TURN server";
+
radicale = mkOption {
+
type = types.bool;
+
default = cfg.radicale.enable;
+
};
+
};
+
+
config = mkIf cfg.fail2ban.enable {
+
services.fail2ban = {
+
enable = true;
+
bantime = "24h";
+
bantime-increment = {
+
enable = true;
+
multipliers = "1 2 4 8 16 32 64";
+
maxtime = "168h";
+
overalljails = true;
+
};
+
jails."radicale".settings = mkIf cfg.fail2ban.radicale {
+
port = "5232";
+
filter = "radicale";
+
banaction = "%(banaction_allports)s[name=radicale]";
+
backend = "systemd";
+
journalmatch = "_SYSTEMD_UNIT=radicale.service";
+
maxRetry = 2;
+
bantime = -1;
+
findtime = 14400;
+
};
+
};
+
environment.etc = {
+
"fail2ban/filter.d/radicale.local".text = mkIf cfg.fail2ban.radicale ''
+
[Definition]
+
failregex = ^.*Failed\slogin\sattempt\sfrom\s.*\(forwarded for \'<HOST>\'.*\):\s.*
+
'';
+
};
+
};
+
}
+29 -21
modules/gitea.nix
···
let
cfg = config.eilean;
domain = config.networking.domain;
+
subdomain = "git.${domain}";
in {
options.eilean.gitea = {
enable = mkEnableOption "gitea";
···
};
config = mkIf cfg.gitea.enable {
+
security.acme-eon.nginxCerts = [ subdomain ];
+
services.nginx = {
enable = true;
recommendedProxySettings = true;
-
virtualHosts."git.${domain}" = {
-
enableACME = true;
+
virtualHosts."${subdomain}" = {
+
enableACME = lib.mkIf (!cfg.acme-eon) true;
forceSSL = true;
locations."/" = {
-
proxyPass = "http://localhost:${builtins.toString config.services.gitea.settings.server.HTTP_PORT}/";
+
proxyPass = "http://localhost:${
+
builtins.toString config.services.gitea.settings.server.HTTP_PORT
+
}/";
};
};
};
···
mailerPasswordFile = cfg.mailserver.systemAccountPasswordFile;
settings = {
server = {
-
ROOT_URL = "https://git.${domain}/";
-
DOMAIN = "git.${domain}";
+
ROOT_URL = "https://${subdomain}/";
+
DOMAIN = subdomain;
};
mailer = {
ENABLED = true;
···
RestrictRealtime = mkForce false;
RestrictSUIDSGID = mkForce false;
SystemCallArchitectures = mkForce "";
-
SystemCallFilter = mkForce [];
+
SystemCallFilter = mkForce [ ];
};
eilean.dns.enable = true;
-
eilean.services.dns.zones.${config.networking.domain}.records = [
-
{
-
name = "git";
-
type = "CNAME";
-
data = "vps";
-
}
-
];
+
eilean.services.dns.zones.${config.networking.domain}.records = [{
+
name = "git";
+
type = "CNAME";
+
value = cfg.domainName;
+
}];
# proxy port 22 on ethernet interface to internal gitea ssh server
# openssh server remains accessible on port 22 via vpn(s)
···
};
networking.firewall = {
-
allowedTCPPorts = [
-
22
-
cfg.gitea.sshPort
-
];
+
allowedTCPPorts = [ 22 cfg.gitea.sshPort ];
extraCommands = ''
# proxy all traffic on public interface to the gitea SSH server
-
iptables -A PREROUTING -t nat -i ${config.eilean.publicInterface} -p tcp --dport 22 -j REDIRECT --to-port ${builtins.toString cfg.gitea.sshPort}
-
ip6tables -A PREROUTING -t nat -i ${config.eilean.publicInterface} -p tcp --dport 22 -j REDIRECT --to-port ${builtins.toString cfg.gitea.sshPort}
+
iptables -A PREROUTING -t nat -i ${config.eilean.publicInterface} -p tcp --dport 22 -j REDIRECT --to-port ${
+
builtins.toString cfg.gitea.sshPort
+
}
+
ip6tables -A PREROUTING -t nat -i ${config.eilean.publicInterface} -p tcp --dport 22 -j REDIRECT --to-port ${
+
builtins.toString cfg.gitea.sshPort
+
}
# proxy locally originating outgoing packets
-
iptables -A OUTPUT -d ${config.eilean.serverIpv4} -t nat -p tcp --dport 22 -j REDIRECT --to-port ${builtins.toString cfg.gitea.sshPort}
-
ip6tables -A OUTPUT -d ${config.eilean.serverIpv6} -t nat -p tcp --dport 22 -j REDIRECT --to-port ${builtins.toString cfg.gitea.sshPort}
+
iptables -A OUTPUT -d ${config.eilean.serverIpv4} -t nat -p tcp --dport 22 -j REDIRECT --to-port ${
+
builtins.toString cfg.gitea.sshPort
+
}
+
ip6tables -A OUTPUT -d ${config.eilean.serverIpv6} -t nat -p tcp --dport 22 -j REDIRECT --to-port ${
+
builtins.toString cfg.gitea.sshPort
+
}
'';
};
+10 -16
modules/headscale.nix
···
{ pkgs, config, lib, ... }:
with lib;
-
let
-
cfg = config.eilean;
+
let cfg = config.eilean;
in {
options.eilean.headscale = with lib; {
enable = mkEnableOption "headscale";
zone = mkOption {
type = types.str;
-
default = "${config.networking.domain}";
+
default = config.networking.domain;
+
defaultText = "config.networking.domain";
};
domain = mkOption {
type = types.str;
default = "headscale.${config.networking.domain}";
+
defaultText = "headscale.$\${config.networking.domain}";
};
};
···
settings = {
server_url = "https://${cfg.headscale.domain}";
logtail.enabled = false;
-
ip_prefixes = [ "100.64.0.0/10" ];
-
dns_config = {
-
# magicDns = true;
-
nameservers = config.networking.nameservers;
-
base_domain = "${cfg.headscale.zone}";
-
};
+
ip_prefixes = [ "100.64.0.0/10" "fd7a:115c:a1e0::/48" ];
};
};
···
environment.systemPackages = [ config.services.headscale.package ];
eilean.dns.enable = true;
-
eilean.services.dns.zones.${cfg.headscale.zone}.records = [
-
{
-
name = "${cfg.headscale.domain}.";
-
type = "CNAME";
-
data = "vps";
-
}
-
];
+
eilean.services.dns.zones.${cfg.headscale.zone}.records = [{
+
name = "${cfg.headscale.domain}.";
+
type = "CNAME";
+
value = cfg.domainName;
+
}];
};
}
-78
modules/mailserver/borgbackup.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
let
-
cfg = config.mailserver.borgbackup;
-
-
methodFragment = lib.optional (cfg.compression.method != null) cfg.compression.method;
-
autoFragment =
-
if cfg.compression.auto && cfg.compression.method == null
-
then throw "compression.method must be set when using auto."
-
else lib.optional cfg.compression.auto "auto";
-
levelFragment =
-
if cfg.compression.level != null && cfg.compression.method == null
-
then throw "compression.method must be set when using compression.level."
-
else lib.optional (cfg.compression.level != null) (toString cfg.compression.level);
-
compressionFragment = lib.concatStringsSep "," (lib.flatten [autoFragment methodFragment levelFragment]);
-
compression = lib.optionalString (compressionFragment != "") "--compression ${compressionFragment}";
-
-
encryptionFragment = cfg.encryption.method;
-
passphraseFile = lib.escapeShellArg cfg.encryption.passphraseFile;
-
passphraseFragment = lib.optionalString (cfg.encryption.method != "none")
-
(if cfg.encryption.passphraseFile != null then ''env BORG_PASSPHRASE="$(cat ${passphraseFile})"''
-
else throw "passphraseFile must be set when using encryption.");
-
-
locations = lib.escapeShellArgs cfg.locations;
-
name = lib.escapeShellArg cfg.name;
-
-
repoLocation = lib.escapeShellArg cfg.repoLocation;
-
-
extraInitArgs = lib.escapeShellArgs cfg.extraArgumentsForInit;
-
extraCreateArgs = lib.escapeShellArgs cfg.extraArgumentsForCreate;
-
-
cmdPreexec = lib.optionalString (cfg.cmdPreexec != null) cfg.cmdPreexec;
-
cmdPostexec = lib.optionalString (cfg.cmdPostexec != null) cfg.cmdPostexec;
-
-
borgScript = ''
-
export BORG_REPO=${repoLocation}
-
${cmdPreexec}
-
${passphraseFragment} ${pkgs.borgbackup}/bin/borg init ${extraInitArgs} --encryption ${encryptionFragment} || true
-
${passphraseFragment} ${pkgs.borgbackup}/bin/borg create ${extraCreateArgs} ${compression} ::${name} ${locations}
-
${cmdPostexec}
-
'';
-
in {
-
config = lib.mkIf (config.mailserver.enable && cfg.enable) {
-
environment.systemPackages = with pkgs; [
-
borgbackup
-
];
-
-
systemd.services.borgbackup = {
-
description = "borgbackup";
-
unitConfig.Documentation = "man:borgbackup";
-
script = borgScript;
-
serviceConfig = {
-
User = cfg.user;
-
Group = cfg.group;
-
CPUSchedulingPolicy = "idle";
-
IOSchedulingClass = "idle";
-
ProtectSystem = "full";
-
};
-
startAt = cfg.startAt;
-
};
-
};
-
}
-30
modules/mailserver/clamav.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, options, ... }:
-
-
let
-
cfg = config.mailserver;
-
in
-
{
-
config = lib.mkIf (cfg.enable && cfg.virusScanning) {
-
services.clamav.daemon = {
-
enable = true;
-
settings.PhishingScanURLs = "no";
-
};
-
services.clamav.updater.enable = true;
-
};
-
}
-48
modules/mailserver/common.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib }:
-
-
let
-
cfg = config.mailserver;
-
in
-
{
-
# cert :: PATH
-
certificatePath = if cfg.certificateScheme == 1
-
then cfg.certificateFile
-
else if cfg.certificateScheme == 2
-
then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
-
else if cfg.certificateScheme == 3
-
then "${config.security.acme.certs.${cfg.fqdn}.directory}/fullchain.pem"
-
else throw "Error: Certificate Scheme must be in { 1, 2, 3 }";
-
-
# key :: PATH
-
keyPath = if cfg.certificateScheme == 1
-
then cfg.keyFile
-
else if cfg.certificateScheme == 2
-
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
-
else if cfg.certificateScheme == 3
-
then "${config.security.acme.certs.${cfg.fqdn}.directory}/key.pem"
-
else throw "Error: Certificate Scheme must be in { 1, 2, 3 }";
-
-
passwordFiles = let
-
mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password" hash;
-
in
-
lib.mapAttrs (name: value:
-
if value.passwordFile == null then
-
builtins.toString (mkHashFile name value.password)
-
else value.passwordFile) cfg.loginAccounts;
-
}
-4
modules/mailserver/debug.nix
···
-
{ config, lib, ... }:
-
{
-
mailserver.policydSPFExtraConfig = lib.mkIf config.mailserver.debug "debugLevel = 4";
-
}
-1035
modules/mailserver/default.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, lib, pkgs, ... }:
-
-
with lib;
-
-
let
-
cfg = config.mailserver;
-
in
-
{
-
options.mailserver = {
-
enable = lib.mkEnableOption "nixos-mailserver";
-
-
openFirewall = mkOption {
-
type = types.bool;
-
default = true;
-
description = "Automatically open ports in the firewall.";
-
};
-
-
fqdn = mkOption {
-
type = types.str;
-
example = "mx.example.com";
-
description = "The fully qualified domain name of the mail server.";
-
};
-
-
domains = mkOption {
-
type = types.listOf types.str;
-
example = [ "example.com" ];
-
default = [];
-
description = "The domains that this mail server serves.";
-
};
-
-
certificateDomains = mkOption {
-
type = types.listOf types.str;
-
example = [ "imap.example.com" "pop3.example.com" ];
-
default = [];
-
description = "Secondary domains and subdomains for which it is necessary to generate a certificate.";
-
};
-
-
messageSizeLimit = mkOption {
-
type = types.int;
-
example = 52428800;
-
default = 20971520;
-
description = "Message size limit enforced by Postfix.";
-
};
-
-
loginAccounts = mkOption {
-
type = types.attrsOf (types.submodule ({ name, ... }: {
-
options = {
-
name = mkOption {
-
type = types.str;
-
example = "user1@example.com";
-
description = "Username";
-
};
-
-
password = mkOption {
-
type = with types; nullOr str;
-
default = null;
-
example = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
-
description = ''
-
The password.
-
-
Warning: this is stored in plaintext in the Nix store!
-
Use `passwordFile` instead.
-
'';
-
};
-
-
passwordFile = mkOption {
-
type = with types; nullOr path;
-
default = null;
-
example = "/run/keys/user1-passwordhash";
-
description = ''
-
A file containing the user's password.
-
'';
-
};
-
-
aliases = mkOption {
-
type = with types; listOf types.str;
-
example = ["abuse@example.com" "postmaster@example.com"];
-
default = [];
-
description = ''
-
A list of aliases of this login account.
-
Note: Use list entries like "@example.com" to create a catchAll
-
that allows sending from all email addresses in these domain.
-
'';
-
};
-
-
catchAll = mkOption {
-
type = with types; listOf (enum cfg.domains);
-
example = ["example.com" "example2.com"];
-
default = [];
-
description = ''
-
For which domains should this account act as a catch all?
-
Note: Does not allow sending from all addresses of these domains.
-
'';
-
};
-
-
quota = mkOption {
-
type = with types; nullOr types.str;
-
default = null;
-
example = "2G";
-
description = ''
-
Per user quota rules. Accepted sizes are `xx k/M/G/T` with the
-
obvious meaning. Leave blank for the standard quota `100G`.
-
'';
-
};
-
-
sieveScript = mkOption {
-
type = with types; nullOr lines;
-
default = null;
-
example = ''
-
require ["fileinto", "mailbox"];
-
-
if address :is "from" "gitlab@mg.gitlab.com" {
-
fileinto :create "GitLab";
-
stop;
-
}
-
-
# This must be the last rule, it will check if list-id is set, and
-
# file the message into the Lists folder for further investigation
-
elsif header :matches "list-id" "<?*>" {
-
fileinto :create "Lists";
-
stop;
-
}
-
'';
-
description = ''
-
Per-user sieve script.
-
'';
-
};
-
-
sendOnly = mkOption {
-
type = types.bool;
-
default = false;
-
description = ''
-
Specifies if the account should be a send-only account.
-
Emails sent to send-only accounts will be rejected from
-
unauthorized senders with the sendOnlyRejectMessage
-
stating the reason.
-
'';
-
};
-
-
sendOnlyRejectMessage = mkOption {
-
type = types.str;
-
default = "This account cannot receive emails.";
-
description = ''
-
The message that will be returned to the sender when an email is
-
sent to a send-only account. Only used if the account is marked
-
as send-only.
-
'';
-
};
-
};
-
-
config.name = mkDefault name;
-
}));
-
example = {
-
user1 = {
-
password = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
-
};
-
user2 = {
-
password = "$6$oE0ZNv2n7Vk9gOf$9xcZWCCLGdMflIfuA0vR1Q1Xblw6RZqPrP94mEit2/81/7AKj2bqUai5yPyWE.QYPyv6wLMHZvjw3Rlg7yTCD/";
-
};
-
};
-
description = ''
-
The login account of the domain. Every account is mapped to a unix user,
-
e.g. `user1@example.com`. To generate the passwords use `htpasswd` as
-
follows
-
-
```
-
nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
-
```
-
'';
-
default = {};
-
};
-
-
indexDir = mkOption {
-
type = types.nullOr types.str;
-
default = null;
-
description = ''
-
Folder to store search indices. If null, indices are stored
-
along with email, which could not necessarily be desirable,
-
especially when the fullTextSearch option is enable since
-
indices it creates are voluminous and do not need to be backed
-
up.
-
-
Be careful when changing this option value since all indices
-
would be recreated at the new location (and clients would need
-
to resynchronize).
-
-
Note the some variables can be used in the file path. See
-
https://doc.dovecot.org/configuration_manual/mail_location/#variables
-
for details.
-
'';
-
example = "/var/lib/dovecot/indices";
-
};
-
-
fullTextSearch = {
-
enable = lib.mkEnableOption "Full text search indexing with xapian. This has significant performance and disk space cost.";
-
autoIndex = mkOption {
-
type = types.bool;
-
default = true;
-
description = "Enable automatic indexing of messages as they are received or modified.";
-
};
-
autoIndexExclude = mkOption {
-
type = types.listOf types.str;
-
default = [ ];
-
example = [ "\\Trash" "SomeFolder" "Other/*" ];
-
description = ''
-
Mailboxes to exclude from automatic indexing.
-
'';
-
};
-
-
indexAttachments = mkOption {
-
type = types.bool;
-
default = false;
-
description = "Also index text-only attachements. Binary attachements are never indexed.";
-
};
-
-
enforced = mkOption {
-
type = types.enum [ "yes" "no" "body" ];
-
default = "no";
-
description = ''
-
Fail searches when no index is available. If set to
-
<literal>body</literal>, then only body searches (as opposed to
-
header) are affected. If set to <literal>no</literal>, searches may
-
fall back to a very slow brute force search.
-
'';
-
};
-
-
minSize = mkOption {
-
type = types.int;
-
default = 2;
-
description = "Size of the smallest n-gram to index.";
-
};
-
maxSize = mkOption {
-
type = types.int;
-
default = 20;
-
description = "Size of the largest n-gram to index.";
-
};
-
memoryLimit = mkOption {
-
type = types.nullOr types.int;
-
default = null;
-
example = 2000;
-
description = "Memory limit for the indexer process, in MiB. If null, leaves the default (which is rather low), and if 0, no limit.";
-
};
-
-
maintenance = {
-
enable = mkOption {
-
type = types.bool;
-
default = true;
-
description = "Regularly optmize indices, as recommended by upstream.";
-
};
-
-
onCalendar = mkOption {
-
type = types.str;
-
default = "daily";
-
description = "When to run the maintenance job. See systemd.time(7) for more information about the format.";
-
};
-
-
randomizedDelaySec = mkOption {
-
type = types.int;
-
default = 1000;
-
description = "Run the maintenance job not exactly at the time specified with <literal>onCalendar</literal>, but plus or minus this many seconds.";
-
};
-
};
-
};
-
-
lmtpSaveToDetailMailbox = mkOption {
-
type = types.enum ["yes" "no"];
-
default = "yes";
-
description = ''
-
If an email address is delimited by a "+", should it be filed into a
-
mailbox matching the string after the "+"? For example,
-
user1+test@example.com would be filed into the mailbox "test".
-
'';
-
};
-
-
extraVirtualAliases = mkOption {
-
type = let
-
loginAccount = mkOptionType {
-
name = "Login Account";
-
check = (account: builtins.elem account (builtins.attrNames cfg.loginAccounts));
-
};
-
in with types; attrsOf (either loginAccount (nonEmptyListOf loginAccount));
-
example = {
-
"info@example.com" = "user1@example.com";
-
"postmaster@example.com" = "user1@example.com";
-
"abuse@example.com" = "user1@example.com";
-
"multi@example.com" = [ "user1@example.com" "user2@example.com" ];
-
};
-
description = ''
-
Virtual Aliases. A virtual alias `"info@example.com" = "user1@example.com"` means that
-
all mail to `info@example.com` is forwarded to `user1@example.com`. Note
-
that it is expected that `postmaster@example.com` and `abuse@example.com` is
-
forwarded to some valid email address. (Alternatively you can create login
-
accounts for `postmaster` and (or) `abuse`). Furthermore, it also allows
-
the user `user1@example.com` to send emails as `info@example.com`.
-
It's also possible to create an alias for multiple accounts. In this
-
example all mails for `multi@example.com` will be forwarded to both
-
`user1@example.com` and `user2@example.com`.
-
'';
-
default = {};
-
};
-
-
forwards = mkOption {
-
type = with types; attrsOf (either (listOf str) str);
-
example = {
-
"user@example.com" = "user@elsewhere.com";
-
};
-
description = ''
-
To forward mails to an external address. For instance,
-
the value {`"user@example.com" = "user@elsewhere.com";}`
-
means that mails to `user@example.com` are forwarded to
-
`user@elsewhere.com`. The difference with the
-
`extraVirtualAliases` option is that `user@elsewhere.com`
-
can't send mail as `user@example.com`. Also, this option
-
allows to forward mails to external addresses.
-
'';
-
default = {};
-
};
-
-
rejectSender = mkOption {
-
type = types.listOf types.str;
-
example = [ "@example.com" "spammer@example.net" ];
-
description = ''
-
Reject emails from these addresses from unauthorized senders.
-
Use if a spammer is using the same domain or the same sender over and over.
-
'';
-
default = [];
-
};
-
-
rejectRecipients = mkOption {
-
type = types.listOf types.str;
-
example = [ "sales@example.com" "info@example.com" ];
-
description = ''
-
Reject emails addressed to these local addresses from unauthorized senders.
-
Use if a spammer has found email addresses in a catchall domain but you do
-
not want to disable the catchall.
-
'';
-
default = [];
-
};
-
-
vmailUID = mkOption {
-
type = types.int;
-
default = 5000;
-
description = ''
-
The unix UID of the virtual mail user. Be mindful that if this is
-
changed, you will need to manually adjust the permissions of
-
mailDirectory.
-
'';
-
};
-
-
vmailUserName = mkOption {
-
type = types.str;
-
default = "virtualMail";
-
description = ''
-
The user name and group name of the user that owns the directory where all
-
the mail is stored.
-
'';
-
};
-
-
vmailGroupName = mkOption {
-
type = types.str;
-
default = "virtualMail";
-
description = ''
-
The user name and group name of the user that owns the directory where all
-
the mail is stored.
-
'';
-
};
-
-
mailDirectory = mkOption {
-
type = types.path;
-
default = "/var/vmail";
-
description = ''
-
Where to store the mail.
-
'';
-
};
-
-
useFsLayout = mkOption {
-
type = types.bool;
-
default = false;
-
description = ''
-
Sets whether dovecot should organize mail in subdirectories:
-
-
- /var/vmail/example.com/user/.folder.subfolder/ (default layout)
-
- /var/vmail/example.com/user/folder/subfolder/ (FS layout)
-
-
See https://wiki2.dovecot.org/MailboxFormat/Maildir for details.
-
'';
-
};
-
-
hierarchySeparator = mkOption {
-
type = types.str;
-
default = ".";
-
description = ''
-
The hierarchy separator for mailboxes used by dovecot for the namespace 'inbox'.
-
Dovecot defaults to "." but recommends "/".
-
This affects how mailboxes appear to mail clients and sieve scripts.
-
For instance when using "." then in a sieve script "example.com" would refer to the mailbox "com" in the parent mailbox "example".
-
This does not determine the way your mails are stored on disk.
-
See https://wiki.dovecot.org/Namespaces for details.
-
'';
-
};
-
-
mailboxes = mkOption {
-
description = ''
-
The mailboxes for dovecot.
-
Depending on the mail client used it might be necessary to change some mailbox's name.
-
'';
-
default = {
-
Trash = {
-
auto = "no";
-
specialUse = "Trash";
-
};
-
Junk = {
-
auto = "subscribe";
-
specialUse = "Junk";
-
};
-
Drafts = {
-
auto = "subscribe";
-
specialUse = "Drafts";
-
};
-
Sent = {
-
auto = "subscribe";
-
specialUse = "Sent";
-
};
-
};
-
};
-
-
certificateScheme = mkOption {
-
type = types.enum [ 1 2 3 ];
-
default = 2;
-
description = ''
-
Certificate Files. There are three options for these.
-
-
1) You specify locations and manually copy certificates there.
-
2) You let the server create new (self signed) certificates on the fly.
-
3) You let the server create a certificate via `Let's Encrypt`. Note that
-
this implies that a stripped down webserver has to be started. This also
-
implies that the FQDN must be set as an `A` record to point to the IP of
-
the server. In particular port 80 on the server will be opened. For details
-
on how to set up the domain records, see the guide in the readme.
-
'';
-
};
-
-
certificateFile = mkOption {
-
type = types.path;
-
example = "/root/mail-server.crt";
-
description = ''
-
Scheme 1)
-
Location of the certificate
-
'';
-
};
-
-
keyFile = mkOption {
-
type = types.path;
-
example = "/root/mail-server.key";
-
description = ''
-
Scheme 1)
-
Location of the key file
-
'';
-
};
-
-
certificateDirectory = mkOption {
-
type = types.path;
-
default = "/var/certs";
-
description = ''
-
Scheme 2)
-
This is the folder where the certificate will be created. The name is
-
hardcoded to "cert-DOMAIN.pem" and "key-DOMAIN.pem" and the
-
certificate is valid for 10 years.
-
'';
-
};
-
-
enableImap = mkOption {
-
type = types.bool;
-
default = true;
-
description = ''
-
Whether to enable IMAP with STARTTLS on port 143.
-
'';
-
};
-
-
enableImapSsl = mkOption {
-
type = types.bool;
-
default = true;
-
description = ''
-
Whether to enable IMAP with TLS in wrapper-mode on port 993.
-
'';
-
};
-
-
enableSubmission = mkOption {
-
type = types.bool;
-
default = true;
-
description = ''
-
Whether to enable SMTP with STARTTLS on port 587.
-
'';
-
};
-
-
enableSubmissionSsl = mkOption {
-
type = types.bool;
-
default = true;
-
description = ''
-
Whether to enable SMTP with TLS in wrapper-mode on port 465.
-
'';
-
};
-
-
enablePop3 = mkOption {
-
type = types.bool;
-
default = false;
-
description = ''
-
Whether to enable POP3 with STARTTLS on port on port 110.
-
'';
-
};
-
-
enablePop3Ssl = mkOption {
-
type = types.bool;
-
default = false;
-
description = ''
-
Whether to enable POP3 with TLS in wrapper-mode on port 995.
-
'';
-
};
-
-
enableManageSieve = mkOption {
-
type = types.bool;
-
default = false;
-
description = ''
-
Whether to enable ManageSieve, setting this option to true will open
-
port 4190 in the firewall.
-
-
The ManageSieve protocol allows users to manage their Sieve scripts on
-
a remote server with a supported client, including Thunderbird.
-
'';
-
};
-
-
sieveDirectory = mkOption {
-
type = types.path;
-
default = "/var/sieve";
-
description = ''
-
Where to store the sieve scripts.
-
'';
-
};
-
-
virusScanning = mkOption {
-
type = types.bool;
-
default = false;
-
description = ''
-
Whether to activate virus scanning. Note that virus scanning is _very_
-
expensive memory wise.
-
'';
-
};
-
-
dkimSigning = mkOption {
-
type = types.bool;
-
default = true;
-
description = ''
-
Whether to activate dkim signing.
-
'';
-
};
-
-
dkimSelector = mkOption {
-
type = types.str;
-
default = "mail";
-
description = ''
-
-
'';
-
};
-
-
dkimKeyDirectory = mkOption {
-
type = types.path;
-
default = "/var/dkim";
-
description = ''
-
-
'';
-
};
-
-
dkimKeyBits = mkOption {
-
type = types.int;
-
default = 1024;
-
description = ''
-
How many bits in generated DKIM keys. RFC6376 advises minimum 1024-bit keys.
-
-
If you have already deployed a key with a different number of bits than specified
-
here, then you should use a different selector (dkimSelector). In order to get
-
this package to generate a key with the new number of bits, you will either have to
-
change the selector or delete the old key file.
-
'';
-
};
-
-
dkimHeaderCanonicalization = mkOption {
-
type = types.enum ["relaxed" "simple"];
-
default = "relaxed";
-
description = ''
-
DKIM canonicalization algorithm for message headers.
-
-
See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details.
-
'';
-
};
-
-
dkimBodyCanonicalization = mkOption {
-
type = types.enum ["relaxed" "simple"];
-
default = "relaxed";
-
description = ''
-
DKIM canonicalization algorithm for message bodies.
-
-
See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details.
-
'';
-
};
-
-
debug = mkOption {
-
type = types.bool;
-
default = false;
-
description = ''
-
Whether to enable verbose logging for mailserver related services. This
-
intended be used for development purposes only, you probably don't want
-
to enable this unless you're hacking on nixos-mailserver.
-
'';
-
};
-
-
maxConnectionsPerUser = mkOption {
-
type = types.int;
-
default = 100;
-
description = ''
-
Maximum number of IMAP/POP3 connections allowed for a user from each IP address.
-
E.g. a value of 50 allows for 50 IMAP and 50 POP3 connections at the same
-
time for a single user.
-
'';
-
};
-
-
localDnsResolver = mkOption {
-
type = types.bool;
-
default = true;
-
description = ''
-
Runs a local DNS resolver (kresd) as recommended when running rspamd. This prevents your log file from filling up with rspamd_monitored_dns_mon entries.
-
'';
-
};
-
-
recipientDelimiter = mkOption {
-
type = types.str;
-
default = "+";
-
description = ''
-
Configure the recipient delimiter.
-
'';
-
};
-
-
redis = {
-
address = mkOption {
-
type = types.str;
-
# read the default from nixos' redis module
-
default = let
-
cf = config.services.redis.servers.rspamd.bind;
-
cfdefault = if cf == null then "127.0.0.1" else cf;
-
ips = lib.strings.splitString " " cfdefault;
-
ip = lib.lists.head (ips ++ [ "127.0.0.1" ]);
-
isIpv6 = ip: lib.lists.elem ":" (lib.stringToCharacters ip);
-
in
-
if (ip == "0.0.0.0" || ip == "::")
-
then "127.0.0.1"
-
else if isIpv6 ip then "[${ip}]" else ip;
-
defaultText = lib.literalDocBook "computed from <option>config.services.redis.servers.rspamd.bind</option>";
-
description = ''
-
Address that rspamd should use to contact redis.
-
'';
-
};
-
-
port = mkOption {
-
type = types.port;
-
default = config.services.redis.servers.rspamd.port;
-
defaultText = lib.literalExpression "config.services.redis.servers.rspamd.port";
-
description = ''
-
Port that rspamd should use to contact redis.
-
'';
-
};
-
-
password = mkOption {
-
type = types.nullOr types.str;
-
default = config.services.redis.servers.rspamd.requirePass;
-
defaultText = lib.literalExpression "config.services.redis.servers.rspamd.requirePass";
-
description = ''
-
Password that rspamd should use to contact redis, or null if not required.
-
'';
-
};
-
};
-
-
rewriteMessageId = mkOption {
-
type = types.bool;
-
default = false;
-
description = ''
-
Rewrites the Message-ID's hostname-part of outgoing emails to the FQDN.
-
Please be aware that this may cause problems with some mail clients
-
relying on the original Message-ID.
-
'';
-
};
-
-
sendingFqdn = mkOption {
-
type = types.str;
-
default = cfg.fqdn;
-
defaultText = "config.mailserver.fqdn";
-
example = "myserver.example.com";
-
description = ''
-
The fully qualified domain name of the mail server used to
-
identify with remote servers.
-
-
If this server's IP serves purposes other than a mail server,
-
it may be desirable for the server to have a name other than
-
that to which the user will connect. For example, the user
-
might connect to mx.example.com, but the server's IP has
-
reverse DNS that resolves to myserver.example.com; in this
-
scenario, some mail servers may reject or penalize the
-
message.
-
-
This setting allows the server to identify as
-
myserver.example.com when forwarding mail, independently of
-
`fqdn` (which, for SSL reasons, should generally be the name
-
to which the user connects).
-
-
Set this to the name to which the sending IP's reverse DNS
-
resolves.
-
'';
-
};
-
-
policydSPFExtraConfig = mkOption {
-
type = types.lines;
-
default = "";
-
example = ''
-
skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1
-
'';
-
description = ''
-
Extra configuration options for policyd-spf. This can be use to among
-
other things skip spf checking for some IP addresses.
-
'';
-
};
-
-
monitoring = {
-
enable = lib.mkEnableOption "monitoring via monit";
-
-
alertAddress = mkOption {
-
type = types.str;
-
description = ''
-
The email address to send alerts to.
-
'';
-
};
-
-
config = mkOption {
-
type = types.str;
-
default = ''
-
set daemon 120 with start delay 60
-
set mailserver
-
localhost
-
-
set httpd port 2812 and use address localhost
-
allow localhost
-
allow admin:obwjoawijerfoijsiwfj29jf2f2jd
-
-
check filesystem root with path /
-
if space usage > 80% then alert
-
if inode usage > 80% then alert
-
-
check system $HOST
-
if cpu usage > 95% for 10 cycles then alert
-
if memory usage > 75% for 5 cycles then alert
-
if swap usage > 20% for 10 cycles then alert
-
if loadavg (1min) > 90 for 15 cycles then alert
-
if loadavg (5min) > 80 for 10 cycles then alert
-
if loadavg (15min) > 70 for 8 cycles then alert
-
-
check process sshd with pidfile /var/run/sshd.pid
-
start program "${pkgs.systemd}/bin/systemctl start sshd"
-
stop program "${pkgs.systemd}/bin/systemctl stop sshd"
-
if failed port 22 protocol ssh for 2 cycles then restart
-
-
check process postfix with pidfile /var/lib/postfix/queue/pid/master.pid
-
start program = "${pkgs.systemd}/bin/systemctl start postfix"
-
stop program = "${pkgs.systemd}/bin/systemctl stop postfix"
-
if failed port 25 protocol smtp for 5 cycles then restart
-
-
check process dovecot with pidfile /var/run/dovecot2/master.pid
-
start program = "${pkgs.systemd}/bin/systemctl start dovecot2"
-
stop program = "${pkgs.systemd}/bin/systemctl stop dovecot2"
-
if failed host ${cfg.fqdn} port 993 type tcpssl sslauto protocol imap for 5 cycles then restart
-
-
check process rspamd with matching "rspamd: main process"
-
start program = "${pkgs.systemd}/bin/systemctl start rspamd"
-
stop program = "${pkgs.systemd}/bin/systemctl stop rspamd"
-
'';
-
defaultText = lib.literalDocBook "see source";
-
description = ''
-
The configuration used for monitoring via monit.
-
Use a mail address that you actively check and set it via 'set alert ...'.
-
'';
-
};
-
};
-
-
borgbackup = {
-
enable = lib.mkEnableOption "backup via borgbackup";
-
-
repoLocation = mkOption {
-
type = types.str;
-
default = "/var/borgbackup";
-
description = ''
-
The location where borg saves the backups.
-
This can be a local path or a remote location such as user@host:/path/to/repo.
-
It is exported and thus available as an environment variable to cmdPreexec and cmdPostexec.
-
'';
-
};
-
-
startAt = mkOption {
-
type = types.str;
-
default = "hourly";
-
description = "When or how often the backup should run. Must be in the format described in systemd.time 7.";
-
};
-
-
user = mkOption {
-
type = types.str;
-
default = "virtualMail";
-
description = "The user borg and its launch script is run as.";
-
};
-
-
group = mkOption {
-
type = types.str;
-
default = "virtualMail";
-
description = "The group borg and its launch script is run as.";
-
};
-
-
compression = {
-
method = mkOption {
-
type = types.nullOr (types.enum ["none" "lz4" "zstd" "zlib" "lzma"]);
-
default = null;
-
description = "Leaving this unset allows borg to choose. The default for borg 1.1.4 is lz4.";
-
};
-
-
level = mkOption {
-
type = types.nullOr types.int;
-
default = null;
-
description = ''
-
Denotes the level of compression used by borg.
-
Most methods accept levels from 0 to 9 but zstd which accepts values from 1 to 22.
-
If null the decision is left up to borg.
-
'';
-
};
-
-
auto = mkOption {
-
type = types.bool;
-
default = false;
-
description = "Leaves it to borg to determine whether an individual file should be compressed.";
-
};
-
};
-
-
encryption = {
-
method = mkOption {
-
type = types.enum [
-
"none"
-
"authenticated"
-
"authenticated-blake2"
-
"repokey"
-
"keyfile"
-
"repokey-blake2"
-
"keyfile-blake2"
-
];
-
default = "none";
-
description = ''
-
The backup can be encrypted by choosing any other value than 'none'.
-
When using encryption the password / passphrase must be provided in passphraseFile.
-
'';
-
};
-
-
passphraseFile = mkOption {
-
type = types.nullOr types.path;
-
default = null;
-
description = "Path to a file containing the encryption password or passphrase.";
-
};
-
};
-
-
name = mkOption {
-
type = types.str;
-
default = "{hostname}-{user}-{now}";
-
description = ''
-
The name of the individual backups as used by borg.
-
Certain placeholders will be replaced by borg.
-
'';
-
};
-
-
locations = mkOption {
-
type = types.listOf types.path;
-
default = [cfg.mailDirectory];
-
description = "The locations that are to be backed up by borg.";
-
};
-
-
extraArgumentsForInit = mkOption {
-
type = types.listOf types.str;
-
default = ["--critical"];
-
description = "Additional arguments to add to the borg init command line.";
-
};
-
-
extraArgumentsForCreate = mkOption {
-
type = types.listOf types.str;
-
default = [ ];
-
description = "Additional arguments to add to the borg create command line e.g. '--stats'.";
-
};
-
-
cmdPreexec = mkOption {
-
type = types.nullOr types.str;
-
default = null;
-
description = ''
-
The command to be executed before each backup operation.
-
This is called prior to borg init in the same script that runs borg init and create and cmdPostexec.
-
Example:
-
export BORG_RSH="ssh -i /path/to/private/key"
-
'';
-
};
-
-
cmdPostexec = mkOption {
-
type = types.nullOr types.str;
-
default = null;
-
description = ''
-
The command to be executed after each backup operation.
-
This is called after borg create completed successfully and in the same script that runs
-
cmdPreexec, borg init and create.
-
'';
-
};
-
-
};
-
-
rebootAfterKernelUpgrade = {
-
enable = mkOption {
-
type = types.bool;
-
default = false;
-
example = true;
-
description = ''
-
Whether to enable automatic reboot after kernel upgrades.
-
This is to be used in conjunction with system.autoUpgrade.enable = true"
-
'';
-
};
-
method = mkOption {
-
type = types.enum [ "reboot" "systemctl kexec" ];
-
default = "reboot";
-
description = ''
-
Whether to issue a full "reboot" or just a "systemctl kexec"-only reboot.
-
It is recommended to use the default value because the quicker kexec reboot has a number of problems.
-
Also if your server is running in a virtual machine the regular reboot will already be very quick.
-
'';
-
};
-
};
-
-
backup = {
-
enable = lib.mkEnableOption "backup via rsnapshot";
-
-
snapshotRoot = mkOption {
-
type = types.path;
-
default = "/var/rsnapshot";
-
description = ''
-
The directory where rsnapshot stores the backup.
-
'';
-
};
-
-
cmdPreexec = mkOption {
-
type = types.nullOr types.str;
-
default = null;
-
description = ''
-
The command to be executed before each backup operation. This is wrapped in a shell script to be called by rsnapshot.
-
'';
-
};
-
-
cmdPostexec = mkOption {
-
type = types.nullOr types.str;
-
default = null;
-
description = "The command to be executed after each backup operation. This is wrapped in a shell script to be called by rsnapshot.";
-
};
-
-
retain = {
-
hourly = mkOption {
-
type = types.int;
-
default = 24;
-
description = "How many hourly snapshots are retained.";
-
};
-
daily = mkOption {
-
type = types.int;
-
default = 7;
-
description = "How many daily snapshots are retained.";
-
};
-
weekly = mkOption {
-
type = types.int;
-
default = 54;
-
description = "How many weekly snapshots are retained.";
-
};
-
};
-
-
cronIntervals = mkOption {
-
type = types.attrsOf types.str;
-
default = {
-
# minute, hour, day-in-month, month, weekday (0 = sunday)
-
hourly = " 0 * * * *"; # Every full hour
-
daily = "30 3 * * *"; # Every day at 3:30
-
weekly = " 0 5 * * 0"; # Every sunday at 5:00 AM
-
};
-
description = ''
-
Periodicity at which intervals should be run by cron.
-
Note that the intervals also have to exist in configuration
-
as retain options.
-
'';
-
};
-
};
-
};
-
-
imports = [
-
./borgbackup.nix
-
./debug.nix
-
./rsnapshot.nix
-
./clamav.nix
-
./monit.nix
-
./users.nix
-
./environment.nix
-
./networking.nix
-
./systemd.nix
-
./dovecot.nix
-
./opendkim.nix
-
./postfix.nix
-
./rspamd.nix
-
./nginx.nix
-
./kresd.nix
-
./post-upgrade-check.nix
-
];
-
}
-15
modules/mailserver/dovecot/imap_sieve/report-ham.sieve
···
-
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
-
-
if environment :matches "imap.mailbox" "*" {
-
set "mailbox" "${1}";
-
}
-
-
if string "${mailbox}" "Trash" {
-
stop;
-
}
-
-
if environment :matches "imap.user" "*" {
-
set "username" "${1}";
-
}
-
-
pipe :copy "sa-learn-ham.sh" [ "${username}" ];
-7
modules/mailserver/dovecot/imap_sieve/report-spam.sieve
···
-
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
-
-
if environment :matches "imap.user" "*" {
-
set "username" "${1}";
-
}
-
-
pipe :copy "sa-learn-spam.sh" [ "${username}" ];
-3
modules/mailserver/dovecot/pipe_bin/sa-learn-ham.sh
···
-
#!/bin/bash
-
set -o errexit
-
exec rspamc -h /run/rspamd/worker-controller.sock learn_ham
-3
modules/mailserver/dovecot/pipe_bin/sa-learn-spam.sh
···
-
#!/bin/bash
-
set -o errexit
-
exec rspamc -h /run/rspamd/worker-controller.sock learn_spam
-325
modules/mailserver/dovecot.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
with (import ./common.nix { inherit config pkgs lib; });
-
-
let
-
cfg = config.mailserver;
-
-
passwdDir = "/run/dovecot2";
-
passwdFile = "${passwdDir}/passwd";
-
-
bool2int = x: if x then "1" else "0";
-
-
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
-
-
# maildir in format "/${domain}/${user}"
-
dovecotMaildir =
-
"maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}"
-
+ (lib.optionalString (cfg.indexDir != null)
-
":INDEX=${cfg.indexDir}/%d/%n"
-
);
-
-
postfixCfg = config.services.postfix;
-
dovecot2Cfg = config.services.dovecot2;
-
-
stateDir = "/var/lib/dovecot";
-
-
pipeBin = pkgs.stdenv.mkDerivation {
-
name = "pipe_bin";
-
src = ./dovecot/pipe_bin;
-
buildInputs = with pkgs; [ makeWrapper coreutils bash rspamd ];
-
buildCommand = ''
-
mkdir -p $out/pipe/bin
-
cp $src/* $out/pipe/bin/
-
chmod a+x $out/pipe/bin/*
-
patchShebangs $out/pipe/bin
-
-
for file in $out/pipe/bin/*; do
-
wrapProgram $file \
-
--set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin"
-
done
-
'';
-
};
-
-
genPasswdScript = pkgs.writeScript "generate-password-file" ''
-
#!${pkgs.stdenv.shell}
-
-
set -euo pipefail
-
-
if (! test -d "${passwdDir}"); then
-
mkdir "${passwdDir}"
-
chmod 755 "${passwdDir}"
-
fi
-
-
for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do
-
if [ ! -f "$f" ]; then
-
echo "Expected password hash file $f does not exist!"
-
exit 1
-
fi
-
done
-
-
cat <<EOF > ${passwdFile}
-
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
-
let passwordHash = ''$(${pkgs.dovecot}/bin/doveadm pw -p "$(head -n 1 ${passwordFiles."${name}"})")''; in
-
"${name}:${passwordHash}:${builtins.toString cfg.vmailUID}:${builtins.toString cfg.vmailUID}::${cfg.mailDirectory}:/run/current-system/sw/bin/nologin:"
-
+ (if lib.isString value.quota
-
then "userdb_quota_rule=*:storage=${value.quota}"
-
else "")
-
) cfg.loginAccounts)}
-
EOF
-
-
chmod 600 ${passwdFile}
-
'';
-
-
junkMailboxes = builtins.attrNames (lib.filterAttrs (n: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes);
-
junkMailboxNumber = builtins.length junkMailboxes;
-
# The assertion garantees there is exactly one Junk mailbox.
-
junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else "";
-
-
in
-
{
-
config = with cfg; lib.mkIf enable {
-
assertions = [
-
{
-
assertion = junkMailboxNumber == 1;
-
message = "nixos-mailserver requires exactly one dovecot mailbox with the 'special use' flag set to 'Junk' (${builtins.toString junkMailboxNumber} have been found)";
-
}
-
];
-
-
services.dovecot2 = {
-
enable = true;
-
enableImap = enableImap || enableImapSsl;
-
enablePop3 = enablePop3 || enablePop3Ssl;
-
enablePAM = false;
-
enableQuota = true;
-
mailGroup = vmailGroupName;
-
mailUser = vmailUserName;
-
mailLocation = dovecotMaildir;
-
sslServerCert = certificatePath;
-
sslServerKey = keyPath;
-
enableLmtp = true;
-
modules = [ pkgs.dovecot_pigeonhole ] ++ (lib.optional cfg.fullTextSearch.enable pkgs.dovecot_fts_xapian );
-
mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ "fts" "fts_xapian" ];
-
protocols = lib.optional cfg.enableManageSieve "sieve";
-
-
sieveScripts = {
-
after = builtins.toFile "spam.sieve" ''
-
require "fileinto";
-
-
if header :is "X-Spam" "Yes" {
-
fileinto "${junkMailboxName}";
-
stop;
-
}
-
'';
-
};
-
-
mailboxes = cfg.mailboxes;
-
-
extraConfig = ''
-
#Extra Config
-
${lib.optionalString debug ''
-
mail_debug = yes
-
auth_debug = yes
-
verbose_ssl = yes
-
''}
-
-
${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) ''
-
service imap-login {
-
inet_listener imap {
-
${if cfg.enableImap then ''
-
port = 143
-
'' else ''
-
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
-
port = 0
-
''}
-
}
-
inet_listener imaps {
-
${if cfg.enableImapSsl then ''
-
port = 993
-
ssl = yes
-
'' else ''
-
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
-
port = 0
-
''}
-
}
-
}
-
''}
-
${lib.optionalString (cfg.enablePop3 || cfg.enablePop3Ssl) ''
-
service pop3-login {
-
inet_listener pop3 {
-
${if cfg.enablePop3 then ''
-
port = 110
-
'' else ''
-
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
-
port = 0
-
''}
-
}
-
inet_listener pop3s {
-
${if cfg.enablePop3Ssl then ''
-
port = 995
-
ssl = yes
-
'' else ''
-
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
-
port = 0
-
''}
-
}
-
}
-
''}
-
-
protocol imap {
-
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
-
mail_plugins = $mail_plugins imap_sieve
-
}
-
-
protocol pop3 {
-
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
-
}
-
-
mail_access_groups = ${vmailGroupName}
-
ssl = required
-
ssl_min_protocol = TLSv1.2
-
ssl_prefer_server_ciphers = yes
-
-
service lmtp {
-
unix_listener dovecot-lmtp {
-
group = ${postfixCfg.group}
-
mode = 0600
-
user = ${postfixCfg.user}
-
}
-
}
-
-
recipient_delimiter = ${cfg.recipientDelimiter}
-
lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox}
-
-
protocol lmtp {
-
mail_plugins = $mail_plugins sieve
-
}
-
-
passdb {
-
driver = passwd-file
-
args = ${passwdFile}
-
}
-
-
userdb {
-
driver = passwd-file
-
args = ${passwdFile}
-
}
-
-
service auth {
-
unix_listener auth {
-
mode = 0660
-
user = ${postfixCfg.user}
-
group = ${postfixCfg.group}
-
}
-
}
-
-
auth_mechanisms = plain login
-
-
namespace inbox {
-
separator = ${cfg.hierarchySeparator}
-
inbox = yes
-
}
-
-
plugin {
-
sieve_plugins = sieve_imapsieve sieve_extprograms
-
sieve = file:${cfg.sieveDirectory}/%u/scripts;active=${cfg.sieveDirectory}/%u/active.sieve
-
sieve_default = file:${cfg.sieveDirectory}/%u/default.sieve
-
sieve_default_name = default
-
-
# From elsewhere to Spam folder
-
imapsieve_mailbox1_name = ${junkMailboxName}
-
imapsieve_mailbox1_causes = COPY
-
imapsieve_mailbox1_before = file:${stateDir}/imap_sieve/report-spam.sieve
-
-
# From Spam folder to elsewhere
-
imapsieve_mailbox2_name = *
-
imapsieve_mailbox2_from = ${junkMailboxName}
-
imapsieve_mailbox2_causes = COPY
-
imapsieve_mailbox2_before = file:${stateDir}/imap_sieve/report-ham.sieve
-
-
sieve_pipe_bin_dir = ${pipeBin}/pipe/bin
-
-
sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
-
}
-
-
${lib.optionalString cfg.fullTextSearch.enable ''
-
plugin {
-
plugin = fts fts_xapian
-
fts = xapian
-
fts_xapian = partial=${toString cfg.fullTextSearch.minSize} full=${toString cfg.fullTextSearch.maxSize} attachments=${bool2int cfg.fullTextSearch.indexAttachments} verbose=${bool2int cfg.debug}
-
-
fts_autoindex = ${if cfg.fullTextSearch.autoIndex then "yes" else "no"}
-
-
${lib.strings.concatImapStringsSep "\n" (n: x: "fts_autoindex_exclude${if n==1 then "" else toString n} = ${x}") cfg.fullTextSearch.autoIndexExclude}
-
-
fts_enforced = ${cfg.fullTextSearch.enforced}
-
}
-
-
${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) ''
-
service indexer-worker {
-
vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit*1024*1024)}
-
}
-
''}
-
''}
-
-
lda_mailbox_autosubscribe = yes
-
lda_mailbox_autocreate = yes
-
'';
-
};
-
-
systemd.services.dovecot2 = {
-
preStart = ''
-
${genPasswdScript}
-
rm -rf '${stateDir}/imap_sieve'
-
mkdir '${stateDir}/imap_sieve'
-
cp -p "${./dovecot/imap_sieve}"/*.sieve '${stateDir}/imap_sieve/'
-
for k in "${stateDir}/imap_sieve"/*.sieve ; do
-
${pkgs.dovecot_pigeonhole}/bin/sievec "$k"
-
done
-
chown -R '${dovecot2Cfg.mailUser}:${dovecot2Cfg.mailGroup}' '${stateDir}/imap_sieve'
-
'';
-
};
-
-
systemd.services.postfix.restartTriggers = [ genPasswdScript ];
-
-
systemd.services.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable) {
-
description = "Optimize dovecot indices for fts_xapian";
-
requisite = [ "dovecot2.service" ];
-
after = [ "dovecot2.service" ];
-
startAt = cfg.fullTextSearch.maintenance.onCalendar;
-
serviceConfig = {
-
Type = "oneshot";
-
ExecStart = "${pkgs.dovecot}/bin/doveadm fts optimize -A";
-
PrivateDevices = true;
-
PrivateNetwork = true;
-
ProtectKernelTunables = true;
-
ProtectKernelModules = true;
-
ProtectControlGroups = true;
-
ProtectHome = true;
-
ProtectSystem = true;
-
PrivateTmp = true;
-
};
-
};
-
systemd.timers.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable && cfg.fullTextSearch.maintenance.randomizedDelaySec != 0) {
-
timerConfig = {
-
RandomizedDelaySec = cfg.fullTextSearch.maintenance.randomizedDelaySec;
-
};
-
};
-
};
-
}
-28
modules/mailserver/environment.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
let
-
cfg = config.mailserver;
-
in
-
{
-
config = with cfg; lib.mkIf enable {
-
environment.systemPackages = with pkgs; [
-
dovecot opendkim openssh postfix rspamd
-
] ++ (if certificateScheme == 2 then [ openssl ] else []);
-
};
-
}
-27
modules/mailserver/kresd.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2017 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
let
-
cfg = config.mailserver;
-
in
-
{
-
config = lib.mkIf (cfg.enable && cfg.localDnsResolver) {
-
services.kresd.enable = true;
-
};
-
}
-
-32
modules/mailserver/monit.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
let
-
cfg = config.mailserver;
-
in
-
{
-
config = lib.mkIf (cfg.enable && cfg.monitoring.enable) {
-
services.monit = {
-
enable = true;
-
config = ''
-
set alert ${cfg.monitoring.alertAddress}
-
${cfg.monitoring.config}
-
'';
-
};
-
};
-
}
-37
modules/mailserver/networking.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
let
-
cfg = config.mailserver;
-
in
-
{
-
config = with cfg; lib.mkIf (enable && openFirewall) {
-
-
networking.firewall = {
-
allowedTCPPorts = [ 25 ]
-
++ lib.optional enableSubmission 587
-
++ lib.optional enableSubmissionSsl 465
-
++ lib.optional enableImap 143
-
++ lib.optional enableImapSsl 993
-
++ lib.optional enablePop3 110
-
++ lib.optional enablePop3Ssl 995
-
++ lib.optional enableManageSieve 4190
-
++ lib.optional (certificateScheme == 3) 80;
-
};
-
};
-
}
-44
modules/mailserver/nginx.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
-
{ config, pkgs, lib, ... }:
-
-
with (import ./common.nix { inherit config; });
-
-
let
-
cfg = config.mailserver;
-
acmeRoot = "/var/lib/acme/acme-challenge";
-
in
-
{
-
config = lib.mkIf (cfg.enable && cfg.certificateScheme == 3) {
-
services.nginx = {
-
enable = true;
-
virtualHosts."${cfg.fqdn}" = {
-
serverName = cfg.fqdn;
-
serverAliases = cfg.certificateDomains;
-
forceSSL = true;
-
enableACME = true;
-
acmeRoot = acmeRoot;
-
};
-
};
-
-
security.acme.certs."${cfg.fqdn}".reloadServices = [
-
"postfix.service"
-
"dovecot2.service"
-
];
-
};
-
}
-88
modules/mailserver/opendkim.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2017 Brian Olsen
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
{ config, lib, pkgs, ... }:
-
-
with lib;
-
-
let
-
cfg = config.mailserver;
-
-
dkimUser = config.services.opendkim.user;
-
dkimGroup = config.services.opendkim.group;
-
-
createDomainDkimCert = dom:
-
let
-
dkim_key = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key";
-
dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt";
-
in
-
''
-
if [ ! -f "${dkim_key}" ]
-
then
-
${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \
-
-d "${dom}" \
-
--bits="${toString cfg.dkimKeyBits}" \
-
--directory="${cfg.dkimKeyDirectory}"
-
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}"
-
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}"
-
echo "Generated key for domain ${dom} selector ${cfg.dkimSelector}"
-
fi
-
'';
-
createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.domains);
-
-
keyTable = pkgs.writeText "opendkim-KeyTable"
-
(lib.concatStringsSep "\n" (lib.flip map cfg.domains
-
(dom: "${dom} ${dom}:${cfg.dkimSelector}:${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key")));
-
signingTable = pkgs.writeText "opendkim-SigningTable"
-
(lib.concatStringsSep "\n" (lib.flip map cfg.domains (dom: "${dom} ${dom}")));
-
-
dkim = config.services.opendkim;
-
args = [ "-f" "-l" ] ++ lib.optionals (dkim.configFile != null) [ "-x" dkim.configFile ];
-
in
-
{
-
config = mkIf (cfg.dkimSigning && cfg.enable) {
-
services.opendkim = {
-
enable = true;
-
selector = cfg.dkimSelector;
-
keyPath = cfg.dkimKeyDirectory;
-
domains = "csl:${builtins.concatStringsSep "," cfg.domains}";
-
configFile = pkgs.writeText "opendkim.conf" (''
-
Canonicalization ${cfg.dkimHeaderCanonicalization}/${cfg.dkimBodyCanonicalization}
-
UMask 0002
-
Socket ${dkim.socket}
-
KeyTable file:${keyTable}
-
SigningTable file:${signingTable}
-
'' + (lib.optionalString cfg.debug ''
-
Syslog yes
-
SyslogSuccess yes
-
LogWhy yes
-
''));
-
};
-
-
users.users = optionalAttrs (config.services.postfix.user == "postfix") {
-
postfix.extraGroups = [ "${dkimGroup}" ];
-
};
-
systemd.services.opendkim = {
-
preStart = lib.mkForce createAllCerts;
-
serviceConfig = {
-
ExecStart = lib.mkForce "${pkgs.opendkim}/bin/opendkim ${escapeShellArgs args}";
-
PermissionsStartOnly = lib.mkForce false;
-
};
-
};
-
systemd.tmpfiles.rules = [
-
"d '${cfg.dkimKeyDirectory}' - ${dkimUser} ${dkimGroup} - -"
-
];
-
};
-
}
-46
modules/mailserver/post-upgrade-check.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
with lib;
-
-
let
-
cfg = config.mailserver;
-
in
-
{
-
config = mkIf (cfg.enable && cfg.rebootAfterKernelUpgrade.enable) {
-
systemd.services.nixos-upgrade.serviceConfig.ExecStartPost = pkgs.writeScript "post-upgrade-check" ''
-
#!${pkgs.stdenv.shell}
-
-
# Checks whether the "current" kernel is different from the booted kernel
-
# and then triggers a reboot so that the "current" kernel will be the booted one.
-
# This is just an educated guess. If the links do not differ the kernels might still be different, according to spacefrogg in #nixos.
-
-
current=$(readlink -f /run/current-system/kernel)
-
booted=$(readlink -f /run/booted-system/kernel)
-
-
if [ "$current" == "$booted" ]; then
-
echo "kernel version seems unchanged, skipping reboot" | systemd-cat --priority 4 --identifier "post-upgrade-check";
-
else
-
echo "kernel path changed, possibly a new version" | systemd-cat --priority 2 --identifier "post-upgrade-check"
-
echo "$booted" | systemd-cat --priority 2 --identifier "post-upgrade-kernel-check"
-
echo "$current" | systemd-cat --priority 2 --identifier "post-upgrade-kernel-check"
-
${cfg.rebootAfterKernelUpgrade.method}
-
fi
-
'';
-
};
-
}
-269
modules/mailserver/postfix.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
with (import ./common.nix { inherit config pkgs lib; });
-
-
let
-
inherit (lib.strings) concatStringsSep;
-
cfg = config.mailserver;
-
-
# Merge several lookup tables. A lookup table is a attribute set where
-
# - the key is an address (user@example.com) or a domain (@example.com)
-
# - the value is a list of addresses
-
mergeLookupTables = tables: lib.zipAttrsWith (n: v: lib.flatten v) tables;
-
-
# valiases_postfix :: Map String [String]
-
valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
-
(name: value:
-
let to = name;
-
in map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name))
-
cfg.loginAccounts));
-
-
# catchAllPostfix :: Map String [String]
-
catchAllPostfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
-
(name: value:
-
let to = name;
-
in map (from: {"@${from}" = to;}) value.catchAll)
-
cfg.loginAccounts));
-
-
# all_valiases_postfix :: Map String [String]
-
all_valiases_postfix = mergeLookupTables [valiases_postfix extra_valiases_postfix];
-
-
# attrsToLookupTable :: Map String (Either String [ String ]) -> Map String [String]
-
attrsToLookupTable = aliases: let
-
lookupTables = lib.mapAttrsToList (from: to: {"${from}" = to;}) aliases;
-
in mergeLookupTables lookupTables;
-
-
# extra_valiases_postfix :: Map String [String]
-
extra_valiases_postfix = attrsToLookupTable cfg.extraVirtualAliases;
-
-
# forwards :: Map String [String]
-
forwards = attrsToLookupTable cfg.forwards;
-
-
# lookupTableToString :: Map String [String] -> String
-
lookupTableToString = attrs: let
-
valueToString = value: lib.concatStringsSep ", " value;
-
in lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs);
-
-
# valiases_file :: Path
-
valiases_file = let
-
content = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix]);
-
in builtins.toFile "valias" content;
-
-
# denied_recipients_postfix :: [ String ]
-
denied_recipients_postfix = (map
-
(acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}")
-
(lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts)));
-
denied_recipients_file = builtins.toFile "denied_recipients" (lib.concatStringsSep "\n" denied_recipients_postfix);
-
-
reject_senders_postfix = (map
-
(sender:
-
"${sender} REJECT")
-
(cfg.rejectSender));
-
reject_senders_file = builtins.toFile "reject_senders" (lib.concatStringsSep "\n" (reject_senders_postfix)) ;
-
-
reject_recipients_postfix = (map
-
(recipient:
-
"${recipient} REJECT")
-
(cfg.rejectRecipients));
-
# rejectRecipients :: [ Path ]
-
reject_recipients_file = builtins.toFile "reject_recipients" (lib.concatStringsSep "\n" (reject_recipients_postfix)) ;
-
-
# vhosts_file :: Path
-
vhosts_file = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains);
-
-
# vaccounts_file :: Path
-
# 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 valiases,
-
# every alias is owned (uniquely) by its user.
-
# The user's own address is already in all_valiases_postfix.
-
vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix);
-
-
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (''
-
# Removes sensitive headers from mails handed in via the submission port.
-
# See https://thomas-leister.de/mailserver-debian-stretch/
-
# Uses "pcre" style regex.
-
-
/^Received:/ IGNORE
-
/^X-Originating-IP:/ IGNORE
-
/^X-Mailer:/ IGNORE
-
/^User-Agent:/ IGNORE
-
/^X-Enigmail:/ IGNORE
-
'' + lib.optionalString cfg.rewriteMessageId ''
-
-
# Replaces the user submitted hostname with the server's FQDN to hide the
-
# user's host or network.
-
-
/^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}>
-
'');
-
-
inetSocket = addr: port: "inet:[${toString port}@${addr}]";
-
unixSocket = sock: "unix:${sock}";
-
-
smtpdMilters =
-
(lib.optional cfg.dkimSigning "unix:/run/opendkim/opendkim.sock")
-
++ [ "unix:/run/rspamd/rspamd-milter.sock" ];
-
-
policyd-spf = pkgs.writeText "policyd-spf.conf" cfg.policydSPFExtraConfig;
-
-
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
-
-
submissionOptions =
-
{
-
smtpd_tls_security_level = "encrypt";
-
smtpd_sasl_auth_enable = "yes";
-
smtpd_sasl_type = "dovecot";
-
smtpd_sasl_path = "/run/dovecot2/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";
-
};
-
in
-
{
-
config = with cfg; lib.mkIf enable {
-
-
services.postfix = {
-
enable = true;
-
hostname = "${sendingFqdn}";
-
networksStyle = "host";
-
mapFiles."valias" = valiases_file;
-
mapFiles."vaccounts" = vaccounts_file;
-
mapFiles."denied_recipients" = denied_recipients_file;
-
mapFiles."reject_senders" = reject_senders_file;
-
mapFiles."reject_recipients" = reject_recipients_file;
-
sslCert = certificatePath;
-
sslKey = keyPath;
-
enableSubmission = cfg.enableSubmission;
-
enableSubmissions = cfg.enableSubmissionSsl;
-
virtual = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix forwards]);
-
-
config = {
-
# Extra Config
-
mydestination = "";
-
recipient_delimiter = cfg.recipientDelimiter;
-
smtpd_banner = "${fqdn} ESMTP NO UCE";
-
disable_vrfy_command = true;
-
message_size_limit = toString cfg.messageSizeLimit;
-
-
# virtual mail system
-
virtual_uid_maps = "static:5000";
-
virtual_gid_maps = "static:5000";
-
virtual_mailbox_base = mailDirectory;
-
virtual_mailbox_domains = vhosts_file;
-
virtual_mailbox_maps = mappedFile "valias";
-
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
-
# Avoid leakage of X-Original-To, X-Delivered-To headers between recipients
-
lmtp_destination_recipient_limit = "1";
-
-
# sasl with dovecot
-
smtpd_sasl_type = "dovecot";
-
smtpd_sasl_path = "/run/dovecot2/auth";
-
smtpd_sasl_auth_enable = true;
-
smtpd_relay_restrictions = [
-
"permit_mynetworks" "permit_sasl_authenticated" "reject_unauth_destination"
-
];
-
-
policy-spf_time_limit = "3600s";
-
-
# reject selected senders
-
smtpd_sender_restrictions = [
-
"check_sender_access ${mappedFile "reject_senders"}"
-
];
-
-
# quota and spf checking
-
smtpd_recipient_restrictions = [
-
"check_recipient_access ${mappedFile "denied_recipients"}"
-
"check_recipient_access ${mappedFile "reject_recipients"}"
-
"check_policy_service inet:localhost:12340"
-
"check_policy_service unix:private/policy-spf"
-
];
-
-
# TLS settings, inspired by https://github.com/jeaye/nix-files
-
# Submission by mail clients is handled in submissionOptions
-
smtpd_tls_security_level = "may";
-
-
# strong might suffice and is computationally less expensive
-
smtpd_tls_eecdh_grade = "ultra";
-
-
# Disable obselete protocols
-
smtpd_tls_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
-
smtp_tls_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
-
smtpd_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
-
smtp_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
-
-
smtp_tls_ciphers = "high";
-
smtpd_tls_ciphers = "high";
-
smtp_tls_mandatory_ciphers = "high";
-
smtpd_tls_mandatory_ciphers = "high";
-
-
# Disable deprecated ciphers
-
smtpd_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
-
smtpd_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
-
smtp_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
-
smtp_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
-
-
tls_preempt_cipherlist = true;
-
-
# Allowing AUTH on a non encrypted connection poses a security risk
-
smtpd_tls_auth_only = true;
-
# Log only a summary message on TLS handshake completion
-
smtpd_tls_loglevel = "1";
-
-
# Configure a non blocking source of randomness
-
tls_random_source = "dev:/dev/urandom";
-
-
smtpd_milters = smtpdMilters;
-
non_smtpd_milters = lib.mkIf cfg.dkimSigning ["unix:/run/opendkim/opendkim.sock"];
-
milter_protocol = "6";
-
milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}";
-
-
};
-
-
submissionOptions = submissionOptions;
-
submissionsOptions = submissionOptions;
-
-
masterConfig = {
-
"lmtp" = {
-
# Add headers when delivering, see http://www.postfix.org/smtp.8.html
-
# D => Delivered-To, O => X-Original-To, R => Return-Path
-
args = [ "flags=O" ];
-
};
-
"policy-spf" = {
-
type = "unix";
-
privileged = true;
-
chroot = false;
-
command = "spawn";
-
args = [ "user=nobody" "argv=${pkgs.pypolicyd-spf}/bin/policyd-spf" "${policyd-spf}"];
-
};
-
"submission-header-cleanup" = {
-
type = "unix";
-
private = false;
-
chroot = false;
-
maxproc = 0;
-
command = "cleanup";
-
args = ["-o" "header_checks=pcre:${submissionHeaderCleanupRules}"];
-
};
-
};
-
};
-
};
-
}
-59
modules/mailserver/rsnapshot.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
with lib;
-
-
let
-
cfg = config.mailserver;
-
-
preexecDefined = cfg.backup.cmdPreexec != null;
-
preexecWrapped = pkgs.writeScript "rsnapshot-preexec.sh" ''
-
#!${pkgs.stdenv.shell}
-
set -e
-
-
${cfg.backup.cmdPreexec}
-
'';
-
preexecString = optionalString preexecDefined "cmd_preexec ${preexecWrapped}";
-
-
postexecDefined = cfg.backup.cmdPostexec != null;
-
postexecWrapped = pkgs.writeScript "rsnapshot-postexec.sh" ''
-
#!${pkgs.stdenv.shell}
-
set -e
-
-
${cfg.backup.cmdPostexec}
-
'';
-
postexecString = optionalString postexecDefined "cmd_postexec ${postexecWrapped}";
-
in {
-
config = mkIf (cfg.enable && cfg.backup.enable) {
-
services.rsnapshot = {
-
enable = true;
-
cronIntervals = cfg.backup.cronIntervals;
-
# rsnapshot expects intervals shortest first, e.g. hourly first, then daily.
-
# tabs must separate all elements
-
extraConfig = ''
-
${preexecString}
-
${postexecString}
-
snapshot_root ${cfg.backup.snapshotRoot}/
-
retain hourly ${toString cfg.backup.retain.hourly}
-
retain daily ${toString cfg.backup.retain.daily}
-
retain weekly ${toString cfg.backup.retain.weekly}
-
backup ${cfg.mailDirectory}/ localhost/
-
'';
-
};
-
};
-
}
-119
modules/mailserver/rspamd.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
let
-
cfg = config.mailserver;
-
-
postfixCfg = config.services.postfix;
-
rspamdCfg = config.services.rspamd;
-
rspamdSocket = "rspamd.service";
-
in
-
{
-
config = with cfg; lib.mkIf enable {
-
services.rspamd = {
-
enable = true;
-
inherit debug;
-
locals = {
-
"milter_headers.conf" = { text = ''
-
extended_spam_headers = yes;
-
''; };
-
"redis.conf" = { text = ''
-
servers = "${cfg.redis.address}:${toString cfg.redis.port}";
-
'' + (lib.optionalString (cfg.redis.password != null) ''
-
password = "${cfg.redis.password}";
-
''); };
-
"classifier-bayes.conf" = { text = ''
-
cache {
-
backend = "redis";
-
}
-
''; };
-
"antivirus.conf" = lib.mkIf cfg.virusScanning { text = ''
-
clamav {
-
action = "reject";
-
symbol = "CLAM_VIRUS";
-
type = "clamav";
-
log_clean = true;
-
servers = "/run/clamav/clamd.ctl";
-
scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all
-
}
-
''; };
-
"dkim_signing.conf" = { text = ''
-
# Disable outbound email signing, we use opendkim for this
-
enabled = false;
-
''; };
-
};
-
-
overrides = {
-
"milter_headers.conf" = {
-
text = ''
-
extended_spam_headers = true;
-
'';
-
};
-
};
-
-
workers.rspamd_proxy = {
-
type = "rspamd_proxy";
-
bindSockets = [{
-
socket = "/run/rspamd/rspamd-milter.sock";
-
mode = "0664";
-
}];
-
count = 1; # Do not spawn too many processes of this type
-
extraConfig = ''
-
milter = yes; # Enable milter mode
-
timeout = 120s; # Needed for Milter usually
-
-
upstream "local" {
-
default = yes; # Self-scan upstreams are always default
-
self_scan = yes; # Enable self-scan
-
}
-
'';
-
};
-
workers.controller = {
-
type = "controller";
-
count = 1;
-
bindSockets = [{
-
socket = "/run/rspamd/worker-controller.sock";
-
mode = "0666";
-
}];
-
includes = [];
-
extraConfig = ''
-
static_dir = "''${WWWDIR}"; # Serve the web UI static assets
-
'';
-
};
-
-
};
-
-
services.redis.servers.rspamd = {
-
enable = lib.mkDefault true;
-
port = lib.mkDefault 6380;
-
};
-
-
systemd.services.rspamd = {
-
requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
-
after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
-
};
-
-
systemd.services.postfix = {
-
after = [ rspamdSocket ];
-
requires = [ rspamdSocket ];
-
};
-
-
users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
-
};
-
}
-
-83
modules/mailserver/systemd.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
let
-
cfg = config.mailserver;
-
certificatesDeps =
-
if cfg.certificateScheme == 1 then
-
[]
-
else if cfg.certificateScheme == 2 then
-
[ "mailserver-selfsigned-certificate.service" ]
-
else
-
[ "acme-finished-${cfg.fqdn}.target" ];
-
in
-
{
-
config = with cfg; lib.mkIf enable {
-
# Create self signed certificate
-
systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == 2) {
-
after = [ "local-fs.target" ];
-
script = ''
-
# Create certificates if they do not exist yet
-
dir="${cfg.certificateDirectory}"
-
fqdn="${cfg.fqdn}"
-
[[ $fqdn == /* ]] && fqdn=$(< "$fqdn")
-
key="$dir/key-${cfg.fqdn}.pem";
-
cert="$dir/cert-${cfg.fqdn}.pem";
-
-
if [[ ! -f $key || ! -f $cert ]]; then
-
mkdir -p "${cfg.certificateDirectory}"
-
(umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "$key" 2048) &&
-
"${pkgs.openssl}/bin/openssl" req -new -key "$key" -x509 -subj "/CN=$fqdn" \
-
-days 3650 -out "$cert"
-
fi
-
'';
-
serviceConfig = {
-
Type = "oneshot";
-
PrivateTmp = true;
-
};
-
};
-
-
# Create maildir folder before dovecot startup
-
systemd.services.dovecot2 = {
-
wants = certificatesDeps;
-
after = certificatesDeps;
-
preStart = let
-
directories = lib.strings.escapeShellArgs (
-
[ mailDirectory ]
-
++ lib.optional (cfg.indexDir != null) cfg.indexDir
-
);
-
in ''
-
# Create mail directory and set permissions. See
-
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>.
-
mkdir -p ${directories}
-
chgrp "${vmailGroupName}" ${directories}
-
chmod 02770 ${directories}
-
'';
-
};
-
-
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
-
systemd.services.postfix = {
-
wants = certificatesDeps;
-
after = [ "dovecot2.service" ]
-
++ lib.optional cfg.dkimSigning "opendkim.service"
-
++ certificatesDeps;
-
requires = [ "dovecot2.service" ]
-
++ lib.optional cfg.dkimSigning "opendkim.service";
-
};
-
};
-
}
-101
modules/mailserver/users.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
with config.mailserver;
-
-
let
-
vmail_user = {
-
name = vmailUserName;
-
isSystemUser = true;
-
uid = vmailUID;
-
home = mailDirectory;
-
createHome = true;
-
group = vmailGroupName;
-
};
-
-
-
virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" ''
-
#!${pkgs.stdenv.shell}
-
-
set -euo pipefail
-
-
# Create directory to store user sieve scripts if it doesn't exist
-
if (! test -d "${sieveDirectory}"); then
-
mkdir "${sieveDirectory}"
-
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}"
-
chmod 770 "${sieveDirectory}"
-
fi
-
-
# Copy user's sieve script to the correct location (if it exists). If it
-
# is null, remove the file.
-
${lib.concatMapStringsSep "\n" ({ name, sieveScript }:
-
if lib.isString sieveScript then ''
-
if (! test -d "${sieveDirectory}/${name}"); then
-
mkdir -p "${sieveDirectory}/${name}"
-
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}"
-
chmod 770 "${sieveDirectory}/${name}"
-
fi
-
cat << 'EOF' > "${sieveDirectory}/${name}/default.sieve"
-
${sieveScript}
-
EOF
-
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve"
-
'' else ''
-
if (test -f "${sieveDirectory}/${name}/default.sieve"); then
-
rm "${sieveDirectory}/${name}/default.sieve"
-
fi
-
if (test -f "${sieveDirectory}/${name}.svbin"); then
-
rm "${sieveDirectory}/${name}/default.svbin"
-
fi
-
'') (map (user: { inherit (user) name sieveScript; })
-
(lib.attrValues loginAccounts))}
-
'';
-
in {
-
config = lib.mkIf enable {
-
# assert that all accounts provide a password
-
assertions = (map (acct: {
-
assertion = (acct.password != null || acct.passwordFile != null);
-
message = "${acct.name} must provide either a password or a password file";
-
}) (lib.attrValues loginAccounts));
-
-
# warn for accounts that specify both password and file
-
warnings = (map
-
(acct: "${acct.name} specifies both a password and password file; password file will be used")
-
(lib.filter
-
(acct: (acct.password != null && acct.passwordFile != null))
-
(lib.attrValues loginAccounts)));
-
-
# set the vmail gid to a specific value
-
users.groups = {
-
"${vmailGroupName}" = { gid = vmailUID; };
-
};
-
-
# define all users
-
users.users = {
-
"${vmail_user.name}" = lib.mkForce vmail_user;
-
};
-
-
systemd.services.activate-virtual-mail-users = {
-
wantedBy = [ "multi-user.target" ];
-
before = [ "dovecot2.service" ];
-
serviceConfig = {
-
ExecStart = virtualMailUsersActivationScript;
-
};
-
enable = true;
-
};
-
};
-
}
+39 -14
modules/mailserver.nix
···
let
cfg = config.eilean;
domain = config.networking.domain;
+
subdomain = "mail.${domain}";
in {
options.eilean.mailserver = {
enable = mkEnableOption "mailserver";
···
};
config = mkIf cfg.mailserver.enable {
+
security.acme-eon.certs."${subdomain}" = lib.mkIf cfg.acme-eon {
+
group = "turnserver";
+
reloadServices = [ "postfix.service" "dovecot.service" ];
+
};
+
mailserver = {
enable = true;
-
fqdn = "mail.${domain}";
+
fqdn = subdomain;
domains = [ "${domain}" ];
loginAccounts = {
···
# Use Let's Encrypt certificates. Note that this needs to set up a stripped
# down nginx and opens port 80.
-
certificateScheme = 3;
-
+
certificateScheme = if cfg.acme-eon then "manual" else "acme-nginx";
+
certificateFile = lib.mkIf cfg.acme-eon "${
+
config.security.acme-eon.certs.${subdomain}.directory
+
}/fullchain.pem";
+
keyFile = lib.mkIf cfg.acme-eon
+
"${config.security.acme-eon.certs.${subdomain}.directory}/key.pem";
localDnsResolver = false;
};
···
return 301 $scheme://${domain}$request_uri;
'';
+
systemd.services.dovecot2 = lib.mkIf cfg.acme-eon {
+
wants = [ "acme-eon-${subdomain}.service" ];
+
after = [ "acme-eon-${subdomain}.service" ];
+
};
+
+
systemd.services.postfix = lib.mkIf cfg.acme-eon {
+
wants = [ "acme-eon-${subdomain}.service" ];
+
after = [ "acme-eon-${subdomain}.service" ];
+
};
+
+
services.postfix.config = {
+
smtpd_tls_protocols =
+
mkForce "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
+
smtp_tls_protocols =
+
mkForce "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
+
smtpd_tls_mandatory_protocols =
+
mkForce "TLSv1.3, !TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
+
smtp_tls_mandatory_protocols =
+
mkForce "TLSv1.3, !TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
+
};
+
eilean.dns.enable = true;
eilean.services.dns.zones.${config.networking.domain}.records = [
{
name = "mail";
type = "A";
-
data = cfg.serverIpv4;
+
value = cfg.serverIpv4;
}
{
name = "mail";
type = "AAAA";
-
data = cfg.serverIpv6;
+
value = cfg.serverIpv6;
}
{
name = "@";
type = "MX";
-
data = "10 mail";
+
value = "10 mail";
}
{
name = "@";
type = "TXT";
-
data = "\"v=spf1 a:mail.${config.networking.domain} -all\"";
-
}
-
{
-
name = "mail._domainkey";
-
ttl = 10800;
-
type = "TXT";
-
data = "\"v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6YmYYvoFF7VqtGcozpVQa78aaGgZdvc5ZIHqzmkKdCBEyDF2FRbCEK4s2AlC8hhc8O4mSSe3S4AzEhlRgHXbU22GBaUZ3s2WHS8JJwZvWeTjsbXQwjN/U7xpkqXPHLH9IVfOJbHlp4HQmCAXw4NaypgkkxIGK0jaZHm2j6/1izQIDAQAB\"";
+
value = ''"v=spf1 a:mail.${config.networking.domain} -all"'';
}
{
name = "_dmarc";
ttl = 10800;
type = "TXT";
-
data = "\"v=DMARC1; p=reject\"";
+
value = ''"v=DMARC1; p=reject"'';
}
];
};
+24 -24
modules/mastodon.nix
···
let
cfg = config.eilean;
domain = config.networking.domain;
+
subdomain = "mastodon.${domain}";
in {
-
options.eilean.mastodon = {
-
enable = mkEnableOption "mastodon";
-
};
+
options.eilean.mastodon = { enable = mkEnableOption "mastodon"; };
config = mkIf cfg.mastodon.enable {
services.mastodon = {
···
};
extraConfig = {
# override localDomain
-
LOCAL_DOMAIN = "${domain}";
-
WEB_DOMAIN = "mastodon.${domain}";
+
LOCAL_DOMAIN = domain;
+
WEB_DOMAIN = subdomain;
# https://peterbabic.dev/blog/setting-up-smtp-in-mastodon/
-
SMTP_SSL="true";
-
SMTP_ENABLE_STARTTLS="false";
-
SMTP_OPENSSL_VERIFY_MODE="none";
+
SMTP_SSL = "true";
+
SMTP_ENABLE_STARTTLS = "false";
+
SMTP_OPENSSL_VERIFY_MODE = "none";
};
};
-
users.groups.${config.services.mastodon.group}.members = [ config.services.nginx.user ];
+
users.groups.${config.services.mastodon.group}.members =
+
[ config.services.nginx.user ];
+
+
security.acme-eon.nginxCerts = lib.mkIf cfg.acme-eon [ subdomain ];
services.nginx = {
enable = true;
···
# relies on root domain being set up
"${domain}".locations = {
"/.well-known/host-meta".extraConfig = ''
-
return 301 https://mastodon.${domain}$request_uri;
+
return 301 https://${subdomain}$request_uri;
'';
"/.well-known/webfinger".extraConfig = ''
-
return 301 https://mastodon.${domain}$request_uri;
+
return 301 https://${subdomain}$request_uri;
'';
};
-
"mastodon.${domain}" = {
+
"${subdomain}" = {
root = "${config.services.mastodon.package}/public/";
forceSSL = true;
-
enableACME = true;
+
enableACME = lib.mkIf (!cfg.acme-eon) true;
locations."/system/".alias = "/var/lib/mastodon/public-system/";
-
locations."/" = {
-
tryFiles = "$uri @proxy";
-
};
+
locations."/" = { tryFiles = "$uri @proxy"; };
locations."@proxy" = {
-
proxyPass = "http://127.0.0.1:${builtins.toString config.services.mastodon.webPort}";
+
proxyPass = "http://127.0.0.1:${
+
builtins.toString config.services.mastodon.webPort
+
}";
proxyWebsockets = true;
};
};
···
};
eilean.dns.enable = true;
-
eilean.services.dns.zones.${config.networking.domain}.records = [
-
{
-
name = "mastodon";
-
type = "CNAME";
-
data = "vps";
-
}
-
];
+
eilean.services.dns.zones.${config.networking.domain}.records = [{
+
name = "mastodon";
+
type = "CNAME";
+
value = cfg.domainName;
+
}];
};
}
+25 -29
modules/matrix/mautrix-instagram.nix
···
-
{
-
lib,
-
config,
-
pkgs,
-
...
-
}: let
+
{ lib, config, pkgs, ... }:
+
let
cfg = config.services.mautrix-instagram;
dataDir = "/var/lib/mautrix-instagram";
registrationFile = "${dataDir}/instagram-registration.yaml";
settingsFile = "${dataDir}/config.json";
-
settingsFileUnsubstituted = settingsFormat.generate "mautrix-instagram-config-unsubstituted.json" cfg.settings;
-
settingsFormat = pkgs.formats.json {};
+
settingsFileUnsubstituted =
+
settingsFormat.generate "mautrix-instagram-config-unsubstituted.json"
+
cfg.settings;
+
settingsFormat = pkgs.formats.json { };
appservicePort = 29319;
mkDefaults = lib.mapAttrsRecursive (n: v: lib.mkDefault v);
···
};
bridge = {
username_template = "instagram_{{.}}";
-
double_puppet_server_map = {};
-
login_shared_secret_map = {};
+
double_puppet_server_map = { };
+
login_shared_secret_map = { };
permissions."*" = "relay";
relay.enabled = true;
};
···
in {
options.services.mautrix-instagram = {
-
enable = lib.mkEnableOption (lib.mdDoc "mautrix-instagram, a puppeting/relaybot bridge between Matrix and Instagram.");
+
enable = lib.mkEnableOption (lib.mdDoc
+
"mautrix-instagram, a puppeting/relaybot bridge between Matrix and Instagram.");
settings = lib.mkOption {
type = settingsFormat.type;
···
ephemeral_events = false;
};
bridge = {
-
history_sync = {
-
request_full_sync = true;
-
};
+
history_sync = { request_full_sync = true; };
private_chat_portal_meta = true;
mute_bridging = true;
encryption = {
···
default = true;
require = true;
};
-
provisioning = {
-
shared_secret = "disable";
-
};
-
permissions = {
-
"example.com" = "user";
-
};
+
provisioning = { shared_secret = "disable"; };
+
permissions = { "example.com" = "user"; };
};
};
};
serviceDependencies = lib.mkOption {
type = with lib.types; listOf str;
-
default = lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit;
+
default = lib.optional config.services.matrix-synapse.enable
+
config.services.matrix-synapse.serviceUnit;
defaultText = lib.literalExpression ''
optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnits
'';
···
description = "Mautrix-Instagram bridge user";
};
-
users.groups.mautrix-instagram = {};
+
users.groups.mautrix-instagram = { };
services.mautrix-instagram.settings = lib.mkMerge (map mkDefaults [
defaultConfig
# Note: this is defined here to avoid the docs depending on `config`
-
{ homeserver.domain = config.services.matrix-synapse.settings.server_name; }
+
{
+
homeserver.domain = config.services.matrix-synapse.settings.server_name;
+
}
]);
systemd.services.mautrix-instagram = {
description = "Mautrix-Instagram Service - A Instagram bridge for Matrix";
-
wantedBy = ["multi-user.target"];
-
wants = ["network-online.target"] ++ cfg.serviceDependencies;
-
after = ["network-online.target"] ++ cfg.serviceDependencies;
+
wantedBy = [ "multi-user.target" ];
+
wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
+
after = [ "network-online.target" ] ++ cfg.serviceDependencies;
preStart = ''
# substitute the settings file by environment variables
···
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
-
SystemCallFilter = ["@system-service"];
+
SystemCallFilter = [ "@system-service" ];
Type = "simple";
-
UMask = 0027;
+
UMask = 27;
};
-
restartTriggers = [settingsFileUnsubstituted];
+
restartTriggers = [ settingsFileUnsubstituted ];
};
};
}
+25 -29
modules/matrix/mautrix-messenger.nix
···
-
{
-
lib,
-
config,
-
pkgs,
-
...
-
}: let
+
{ lib, config, pkgs, ... }:
+
let
cfg = config.services.mautrix-messenger;
dataDir = "/var/lib/mautrix-messenger";
registrationFile = "${dataDir}/messenger-registration.yaml";
settingsFile = "${dataDir}/config.json";
-
settingsFileUnsubstituted = settingsFormat.generate "mautrix-messenger-config-unsubstituted.json" cfg.settings;
-
settingsFormat = pkgs.formats.json {};
+
settingsFileUnsubstituted =
+
settingsFormat.generate "mautrix-messenger-config-unsubstituted.json"
+
cfg.settings;
+
settingsFormat = pkgs.formats.json { };
appservicePort = 29320;
mkDefaults = lib.mapAttrsRecursive (n: v: lib.mkDefault v);
···
};
bridge = {
username_template = "messenger_{{.}}";
-
double_puppet_server_map = {};
-
login_shared_secret_map = {};
+
double_puppet_server_map = { };
+
login_shared_secret_map = { };
permissions."*" = "relay";
relay.enabled = true;
};
···
in {
options.services.mautrix-messenger = {
-
enable = lib.mkEnableOption (lib.mdDoc "mautrix-messenger, a puppeting/relaybot bridge between Matrix and Messenger.");
+
enable = lib.mkEnableOption (lib.mdDoc
+
"mautrix-messenger, a puppeting/relaybot bridge between Matrix and Messenger.");
settings = lib.mkOption {
type = settingsFormat.type;
···
ephemeral_events = false;
};
bridge = {
-
history_sync = {
-
request_full_sync = true;
-
};
+
history_sync = { request_full_sync = true; };
private_chat_portal_meta = true;
mute_bridging = true;
encryption = {
···
default = true;
require = true;
};
-
provisioning = {
-
shared_secret = "disable";
-
};
-
permissions = {
-
"example.com" = "user";
-
};
+
provisioning = { shared_secret = "disable"; };
+
permissions = { "example.com" = "user"; };
};
};
};
serviceDependencies = lib.mkOption {
type = with lib.types; listOf str;
-
default = lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit;
+
default = lib.optional config.services.matrix-synapse.enable
+
config.services.matrix-synapse.serviceUnit;
defaultText = lib.literalExpression ''
optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnits
'';
···
description = "Mautrix-Messenger bridge user";
};
-
users.groups.mautrix-messenger = {};
+
users.groups.mautrix-messenger = { };
services.mautrix-messenger.settings = lib.mkMerge (map mkDefaults [
defaultConfig
# Note: this is defined here to avoid the docs depending on `config`
-
{ homeserver.domain = config.services.matrix-synapse.settings.server_name; }
+
{
+
homeserver.domain = config.services.matrix-synapse.settings.server_name;
+
}
]);
systemd.services.mautrix-messenger = {
description = "Mautrix-Messenger Service - A Messenger bridge for Matrix";
-
wantedBy = ["multi-user.target"];
-
wants = ["network-online.target"] ++ cfg.serviceDependencies;
-
after = ["network-online.target"] ++ cfg.serviceDependencies;
+
wantedBy = [ "multi-user.target" ];
+
wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
+
after = [ "network-online.target" ] ++ cfg.serviceDependencies;
preStart = ''
# substitute the settings file by environment variables
···
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
-
SystemCallFilter = ["@system-service"];
+
SystemCallFilter = [ "@system-service" ];
Type = "simple";
-
UMask = 0027;
+
UMask = 27;
};
-
restartTriggers = [settingsFileUnsubstituted];
+
restartTriggers = [ settingsFileUnsubstituted ];
};
};
}
-200
modules/matrix/mautrix-signal.nix
···
-
{
-
lib,
-
config,
-
pkgs,
-
...
-
}: let
-
cfg = config.services.mautrix-signal;
-
dataDir = "/var/lib/mautrix-signal";
-
registrationFile = "${dataDir}/signal-registration.yaml";
-
settingsFile = "${dataDir}/config.json";
-
settingsFileUnsubstituted = settingsFormat.generate "mautrix-signal-config-unsubstituted.json" cfg.settings;
-
settingsFormat = pkgs.formats.json {};
-
appservicePort = 29328;
-
-
mkDefaults = lib.mapAttrsRecursive (n: v: lib.mkDefault v);
-
defaultConfig = {
-
homeserver.address = "http://localhost:8448";
-
signal = {
-
socket_path = config.services.signald.socketPath;
-
outgoing_attachment_dir = "/var/lib/signald/tmp";
-
};
-
appservice = {
-
hostname = "[::]";
-
port = appservicePort;
-
database.type = "sqlite3";
-
database.uri = "${dataDir}/mautrix-signal.db";
-
id = "signal";
-
bot.username = "signalbot";
-
bot.displayname = "Signal Bridge Bot";
-
as_token = "";
-
hs_token = "";
-
};
-
bridge = {
-
username_template = "signal_{{.}}";
-
double_puppet_server_map = {};
-
login_shared_secret_map = {};
-
permissions."*" = "relay";
-
};
-
logging = {
-
min_level = "info";
-
writers = lib.singleton {
-
type = "stdout";
-
format = "pretty-colored";
-
time_format = " ";
-
};
-
};
-
};
-
-
in {
-
options.services.mautrix-signal = {
-
enable = lib.mkEnableOption (lib.mdDoc "mautrix-signal, a puppeting/relaybot bridge between Matrix and Signal.");
-
-
settings = lib.mkOption {
-
type = settingsFormat.type;
-
default = defaultConfig;
-
description = lib.mdDoc ''
-
{file}`config.yaml` configuration as a Nix attribute set.
-
Configuration options should match those described in
-
[example-config.yaml](https://github.com/mautrix/signal/blob/master/example-config.yaml).
-
'';
-
example = {
-
appservice = {
-
database = {
-
type = "postgres";
-
uri = "postgresql:///mautrix_signal?host=/run/postgresql";
-
};
-
id = "signal";
-
ephemeral_events = false;
-
};
-
bridge = {
-
history_sync = {
-
request_full_sync = true;
-
};
-
private_chat_portal_meta = true;
-
mute_bridging = true;
-
encryption = {
-
allow = true;
-
default = true;
-
require = true;
-
};
-
provisioning = {
-
shared_secret = "disable";
-
};
-
permissions = {
-
"example.com" = "user";
-
};
-
};
-
};
-
};
-
-
serviceDependencies = lib.mkOption {
-
type = with lib.types; listOf str;
-
default = lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit;
-
defaultText = lib.literalExpression ''
-
optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnits
-
'';
-
description = lib.mdDoc ''
-
List of Systemd services to require and wait for when starting the application service.
-
'';
-
};
-
};
-
-
config = lib.mkIf cfg.enable {
-
-
services.signald.enable = true;
-
-
users.users.mautrix-signal = {
-
isSystemUser = true;
-
group = "mautrix-signal";
-
home = dataDir;
-
description = "Mautrix-Signal bridge user";
-
};
-
-
users.groups.mautrix-signal = {};
-
-
services.mautrix-signal.settings = lib.mkMerge (map mkDefaults [
-
defaultConfig
-
# Note: this is defined here to avoid the docs depending on `config`
-
{ homeserver.domain = config.services.matrix-synapse.settings.server_name; }
-
]);
-
-
systemd.services.mautrix-signal = {
-
description = "Mautrix-Signal Service - A Signal bridge for Matrix";
-
-
requires = [ "signald.service" ];
-
# voice messages need `ffmpeg`
-
path = [ pkgs.ffmpeg ];
-
-
wantedBy = ["multi-user.target"];
-
wants = ["network-online.target"] ++ cfg.serviceDependencies;
-
after = ["network-online.target" "signald.service"] ++ cfg.serviceDependencies;
-
-
preStart = ''
-
# substitute the settings file by environment variables
-
# in this case read from EnvironmentFile
-
test -f '${settingsFile}' && rm -f '${settingsFile}'
-
old_umask=$(umask)
-
umask 0177
-
${pkgs.envsubst}/bin/envsubst \
-
-o '${settingsFile}' \
-
-i '${settingsFileUnsubstituted}'
-
umask $old_umask
-
-
# generate the appservice's registration file if absent
-
if [ ! -f '${registrationFile}' ]; then
-
${pkgs.mautrix-signal}/bin/mautrix-signal \
-
--generate-registration \
-
--config='${settingsFile}' \
-
--registration='${registrationFile}'
-
fi
-
chmod 640 ${registrationFile}
-
-
umask 0177
-
${pkgs.yq}/bin/yq -s '.[0].appservice.as_token = .[1].as_token
-
| .[0].appservice.hs_token = .[1].hs_token
-
| .[0]' '${settingsFile}' '${registrationFile}' \
-
> '${settingsFile}.tmp'
-
mv '${settingsFile}.tmp' '${settingsFile}'
-
umask $old_umask
-
'';
-
-
serviceConfig = {
-
SupplementaryGroups = [ "signald" ];
-
User = "mautrix-signal";
-
Group = "mautrix-signal";
-
StateDirectory = baseNameOf dataDir;
-
WorkingDirectory = dataDir;
-
ExecStart = ''
-
${pkgs.mautrix-signal}/bin/mautrix-signal \
-
--config='${settingsFile}' \
-
--registration='${registrationFile}'
-
'';
-
LockPersonality = true;
-
MemoryDenyWriteExecute = true;
-
NoNewPrivileges = true;
-
PrivateDevices = true;
-
PrivateTmp = true;
-
PrivateUsers = true;
-
ProtectClock = true;
-
ProtectControlGroups = true;
-
ProtectHome = true;
-
ProtectHostname = true;
-
ProtectKernelLogs = true;
-
ProtectKernelModules = true;
-
ProtectKernelTunables = true;
-
ProtectSystem = "strict";
-
Restart = "on-failure";
-
RestartSec = "30s";
-
RestrictRealtime = true;
-
RestrictSUIDSGID = true;
-
SystemCallArchitectures = "native";
-
SystemCallErrorNumber = "EPERM";
-
SystemCallFilter = ["@system-service"];
-
Type = "simple";
-
UMask = 0027;
-
};
-
restartTriggers = [settingsFileUnsubstituted];
-
};
-
};
-
}
+107 -96
modules/matrix/synapse.nix
···
let
cfg = config.eilean;
turnSharedSecretFile = "/run/matrix-synapse/turn-shared-secret";
-
in
-
{
+
domain = config.networking.domain;
+
subdomain = "matrix.${domain}";
+
in {
options.eilean.matrix = {
enable = mkEnableOption "matrix";
turn = mkOption {
···
LC_CTYPE = "C";
'';
+
security.acme-eon.nginxCerts = lib.mkIf cfg.acme-eon [ domain subdomain ];
+
services.nginx = {
enable = true;
# only recommendedProxySettings and recommendedGzipSettings are strictly required,
···
virtualHosts = {
# This host section can be placed on a different host than the rest,
-
# i.e. to delegate from the host being accessible as ${config.networking.domain}
+
# i.e. to delegate from the host being accessible as ${domain}
# to another host actually running the Matrix homeserver.
-
"${config.networking.domain}" = {
-
enableACME = true;
+
"${domain}" = {
+
enableACME = lib.mkIf (!cfg.acme-eon) true;
forceSSL = true;
-
locations."= /.well-known/matrix/server".extraConfig =
-
let
-
# use 443 instead of the default 8448 port to unite
-
# the client-server and server-server port for simplicity
-
server = { "m.server" = "matrix.${config.networking.domain}:443"; };
-
in ''
-
default_type application/json;
-
return 200 '${builtins.toJSON server}';
-
'';
-
locations."= /.well-known/matrix/client".extraConfig =
-
let
-
client = {
-
"m.homeserver" = { "base_url" = "https://matrix.${config.networking.domain}"; };
-
"m.identity_server" = { "base_url" = "https://vector.im"; };
-
};
+
locations."= /.well-known/matrix/server".extraConfig = let
+
# use 443 instead of the default 8448 port to unite
+
# the client-server and server-server port for simplicity
+
server = { "m.server" = "${subdomain}:443"; };
+
in ''
+
default_type application/json;
+
return 200 '${builtins.toJSON server}';
+
'';
+
locations."= /.well-known/matrix/client".extraConfig = let
+
client = {
+
"m.homeserver" = { "base_url" = "https://${subdomain}"; };
+
"m.identity_server" = { "base_url" = "https://vector.im"; };
+
};
# ACAO required to allow element-web on any URL to request this json file
# set other headers due to https://github.com/yandex/gixy/blob/master/docs/en/plugins/addheaderredefinition.md
-
in ''
-
default_type application/json;
-
add_header Access-Control-Allow-Origin *;
-
add_header Strict-Transport-Security max-age=31536000 always;
-
add_header X-Frame-Options SAMEORIGIN always;
-
add_header X-Content-Type-Options nosniff always;
-
add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; frame-src 'self'; frame-ancestors 'self'; form-action 'self';" always;
-
add_header Referrer-Policy 'same-origin';
-
return 200 '${builtins.toJSON client}';
-
'';
+
in ''
+
default_type application/json;
+
add_header Access-Control-Allow-Origin *;
+
add_header Strict-Transport-Security max-age=31536000 always;
+
add_header X-Frame-Options SAMEORIGIN always;
+
add_header X-Content-Type-Options nosniff always;
+
add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; frame-src 'self'; frame-ancestors 'self'; form-action 'self';" always;
+
add_header Referrer-Policy 'same-origin';
+
return 200 '${builtins.toJSON client}';
+
'';
};
# Reverse proxy for Matrix client-server and server-server communication
-
"matrix.${config.networking.domain}" = {
-
enableACME = true;
+
"${subdomain}" = {
+
enableACME = lib.mkIf (!cfg.acme-eon) true;
forceSSL = true;
# Or do a redirect instead of the 404, or whatever is appropriate for you.
···
'';
# forward all Matrix API calls to the synapse Matrix homeserver
-
locations."/_matrix" = {
-
proxyPass = "http://127.0.0.1:8008"; # without a trailing /
-
#proxyPassReverse = "http://127.0.0.1:8008"; # without a trailing /
+
locations."~ ^(\\/_matrix|\\/_synapse\\/client)" = {
+
proxyPass = "http://127.0.0.1:8008";
};
};
};
···
enable = true;
settings = mkMerge [
{
-
server_name = config.networking.domain;
+
server_name = domain;
enable_registration = true;
registration_requires_token = true;
registration_shared_secret_path = cfg.matrix.registrationSecretFile;
-
listeners = [
-
{
-
port = 8008;
-
bind_addresses = [ "::1" "127.0.0.1" ];
-
type = "http";
-
tls = false;
-
x_forwarded = true;
-
resources = [
-
{
-
names = [ "client" "federation" ];
-
compress = false;
-
}
-
];
-
}
-
];
+
listeners = [{
+
port = 8008;
+
bind_addresses = [ "::1" "127.0.0.1" ];
+
type = "http";
+
tls = false;
+
x_forwarded = true;
+
resources = [{
+
names = [ "client" "federation" ];
+
compress = false;
+
}];
+
}];
max_upload_size = "100M";
-
app_service_config_files =
-
(optional cfg.matrix.bridges.whatsapp "/var/lib/mautrix-whatsapp/whatsapp-registration.yaml") ++
-
(optional cfg.matrix.bridges.signal "/var/lib/mautrix-signal/signal-registration.yaml") ++
-
(optional cfg.matrix.bridges.instagram "/var/lib/mautrix-instagram/instagram-registration.yaml") ++
-
(optional cfg.matrix.bridges.messenger "/var/lib/mautrix-messenger/messenger-registration.yaml");
+
app_service_config_files = (optional cfg.matrix.bridges.instagram
+
"/var/lib/mautrix-instagram/instagram-registration.yaml")
+
++ (optional cfg.matrix.bridges.messenger
+
"/var/lib/mautrix-messenger/messenger-registration.yaml");
}
(mkIf cfg.matrix.turn {
turn_uris = with config.services.coturn; [
···
turn_user_lifetime = "1h";
})
];
-
extraConfigFiles = mkIf cfg.matrix.turn (
-
[ turnSharedSecretFile ]
-
);
+
extraConfigFiles = mkIf cfg.matrix.turn ([ turnSharedSecretFile ]);
};
-
systemd.services.matrix-synapse-turn-shared-secret-generator = mkIf cfg.matrix.turn {
-
description = "Generate matrix synapse turn shared secret config file";
-
script = ''
-
mkdir -p "$(dirname '${turnSharedSecretFile}')"
-
echo "turn_shared_secret: $(cat '${config.services.coturn.static-auth-secret-file}')" > '${turnSharedSecretFile}'
-
chmod 770 '${turnSharedSecretFile}'
-
chown ${config.systemd.services.matrix-synapse.serviceConfig.User}:${config.systemd.services.matrix-synapse.serviceConfig.Group} '${turnSharedSecretFile}'
+
systemd.services.matrix-synapse-turn-shared-secret-generator =
+
mkIf cfg.matrix.turn {
+
description = "Generate matrix synapse turn shared secret config file";
+
script = ''
+
mkdir -p "$(dirname '${turnSharedSecretFile}')"
+
echo "turn_shared_secret: $(cat '${config.services.coturn.static-auth-secret-file}')" > '${turnSharedSecretFile}'
+
chmod 770 '${turnSharedSecretFile}'
+
chown ${config.systemd.services.matrix-synapse.serviceConfig.User}:${config.systemd.services.matrix-synapse.serviceConfig.Group} '${turnSharedSecretFile}'
'';
-
serviceConfig.Type = "oneshot";
-
serviceConfig.RemainAfterExit = true;
-
after = [ "coturn-static-auth-secret-generator.service" ];
-
requires = [ "coturn-static-auth-secret-generator.service" ];
-
};
-
systemd.services."matrix-synapse".after = mkIf cfg.matrix.turn [ "matrix-synapse-turn-shared-secret-generator.service" ];
-
systemd.services."matrix-synapse".requires = mkIf cfg.matrix.turn [ "matrix-synapse-turn-shared-secret-generator.service" ];
+
serviceConfig.Type = "oneshot";
+
serviceConfig.RemainAfterExit = true;
+
after = [ "coturn-static-auth-secret-generator.service" ];
+
requires = [ "coturn-static-auth-secret-generator.service" ];
+
};
+
systemd.services."matrix-synapse".after = mkIf cfg.matrix.turn
+
[ "matrix-synapse-turn-shared-secret-generator.service" ];
+
systemd.services."matrix-synapse".requires = mkIf cfg.matrix.turn
+
[ "matrix-synapse-turn-shared-secret-generator.service" ];
systemd.services.matrix-synapse.serviceConfig.SupplementaryGroups =
-
(optional cfg.matrix.bridges.whatsapp config.systemd.services.mautrix-whatsapp.serviceConfig.Group) ++
-
(optional cfg.matrix.bridges.signal config.systemd.services.mautrix-signal.serviceConfig.Group) ++
-
(optional cfg.matrix.bridges.instagram config.systemd.services.mautrix-instagram.serviceConfig.Group) ++
-
(optional cfg.matrix.bridges.messenger config.systemd.services.mautrix-messenger.serviceConfig.Group);
+
# remove after https://github.com/NixOS/nixpkgs/pull/311681/files
+
(optional cfg.matrix.bridges.whatsapp
+
config.systemd.services.mautrix-whatsapp.serviceConfig.Group)
+
++ (optional cfg.matrix.bridges.instagram
+
config.systemd.services.mautrix-instagram.serviceConfig.Group)
+
++ (optional cfg.matrix.bridges.messenger
+
config.systemd.services.mautrix-messenger.serviceConfig.Group);
services.mautrix-whatsapp = mkIf cfg.matrix.bridges.whatsapp {
enable = true;
-
settings.homeserver.address = "https://matrix.${config.networking.domain}";
-
settings.homeserver.domain = config.networking.domain;
+
settings.homeserver.address = "https://${subdomain}";
+
settings.homeserver.domain = domain;
settings.appservice.hostname = "localhost";
settings.appservice.address = "http://localhost:29318";
settings.bridge.personal_filtering_spaces = true;
settings.bridge.history_sync.backfill = false;
-
settings.bridge.permissions."@${config.eilean.username}:${config.networking.domain}" = "admin";
+
settings.bridge.permissions."@${config.eilean.username}:${domain}" =
+
"admin";
+
settings.bridge.encryption.allow = true;
+
settings.bridge.encryption.default = true;
};
+
# using https://github.com/NixOS/nixpkgs/pull/277368
services.mautrix-signal = mkIf cfg.matrix.bridges.signal {
enable = true;
-
settings.homeserver.address = "https://matrix.${config.networking.domain}";
-
settings.homeserver.domain = config.networking.domain;
+
settings.homeserver.address = "https://${subdomain}";
+
settings.homeserver.domain = domain;
settings.appservice.hostname = "localhost";
settings.appservice.address = "http://localhost:29328";
settings.bridge.personal_filtering_spaces = true;
-
settings.bridge.permissions."@${config.eilean.username}:${config.networking.domain}" = "admin";
+
settings.bridge.permissions."@${config.eilean.username}:${domain}" =
+
"admin";
+
settings.bridge.encryption.allow = true;
+
settings.bridge.encryption.default = true;
};
+
# TODO replace with upstreamed mautrix-meta
services.mautrix-instagram = mkIf cfg.matrix.bridges.instagram {
enable = true;
-
settings.homeserver.address = "https://matrix.${config.networking.domain}";
-
settings.homeserver.domain = config.networking.domain;
+
settings.homeserver.address = "https://${subdomain}";
+
settings.homeserver.domain = domain;
settings.appservice.hostname = "localhost";
settings.appservice.address = "http://localhost:29319";
settings.bridge.personal_filtering_spaces = true;
settings.bridge.backfill.enabled = false;
-
settings.bridge.permissions."@${config.eilean.username}:${config.networking.domain}" = "admin";
+
settings.bridge.permissions."@${config.eilean.username}:${domain}" =
+
"admin";
+
settings.bridge.encryption.allow = true;
+
settings.bridge.encryption.default = true;
};
services.mautrix-messenger = mkIf cfg.matrix.bridges.messenger {
enable = true;
-
settings.homeserver.address = "https://matrix.${config.networking.domain}";
-
settings.homeserver.domain = config.networking.domain;
+
settings.homeserver.address = "https://${subdomain}";
+
settings.homeserver.domain = domain;
settings.appservice.hostname = "localhost";
settings.appservice.address = "http://localhost:29320";
settings.bridge.personal_filtering_spaces = true;
settings.bridge.backfill.enabled = false;
-
settings.bridge.permissions."@${config.eilean.username}:${config.networking.domain}" = "admin";
+
settings.bridge.permissions."@${config.eilean.username}:${domain}" =
+
"admin";
+
settings.bridge.encryption.allow = true;
+
settings.bridge.encryption.default = true;
};
eilean.turn.enable = mkIf cfg.matrix.turn true;
eilean.dns.enable = true;
-
eilean.services.dns.zones.${config.networking.domain}.records = [
-
{
-
name = "matrix";
-
type = "CNAME";
-
data = "vps";
-
}
-
];
+
eilean.services.dns.zones.${domain}.records = [{
+
name = "matrix";
+
type = "CNAME";
+
value = cfg.domainName;
+
}];
};
}
+81
modules/radicale.nix
···
+
{ pkgs, config, lib, ... }:
+
+
with lib;
+
let
+
cfg = config.eilean;
+
domain = config.networking.domain;
+
passwdDir = "/var/lib/radicale/users";
+
passwdFile = "${passwdDir}/passwd";
+
userOps = { name, ... }: {
+
options = {
+
name = mkOption {
+
type = types.str;
+
readOnly = true;
+
default = name;
+
};
+
passwordFile = mkOption { type = types.nullOr types.str; };
+
};
+
};
+
in {
+
options.eilean.radicale = {
+
enable = mkEnableOption "radicale";
+
users = mkOption {
+
type = with types; nullOr (attrsOf (submodule userOps));
+
default = { };
+
};
+
};
+
+
config = mkIf cfg.radicale.enable {
+
services.radicale = {
+
enable = true;
+
settings = {
+
server = { hosts = [ "0.0.0.0:5232" ]; };
+
auth = {
+
type = "htpasswd";
+
htpasswd_filename = passwdFile;
+
htpasswd_encryption = "bcrypt";
+
};
+
storage = { filesystem_folder = "/var/lib/radicale/collections"; };
+
};
+
};
+
+
systemd.services.radicale = {
+
serviceConfig.ReadWritePaths = [ "/var/lib/radicale" ];
+
preStart = lib.mkIf (cfg.radicale.users != null) ''
+
if (! test -d "${passwdDir}"); then
+
mkdir "${passwdDir}"
+
chmod 755 "${passwdDir}"
+
fi
+
+
umask 077
+
+
cat <<EOF > ${passwdFile}
+
+
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
+
''
+
$(${pkgs.apacheHttpd}/bin/htpasswd -nbB "${name}" "$(head -n 2 ${value.passwordFile})")'')
+
cfg.radicale.users)}
+
EOF
+
'';
+
};
+
+
services.nginx = {
+
enable = true;
+
recommendedProxySettings = true;
+
virtualHosts = {
+
"cal.${domain}" = {
+
forceSSL = true;
+
enableACME = true;
+
locations."/" = { proxyPass = "http://localhost:5232"; };
+
};
+
};
+
};
+
+
eilean.dns.enable = true;
+
eilean.services.dns.zones.${domain}.records = [{
+
name = "cal";
+
type = "CNAME";
+
value = cfg.domainName;
+
}];
+
};
+
}
+22 -20
modules/services/dns/bind.nix
···
{ pkgs, config, lib, ... }:
-
let cfg = config.eilean.services.dns; in
-
lib.mkIf (cfg.enable && cfg.server == "bind") {
+
let cfg = config.eilean.services.dns;
+
in lib.mkIf (cfg.enable && cfg.server == "bind") {
services.bind = {
enable = true;
# recursive resolver
# cacheNetworks = [ "0.0.0.0/0" ];
-
zones =
-
let mapZones = zonename: zone:
-
{
-
master = true;
-
file = "${config.services.bind.directory}/${zonename}";
-
#file = "${import ./zonefile.nix { inherit pkgs config lib zonename zone; }}/${zonename}";
-
# axfr zone transfer
-
slaves = [
-
"127.0.0.1"
-
];
-
};
-
in builtins.mapAttrs mapZones cfg.zones;
+
zones = let
+
mapZones = zonename: zone: {
+
master = true;
+
file = "${config.services.bind.directory}/${zonename}";
+
#file = "${import ./zonefile.nix { inherit pkgs config lib zonename zone; }}/${zonename}";
+
# axfr zone transfer
+
slaves = [ "127.0.0.1" ];
+
};
+
in builtins.mapAttrs mapZones cfg.zones;
};
+
+
users.users = { named.extraGroups = [ config.services.opendkim.group ]; };
### bind prestart copy zonefiles
-
systemd.services.bind.preStart =
-
let ops =
-
let mapZones = zonename: zone:
+
systemd.services.bind.preStart = let
+
ops = let
+
mapZones = zonename: zone:
let
-
zonefile = "${import ./zonefile.nix { inherit pkgs config lib zonename zone; }}/${zonename}";
+
zonefile = "${
+
import ./zonefile.nix { inherit pkgs config lib zonename zone; }
+
}/${zonename}";
path = "${config.services.bind.directory}/${zonename}";
in ''
if ! diff ${zonefile} ${path} > /dev/null; then
cp ${zonefile} ${path}
+
cat ${config.mailserver.dkimKeyDirectory}/*.txt >> ${path}
# remove journal file to avoid 'journal out of sync with zone'
# NB this will reset dynamic updates
rm -f ${path}.signed.jnl
fi
'';
-
in lib.attrsets.mapAttrsToList mapZones cfg.zones;
-
in builtins.concatStringsSep "\n" ops;
+
in lib.attrsets.mapAttrsToList mapZones cfg.zones;
+
in builtins.concatStringsSep "\n" ops;
}
+15 -26
modules/services/dns/default.nix
···
default = "dns";
};
# TODO auto increment
-
serial = mkOption {
-
type = types.int;
-
};
+
serial = mkOption { type = types.int; };
refresh = mkOption {
type = types.int;
default = 3600; # 1hr
···
default = 3600; # 1hr
};
};
-
records =
-
let recordOpts.options = {
-
name = mkOption {
-
type = types.str;
-
};
+
records = let
+
recordOpts.options = {
+
name = mkOption { type = types.str; };
ttl = mkOption {
type = with types; nullOr int;
default = null;
};
-
type = mkOption {
-
type = types.str;
-
};
-
data = mkOption {
-
type = types.str;
-
};
+
type = mkOption { type = types.str; };
+
value = mkOption { type = types.str; };
};
-
in mkOption {
-
type = with types; listOf (submodule recordOpts);
-
default = [ ];
-
};
+
in mkOption {
+
type = with types; listOf (submodule recordOpts);
+
default = [ ];
+
};
};
-
in
-
{
-
imports = [ ./bind.nix ];
+
in {
+
imports = [ ./bind.nix ./eon.nix ];
options.eilean.services.dns = {
enable = mkEnableOption "DNS server";
server = mkOption {
-
type = types.enum [ "bind" ];
-
default = "bind";
+
type = types.enum [ "bind" "eon" ];
+
default = if config.eilean.acme-eon then "eon" else "bind";
};
openFirewall = mkOption {
type = types.bool;
default = true;
};
-
zones = mkOption {
-
type = with types; attrsOf (submodule zoneOptions);
-
};
+
zones = mkOption { type = with types; attrsOf (submodule zoneOptions); };
};
config.networking.firewall = mkIf config.eilean.services.dns.openFirewall {
+42
modules/services/dns/eon.nix
···
+
{ pkgs, config, lib, ... }:
+
+
let cfg = config.eilean.services.dns;
+
in lib.mkIf (cfg.enable && cfg.server == "eon") {
+
services.eon = {
+
enable = true;
+
application = "capd";
+
capnpAddress = lib.mkDefault config.networking.domain;
+
zoneFiles = let
+
mapZonefile = zonename: zone:
+
"${
+
import ./zonefile.nix { inherit pkgs config lib zonename zone; }
+
}/${zonename}";
+
in lib.attrsets.mapAttrsToList mapZonefile cfg.zones;
+
};
+
+
users.users = { eon.extraGroups = [ config.services.opendkim.group ]; };
+
+
### bind prestart copy zonefiles
+
systemd.services.eon.postStart = let
+
update = ''
+
update() {
+
local file="$1"
+
local domain="$2"
+
local input=$(tr -d '\n' < "$file")
+
local record_name=$(echo "$input" | ${pkgs.gawk}/bin/awk '{print $1}')
+
local record_type=$(echo "$input" | ${pkgs.gawk}/bin/awk '{print $3}')
+
local ttl=3600
+
local record_value=$(echo "$input" | ${pkgs.gnused}/bin/sed -E 's/[^"]*"([^"]*)"[^"]*/\1/g')
+
${config.services.eon.package}/bin/capc update /var/lib/eon/caps/domain/''${domain}.cap -u "add|''${record_name}.''${domain}|''${record_type}|''${record_value}|''${ttl}" || exit 0
+
}
+
shopt -s nullglob
+
'';
+
ops = let
+
mapZones = zonename: zone: ''
+
for f in ${config.mailserver.dkimKeyDirectory}/${zonename}.*.txt; do
+
update $f ${zonename}
+
done
+
'';
+
in lib.attrsets.mapAttrsToList mapZones cfg.zones;
+
in update + builtins.concatStringsSep "\n" ops;
+
}
+4 -12
modules/services/dns/zonefile.nix
···
-
{
-
pkgs,
-
config,
-
lib,
-
zonename,
-
zone,
-
...
-
}:
+
{ pkgs, config, lib, zonename, zone, ... }:
pkgs.writeTextFile {
name = "zonefile-${zonename}";
···
${builtins.toString zone.soa.expire}
${builtins.toString zone.soa.negativeCacheTtl}
)
-
${
-
lib.strings.concatStringsSep "\n"
-
(builtins.map (rr: "${rr.name} IN ${builtins.toString rr.ttl} ${rr.type} ${rr.data}") zone.records)
-
}
+
${lib.strings.concatStringsSep "\n" (builtins.map
+
(rr: "${rr.name} IN ${builtins.toString rr.ttl} ${rr.type} ${rr.value}")
+
zone.records)}
'';
}
+41 -32
modules/turn.nix
···
let
cfg = config.eilean;
domain = config.networking.domain;
+
subdomain = "turn.${domain}";
staticAuthSecretFile = "/run/coturn/static-auth-secret";
-
in
-
{
-
options.eilean.turn = {
-
enable = mkEnableOption "TURN server";
-
};
+
in {
+
options.eilean.turn = { enable = mkEnableOption "TURN server"; };
config = mkIf cfg.turn.enable {
-
services.coturn = rec {
+
security.acme-eon.certs."${subdomain}" = lib.mkIf cfg.acme-eon {
+
group = "turnserver";
+
reloadServices = [ "coturn" ];
+
};
+
+
services.coturn = let
+
certDir = if cfg.acme-eon then
+
config.security.acme-eon.certs.${subdomain}.directory
+
else
+
config.security.acme.certs.${subdomain}.directory;
+
in {
enable = true;
no-cli = true;
no-tcp-relay = true;
secure-stun = true;
use-auth-secret = true;
static-auth-secret-file = staticAuthSecretFile;
-
realm = "turn.${domain}";
-
relay-ips = with config.eilean; [
-
serverIpv4
-
serverIpv6
-
];
-
cert = "${config.security.acme.certs.${realm}.directory}/full.pem";
-
pkey = "${config.security.acme.certs.${realm}.directory}/key.pem";
+
realm = subdomain;
+
relay-ips = with config.eilean; [ serverIpv4 serverIpv6 ];
+
cert = "${certDir}/fullchain.pem";
+
pkey = "${certDir}/key.pem";
};
systemd.services = {
···
script = ''
if [ ! -f '${staticAuthSecretFile}' ]; then
umask 077
+
DIR="$(dirname '${staticAuthSecretFile}')"
+
mkdir -p "$DIR"
tr -dc A-Za-z0-9 </dev/urandom | head -c 32 > '${staticAuthSecretFile}'
-
chown ${config.systemd.services.coturn.serviceConfig.User}:${config.systemd.services.coturn.serviceConfig.Group} '${staticAuthSecretFile}'
+
chown -R ${config.systemd.services.coturn.serviceConfig.User}:${config.systemd.services.coturn.serviceConfig.Group} "$DIR"
fi
'';
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
};
"coturn" = {
-
after = [ "coturn-static-auth-secret-generator.service" ];
+
after = [ "coturn-static-auth-secret-generator.service" ]
+
++ lib.lists.optional cfg.acme-eon "acme-eon-${subdomain}.service";
requires = [ "coturn-static-auth-secret-generator.service" ];
+
wants = lib.lists.optional cfg.acme-eon "acme-eon-${subdomain}.service";
};
};
-
networking.firewall =
-
with config.services.coturn;
+
networking.firewall = with config.services.coturn;
let
turn-range = {
from = min-port;
···
allowedTCPPortRanges = [ turn-range ];
allowedUDPPorts = stun-ports;
allowedUDPPortRanges = [ turn-range ];
-
};
+
};
-
security.acme.certs.${config.services.coturn.realm} = {
-
postRun = "systemctl reload nginx.service; systemctl restart coturn.service";
-
group = "turnserver";
-
};
-
services.nginx.enable = true;
-
services.nginx.virtualHosts = {
+
security.acme.certs.${config.services.coturn.realm} =
+
lib.mkIf (!cfg.acme-eon) {
+
postRun =
+
"systemctl reload nginx.service; systemctl restart coturn.service";
+
group = "turnserver";
+
};
+
services.nginx.enable = lib.mkIf (!cfg.acme-eon) true;
+
services.nginx.virtualHosts = lib.mkIf (!cfg.acme-eon) {
"${config.services.coturn.realm}" = {
forceSSL = true;
enableACME = true;
};
};
-
users.groups."turnserver".members = [ config.services.nginx.user ];
+
users.groups."turnserver".members =
+
lib.mkIf (!cfg.acme-eon) [ config.services.nginx.user ];
eilean.dns.enable = true;
-
eilean.services.dns.zones.${config.networking.domain}.records = [
-
{
-
name = "turn";
-
type = "CNAME";
-
data = "vps";
-
}
-
];
+
eilean.services.dns.zones.${config.networking.domain}.records = [{
+
name = "turn";
+
type = "CNAME";
+
value = cfg.domainName;
+
}];
};
}
+78 -52
modules/wireguard/default.nix
···
{ pkgs, config, lib, ... }:
with lib;
-
let cfg = config.wireguard; in
-
{
+
let cfg = config.wireguard;
+
in {
options.wireguard = {
enable = mkEnableOption "wireguard";
server = mkOption {
type = with types; bool;
-
default =
-
if cfg.hosts ? config.networking.hostName then
-
cfg.hosts.${config.networking.hostName}.server
-
else false;
+
default = if cfg.hosts ? config.networking.hostName then
+
cfg.hosts.${config.networking.hostName}.server
+
else
+
false;
};
-
hosts =
-
let hostOps = { ... }: {
+
hosts = let
+
hostOps = { ... }: {
options = {
-
ip = mkOption {
-
type = types.str;
-
};
-
publicKey = mkOption {
-
type = types.str;
-
};
+
ip = mkOption { type = types.str; };
+
publicKey = mkOption { type = types.str; };
server = mkOption {
type = types.bool;
default = false;
···
};
};
};
-
in mkOption {
-
type = with types; attrsOf (submodule hostOps);
-
};
+
in mkOption {
+
type = with types; attrsOf (submodule hostOps);
+
default = { };
+
};
};
config = mkIf cfg.enable {
environment.systemPackages = with pkgs; [ wireguard-tools ];
-
networking = {
-
# populate /etc/hosts with hostnames and IPs
-
extraHosts = builtins.concatStringsSep "\n" (
-
attrsets.mapAttrsToList (
-
hostName: values: "${values.ip} ${hostName}"
-
) cfg.hosts
-
);
+
networking = mkMerge [
+
{
+
# populate /etc/hosts with hostnames and IPs
+
extraHosts = builtins.concatStringsSep "\n" (attrsets.mapAttrsToList
+
(hostName: values: "${values.ip} ${hostName}") cfg.hosts);
-
firewall = {
-
allowedUDPPorts = [ 51820 ];
-
checkReversePath = false;
-
};
+
firewall = {
+
allowedUDPPorts = [ 51820 ];
+
checkReversePath = false;
+
};
-
wireguard = {
-
enable = true;
-
interfaces.wg0 = let hostName = config.networking.hostName; in {
-
ips =
-
if cfg.hosts ? hostname then
+
wireguard = {
+
enable = true;
+
interfaces.wg0 = let hostName = config.networking.hostName;
+
in {
+
ips = if cfg.hosts ? hostname then
[ "${cfg.hosts."${hostName}".ip}/24" ]
-
else [ ];
-
listenPort = 51820;
-
privateKeyFile = cfg.hosts."${hostName}".privateKeyFile;
-
peers =
-
let
-
serverPeers = attrsets.mapAttrsToList
-
(hostName: values:
-
if values.server then
-
{
-
allowedIPs = [ "10.0.0.0/24" ];
-
publicKey = values.publicKey;
-
endpoint = "${values.endpoint}:51820";
-
persistentKeepalive = values.persistentKeepalive;
-
}
-
else {})
-
cfg.hosts;
+
else
+
[ ];
+
listenPort = 51820;
+
privateKeyFile = cfg.hosts."${hostName}".privateKeyFile;
+
peers = let
+
serverPeers = attrsets.mapAttrsToList (hostName: values:
+
if values.server then {
+
allowedIPs = [ "10.0.0.0/24" ];
+
publicKey = values.publicKey;
+
endpoint = "${values.endpoint}:51820";
+
persistentKeepalive = values.persistentKeepalive;
+
} else
+
{ }) cfg.hosts;
# remove empty elements
cleanedServerPeers = lists.remove { } serverPeers;
-
in mkIf (!cfg.server) cleanedServerPeers;
+
in mkIf (!cfg.server) cleanedServerPeers;
+
};
+
};
+
}
+
+
(mkIf cfg.server {
+
nat = {
+
enable = true;
+
externalInterface = "enp1s0";
+
internalInterfaces = [ "wg0" ];
};
-
};
-
};
+
firewall = {
+
extraCommands = ''
+
iptables -I FORWARD -i wg0 -o wg0 -j ACCEPT
+
'';
+
trustedInterfaces = [ "wg0" ];
+
};
+
+
wireguard.interfaces.wg0 = {
+
# Route from wireguard to public internet, allowing server to act as VPN
+
postSetup = ''
+
${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -o enp1s0 -j MASQUERADE
+
'';
+
+
postShutdown = ''
+
${pkgs.iptables}/bin/iptables -t nat -D POSTROUTING -o enp1s0 -j MASQUERADE
+
'';
+
+
# add clients
+
peers = with lib.attrsets;
+
mapAttrsToList (hostName: values: {
+
allowedIPs = [ "${values.ip}/32" ];
+
publicKey = values.publicKey;
+
persistentKeepalive = values.persistentKeepalive;
+
}) cfg.hosts;
+
};
+
})
+
];
};
}
-39
modules/wireguard/server.nix
···
-
{ pkgs, config, lib, ... }:
-
-
let cfg = config.wireguard; in
-
{
-
networking = lib.mkIf (cfg.enable && cfg.server) {
-
nat = {
-
enable = true;
-
externalInterface = "enp1s0";
-
internalInterfaces = [ "wg0" ];
-
};
-
firewall = {
-
extraCommands = ''
-
iptables -I FORWARD -i wg0 -o wg0 -j ACCEPT
-
'';
-
trustedInterfaces = [ "wg0" ];
-
};
-
-
wireguard.interfaces.wg0 = {
-
# Route from wireguard to public internet, allowing server to act as VPN
-
postSetup = ''
-
${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING -o enp1s0 -j MASQUERADE
-
'';
-
-
postShutdown = ''
-
${pkgs.iptables}/bin/iptables -t nat -D POSTROUTING -o enp1s0 -j MASQUERADE
-
'';
-
-
# add clients
-
peers = with lib.attrsets;
-
mapAttrsToList (
-
hostName: values: {
-
allowedIPs = [ "${values.ip}/32" ];
-
publicKey = values.publicKey;
-
persistentKeepalive = values.persistentKeepalive;
-
}
-
) cfg.hosts;
-
};
-
};
-
}
+8 -5
pkgs/mautrix-meta.nix
···
{ lib, buildGoModule, fetchFromGitHub, olm }:
-
buildGoModule rec {
+
let version = "0.4.4";
+
in buildGoModule rec {
name = "mautrix-meta";
+
inherit version;
src = fetchFromGitHub {
owner = "mautrix";
repo = "meta";
-
rev = "7941e937055b792d2cbfde5d9c8c4df75e68ff0a";
-
hash = "sha256-QDqN6AAaEngWo4UxKAyIXB7BwCEJqsMTeuMb2fKu/9o=";
+
rev = "v${version}";
+
hash = "sha256-S8x3TGQEs+oh/3Q1Gz00M8dOcjjuHSgzVhqlbikZ8QE=";
};
buildInputs = [ olm ];
-
vendorHash = "sha256-ClHg3OEKgXYsmBm/aFKWZXbaLOmKdNyvw42QGhtTRik=";
+
vendorHash = "sha256-sUnvwPJQOoVzxbo2lS3CRcTrWsPjgYPsKClVw1wZJdM=";
doCheck = false;
···
meta = with lib; {
homepage = "https://github.com/mautrix/meta";
-
description = " A Matrix-Facebook Messenger and Instagram DM puppeting bridge.";
+
description =
+
" A Matrix-Facebook Messenger and Instagram DM puppeting bridge.";
license = licenses.agpl3Plus;
mainProgram = "mautrix-meta";
};
+4 -5
template/configuration.nix
···
{ pkgs, config, lib, ... }:
{
-
imports = [
-
./hardware-configuration.nix
-
];
+
imports = [ ./hardware-configuration.nix ];
boot.loader = {
systemd-boot.enable = true;
···
# TODO replace this with domain
networking.domain = "example.org";
-
security.acme.acceptTerms = true;
+
security.acme.acceptTerms = lib.mkIf (!config.eilean.acme-eon) true;
+
security.acme-eon.acceptTerms = lib.mkIf config.eilean.acme-eon true;
# TODO select internationalisation properties
i18n.defaultLocale = "en_GB.UTF-8";
···
# Before changing this value read the documentation for this option
# (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
system.stateVersion = "23.05"; # Did you read the comment?
-
}
+
}
+19 -20
template/flake.nix
···
{
inputs = {
-
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
-
eilean.url ="github:RyanGibb/eilean-nix/main";
-
# replace the below line to manage the Nixpkgs instance yourself
-
nixpkgs.follows = "eilean/nixpkgs";
-
#eilean.inputs.nixpkgs.follows = "nixpkgs";
+
nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11";
+
eilean.url = "github:RyanGibb/eilean-nix/main";
+
eilean.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, eilean, ... }@inputs:
-
let hostname = "eilean"; in
-
rec {
-
nixosConfigurations.${hostname} = nixpkgs.lib.nixosSystem {
+
let hostname = "eilean";
+
in rec {
+
nixosConfigurations.${hostname} = nixpkgs.lib.nixosSystem {
system = null;
pkgs = null;
modules = [
-
./configuration.nix
-
eilean.nixosModules.default
-
{
-
networking.hostName = hostname;
-
# pin nix command's nixpkgs flake to the system flake to avoid unnecessary downloads
-
nix.registry.nixpkgs.flake = nixpkgs;
-
# record git revision (can be queried with `nixos-version --json)
-
system.configurationRevision = nixpkgs.lib.mkIf (self ? rev) self.rev;
-
}
-
];
-
};
+
./configuration.nix
+
eilean.nixosModules.default
+
{
+
networking.hostName = hostname;
+
# pin nix command's nixpkgs flake to the system flake to avoid unnecessary downloads
+
nix.registry.nixpkgs.flake = nixpkgs;
+
# record git revision (can be queried with `nixos-version --json)
+
system.configurationRevision =
+
nixpkgs.lib.mkIf (self ? rev) self.rev;
+
}
+
];
};
-
}
+
};
+
}