Self-host your own digital island

Compare changes

Choose any two refs to compare.

+185 -42
flake.lock
···
"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": {
···
"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",
-
"nixpkgs": "nixpkgs",
-
"nixpkgs-23_05": "nixpkgs-23_05",
-
"nixpkgs-23_11": "nixpkgs-23_11",
+
"flake-compat": "flake-compat_2",
+
"nixpkgs": [
+
"nixpkgs"
+
],
+
"nixpkgs-24_05": "nixpkgs-24_05",
"utils": "utils"
},
"locked": {
-
"lastModified": 1711069052,
-
"narHash": "sha256-QScBLiWRDmrOhG4/jCrdZTNe8zQdf6gzSZocAYOcG3U=",
+
"lastModified": 1718183756,
+
"narHash": "sha256-m5JQT/RIegSLZJx41Cv7d8Xoa2KKq+5uLkgB5KJR5D0=",
"owner": "RyanGibb",
"repo": "nixos-mailserver",
-
"rev": "435c3a167c52ebe443f6ed1ba3412331ae566d05",
-
"type": "github"
+
"rev": "9dc7a8d40232f600e6ca1e78356cd4398665b46b",
+
"type": "gitlab"
},
"original": {
"owner": "RyanGibb",
-
"ref": "fork-23.11",
+
"ref": "fork-24.05",
"repo": "nixos-mailserver",
-
"type": "github"
+
"type": "gitlab"
}
},
"nixpkgs": {
"locked": {
-
"lastModified": 1709703039,
-
"narHash": "sha256-6hqgQ8OK6gsMu1VtcGKBxKQInRLHtzulDo9Z5jxHEFY=",
-
"owner": "NixOS",
+
"lastModified": 1732981179,
+
"narHash": "sha256-F7thesZPvAMSwjRu0K8uFshTk3ZZSNAsXTIFvXBT+34=",
+
"owner": "nixos",
"repo": "nixpkgs",
-
"rev": "9df3e30ce24fd28c7b3e2de0d986769db5d6225d",
+
"rev": "62c435d93bf046a5396f3016472e8f7c8e2aed65",
"type": "github"
},
"original": {
-
"id": "nixpkgs",
-
"ref": "nixos-unstable",
-
"type": "indirect"
+
"owner": "nixos",
+
"ref": "nixos-24.11",
+
"repo": "nixpkgs",
+
"type": "github"
}
},
-
"nixpkgs-23_05": {
+
"nixpkgs-24_05": {
"locked": {
-
"lastModified": 1704290814,
-
"narHash": "sha256-LWvKHp7kGxk/GEtlrGYV68qIvPHkU9iToomNFGagixU=",
+
"lastModified": 1718086528,
+
"narHash": "sha256-hoB7B7oPgypePz16cKWawPfhVvMSXj4G/qLsfFuhFjw=",
"owner": "NixOS",
"repo": "nixpkgs",
-
"rev": "70bdadeb94ffc8806c0570eb5c2695ad29f0e421",
+
"rev": "47b604b07d1e8146d5398b42d3306fdebd343986",
"type": "github"
},
"original": {
"id": "nixpkgs",
-
"ref": "nixos-23.05",
+
"ref": "nixos-24.05",
"type": "indirect"
}
},
-
"nixpkgs-23_11": {
+
"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": 1710951922,
-
"narHash": "sha256-FOOBJ3DQenLpTNdxMHR2CpGZmYuctb92gF0lpiirZ30=",
-
"owner": "NixOS",
-
"repo": "nixpkgs",
-
"rev": "f091af045dff8347d66d186a62d42aceff159456",
+
"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": {
-
"id": "nixpkgs",
-
"ref": "nixos-23.11",
-
"type": "indirect"
+
"owner": "ocaml",
+
"repo": "opam-repository",
+
"type": "github"
}
},
-
"nixpkgs_2": {
+
"opam2json": {
+
"inputs": {
+
"nixpkgs": [
+
"eon",
+
"opam-nix",
+
"nixpkgs"
+
]
+
},
"locked": {
-
"lastModified": 1710272261,
-
"narHash": "sha256-g0bDwXFmTE7uGDOs9HcJsfLFhH7fOsASbAuOzDC+fhQ=",
-
"owner": "nixos",
-
"repo": "nixpkgs",
-
"rev": "0ad13a6833440b8e238947e47bea7f11071dc2b2",
+
"lastModified": 1671540003,
+
"narHash": "sha256-5pXfbUfpVABtKbii6aaI2EdAZTjHJ2QntEf0QD2O5AM=",
+
"owner": "tweag",
+
"repo": "opam2json",
+
"rev": "819d291ea95e271b0e6027679de6abb4d4f7f680",
"type": "github"
},
"original": {
-
"owner": "nixos",
-
"ref": "nixos-unstable",
-
"repo": "nixpkgs",
+
"owner": "tweag",
+
"repo": "opam2json",
"type": "github"
}
},
"root": {
"inputs": {
+
"eon": "eon",
"nixos-mailserver": "nixos-mailserver",
-
"nixpkgs": "nixpkgs_2"
+
"nixpkgs": "nixpkgs"
}
},
"systems": {
···
"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"
+
"systems": "systems_2"
},
"locked": {
"lastModified": 1709126324,
+15 -6
flake.nix
···
{
inputs = {
-
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
-
nixos-mailserver.url = "github:RyanGibb/nixos-mailserver/fork-23.11";
+
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
+
nixos-mailserver.url = "gitlab:RyanGibb/nixos-mailserver/fork-24.05";
+
eon.url = "github:RyanGibb/eon";
+
+
eon.inputs.nixpkgs.follows = "nixpkgs";
+
nixos-mailserver.inputs.nixpkgs.follows = "nixpkgs";
};
-
outputs = { self, nixpkgs, nixos-mailserver, ... }: rec {
+
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; }; });
+
in {
+
manpage = import ./man { inherit pkgs system nixos-mailserver; };
+
packages.mautrix-meta = (pkgs.callPackage ./pkgs/mautrix-meta.nix { });
+
});
nixosModules.default = {
imports = [
./modules/default.nix
nixos-mailserver.nixosModule
-
({ pkgs, config, ... }: {
+
eon.nixosModules.default
+
eon.nixosModules.acme
+
{
nixpkgs.overlays = [
(final: prev: {
mautrix-meta = (prev.callPackage ./pkgs/mautrix-meta.nix { });
})
];
-
})
+
}
];
};
defaultTemplate.path = ./template;
+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".
+
'';
+
}];
+
};
+
}
+10 -2
modules/default.nix
···
{
imports = [
+
./acme-eon.nix
./services/dns/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/default.nix
+
./radicale.nix
];
options.eilean = with types; {
···
serverIpv4 = mkOption { type = str; };
serverIpv6 = mkOption { type = str; };
publicInterface = mkOption { type = str; };
+
domainName = mkOption {
+
type = types.str;
+
default = "vps";
+
};
};
config = {
# TODO install manpage
environment.systemPackages = [ ];
-
security.acme.defaults.email =
+
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
+10 -15
modules/dns.nix
···
{
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 ++ [
{
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.*
+
'';
+
};
+
};
+
}
+8 -5
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:${
···
mailerPasswordFile = cfg.mailserver.systemAccountPasswordFile;
settings = {
server = {
-
ROOT_URL = "https://git.${domain}/";
-
DOMAIN = "git.${domain}";
+
ROOT_URL = "https://${subdomain}/";
+
DOMAIN = subdomain;
};
mailer = {
ENABLED = true;
···
eilean.services.dns.zones.${config.networking.domain}.records = [{
name = "git";
type = "CNAME";
-
data = "vps";
+
value = cfg.domainName;
}];
# proxy port 22 on ethernet interface to internal gitea ssh server
+1 -6
modules/headscale.nix
···
server_url = "https://${cfg.headscale.domain}";
logtail.enabled = false;
ip_prefixes = [ "100.64.0.0/10" "fd7a:115c:a1e0::/48" ];
-
dns_config = {
-
# magicDns = true;
-
nameservers = config.networking.nameservers;
-
base_domain = "${cfg.headscale.zone}";
-
};
};
};
···
eilean.services.dns.zones.${cfg.headscale.zone}.records = [{
name = "${cfg.headscale.domain}.";
type = "CNAME";
-
data = "vps";
+
value = cfg.domainName;
}];
};
}
+28 -15
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 = "acme-nginx";
-
+
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";
···
{
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"'';
}
];
};
+10 -7
modules/mastodon.nix
···
let
cfg = config.eilean;
domain = config.networking.domain;
+
subdomain = "mastodon.${domain}";
in {
options.eilean.mastodon = { enable = mkEnableOption "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";
···
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;
recommendedProxySettings = 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/";
···
eilean.services.dns.zones.${config.networking.domain}.records = [{
name = "mastodon";
type = "CNAME";
-
data = "vps";
+
value = cfg.domainName;
}];
};
}
-197
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 = 27;
-
};
-
restartTriggers = [ settingsFileUnsubstituted ];
-
};
-
};
-
}
+41 -39
modules/matrix/synapse.nix
···
let
cfg = config.eilean;
turnSharedSecretFile = "/run/matrix-synapse/turn-shared-secret";
+
domain = config.networking.domain;
+
subdomain = "matrix.${domain}";
in {
options.eilean.matrix = {
enable = mkEnableOption "matrix";
···
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"; };
+
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://matrix.${config.networking.domain}";
-
};
+
"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
···
};
# 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;
···
}];
}];
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")
+
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");
}
···
[ "matrix-synapse-turn-shared-secret-generator.service" ];
systemd.services.matrix-synapse.serviceConfig.SupplementaryGroups =
+
# 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.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
···
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}" =
+
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}" =
+
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}" =
+
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}" =
+
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 = [{
+
eilean.services.dns.zones.${domain}.records = [{
name = "matrix";
type = "CNAME";
-
data = "vps";
+
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;
+
}];
+
};
+
}
+3
modules/services/dns/bind.nix
···
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
···
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
+4 -4
modules/services/dns/default.nix
···
default = null;
};
type = mkOption { type = types.str; };
-
data = mkOption { type = types.str; };
+
value = mkOption { type = types.str; };
};
in mkOption {
type = with types; listOf (submodule recordOpts);
···
};
};
in {
-
imports = [ ./bind.nix ];
+
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;
+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;
+
}
+1 -1
modules/services/dns/zonefile.nix
···
${builtins.toString zone.soa.negativeCacheTtl}
)
${lib.strings.concatStringsSep "\n" (builtins.map
-
(rr: "${rr.name} IN ${builtins.toString rr.ttl} ${rr.type} ${rr.data}")
+
(rr: "${rr.name} IN ${builtins.toString rr.ttl} ${rr.type} ${rr.value}")
zone.records)}
'';
}
+32 -15
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"; };
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}";
+
realm = subdomain;
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";
+
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";
};
};
···
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";
+
value = cfg.domainName;
}];
};
}
+6 -4
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;
+2 -1
template/configuration.nix
···
# 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";