Self-host your own digital island

Compare changes

Choose any two refs to compare.

+14 -8
README.md
···
-
# Eilean ๐Ÿ๏ธ
+
# Eilean
-
Eilean enables you to host your own digital 'island', where you control you're own online infrastructure.
-
The use of federated protocols allows you to 'bridge' your island to others.
+
<div align="center">
+
<img src="./eilean-donan.jpg" alt="Eilean Donan"/>
+
<!-- Photo by DAVID ILIFF. License: CC BY-SA 3.0 -->
+
</div>
-
Eilean uses [NixOS](https://nixos.org/) to enable reproducible deployments of services such as webservers, mailservers, federated communication servers, Virtual Private Network servers, and more.
-
However, they still require a lot of manual configuration for domain names, DNS records, user accounts, databases, HTTP proxies, TLS certificates, and more.
+
Eilean enables you to host your own digital island where you control your own online infrastructure.
+
Through the use of open standards and federated protocols Eilean allows you to interoperate with other providers.
+
+
[NixOS](https://nixos.org/) is used to enable reproducible deployments of services such as webservers, mailservers, federated communication servers, Virtual Private Network servers, and more.
+
However, such services still require a lot of manual configuration for domain names, DNS records, user accounts, databases, HTTP proxies, TLS certificates, and more.
Eilean aims to be a optioned framework to allow the simple deployment of these services on a single machine, and a library of documentation for common issues in managing runtime state like secrets, databases, and upgrades.
-
By using Nix, Eilean modules are extensive to other configurations outside this deployment scenario, such as offloading a particularly resource heavy service to a dedicated machine.
+
By using Nix, Eilean modules are extensible to other configurations outside this deployment scenario, such as offloading a particularly resource heavy service to a dedicated machine.
+
+
For instructions on getting started see [docs/getting_started.md](./docs/getting_started.md).
-
Contributions for additional services welcome.
+
Contributions for additional services are welcome.
-
For instructions on getting started see [docs/getting_started.md](./docs/getting_started.md).
-9
docs/adding_eilean.md
···
-
-
# Adding Eilean to an Existing NixOS System
-
-
If you already have a NixOS system and want to use Eilean you can add to your configuration.
-
Note this requires a flake-enabled system.
-
-
Add `github:RyanGibb/eilean-nix` as an input to your flake, and import `eilean.nixosModules.default`.
-
You should then be able to use the configuration options in `config.eilean`.
-
See [../template/flake.nix](../template/flake.nix) for an example.
docs/backups.md

This is a binary file and will not be displayed.

+24 -56
docs/getting_started.md
···
# Setup
This guide walks a user through the first time setup of a server running Eilean.
-
If you already have a NixOS system, please feel look at [adding eilean](adding_eilean.md) to an existing system.
-
Some familiarity with networking, operating systems, and Linux is necessary.
-
Some familiarity with Nix is beneficial.
+
+
If you already have a NixOS system and want to use Eilean you can add to your configuration.
+
Note this requires a flake-enabled system.
+
Add `github:RyanGibb/eilean-nix` as an input to your flake, and import `eilean.nixosModules.default`.
+
You should then be able to use the configuration options in `config.eilean`.
+
See [../template/flake.nix](../template/flake.nix) for an example.
+
+
Otherwise, some familiarity with networking, operating systems, and Linux is necessary.
+
And some familiarity with Nix is beneficial.
## Step 1: Find a server
-
- Option 1 (recommended): a Virtual Private Server (VPS) with a cloud provider such as Hetzner, Vultr, or Digital Ocean. Get an IPv4 address^[1].
+
- Option 1 (recommended): a Virtual Private Server (VPS) with a cloud provider such as Hetzner, Vultr, or Digital Ocean. Get an IPv4 address[^1].
You can use this referral link to get started on Hetzner: https://hetzner.cloud/?ref=XydbkWdf49TY.
- Option 2: your own hardware, such as an old PC or laptop, Raspberry Pi, or a custom-build server.
-
Note you'll need a static IPv4^[1] address for reliable hosting^[2]. If you're behind Network Address Translation (NAT) you'll need to set up port forwarding for every service you want to run.
-
-
The resource requirements depend on the number of services you want to run and
-
resource requirements
-
under 10G disk space with no services...
-
with all enabled...
+
Note you'll need a static IPv4[^1] address for reliable hosting[^2]. If you're behind Network Address Translation (NAT) you'll need to set up port forwarding for every service you want to run.
+
The resource requirements depend on the number of services you want to run, and the load they'll be under.
+
With no services 10ย GiB of disk space and 1ย GiB of RAM is plenty, though you may want to enable swap if you have low memory.
+
With all services enabled and fairly populated databases 40ย GiB of disk space and 2ย GiB of RAM is appropriate.
-
[1]: You could just use an IPv6 address, but much of the Internet is still [IPv4-only](https://stats.labs.apnic.net/ipv6).
+
[^1]: You could just use an IPv6 address, but much of the Internet is still [IPv4-only](https://stats.labs.apnic.net/ipv6).
-
[2]: If you don't have a static address, Dynamic DNS is possible but takes some time to propagate. Email reputation is tied to your IP address; using a residential address assigned by your ISP may get your mail blocked.
+
[^2]: If you don't have a static address, Dynamic DNS is possible but takes some time to propagate. Email reputation is tied to your IP address; using a residential address assigned by your ISP may get your mail blocked.
-
Resource requirements depends how many service you want to run and how much load they'll be under, but 2 GB RAM and 20 GB disk should be a good starting point.
+
Resource requirements depend on how many services you want to run and how much load they'll be under, but 2 GiB RAM and 20 GiB disk should be a good starting point.
## Step 2: Install NixOS with Eilean
···
Note that we are using the `hardware-configuration.nix` generated by `nixos-generate-config`.
Eilean uses [flakes](https://www.tweag.io/blog/2020-05-25-flakes/).
-
Without going into too much depth, they enable hermetic evaluation of Nix expressions and provide a standard way to compose Nix projects^[3].
+
Without going into too much depth, they enable hermetic evaluation of Nix expressions and provide a standard way to compose Nix projects[^3].
-
[3]: [tweag.io/blog/2020-05-25-flakes](https://www.tweag.io/blog/2020-05-25-flakes/).
+
[^3]: [tweag.io/blog/2020-05-25-flakes](https://www.tweag.io/blog/2020-05-25-flakes/).
You can edit the resulting `configuration.nix`.
Check out the `TODO`'s for a place to start.
···
```
Upon boot you should be able to login as root.
-
You may need to run `passwd <username>` (where `<username>` is `eilean` by default) to be able to log in as `<username>`^[4].
+
You may need to run `passwd <username>` (where `<username>` is `eilean` by default) to be able to log in as `<username>`[^4].
You should be able to edit `/etc/nixos/configuration.nix` and rebuild you system with `sudo nixos-rebuild switch`.
-
[4]: [github.com/NixOS/nixpkgs/issues/55424](https://github.com/NixOS/nixpkgs/issues/55424)
+
[^4]: [github.com/NixOS/nixpkgs/issues/55424](https://github.com/NixOS/nixpkgs/issues/55424)
-
By default DHCP will be enabled so your machine will discovery it's IP address, however some providers don't enable DHCPv6 or SLAAC so you need to manually configure the IP address.
+
By default, DHCP will be enabled, so your machine will discover its IP address, however some providers don't enable DHCPv6 or SLAAC, so you need to manually configure the IP address.
For example a Hetzner VPS IPv6 address can be found in the networking tab and enabled with:
```
networking = {
···
## Step 4: Configure Eilean
-
Once your domain is setup, replace these default values of Eilean with your IPv4 and IPv6 network addresses, and your public network interface:
+
Once your domain is set up, replace these default values of Eilean with your IPv4 and IPv6 network addresses, and your public network interface:
```
eilean.serverIpv4 = "203.0.113.0";
eilean.serverIpv6 = "2001:DB8:0:0:0:0:0:0";
···
## Further Information
-
### Website
-
-
TODO
-
-
### Secrets
-
-
TODO
-
-
### Backups
-
-
TODO
-
-
##### Email
-
-
Hosting email allows for an easy, and cheap, SMTP server for services that require it.
-
Receiving EMail shouldn't pose an issues.
-
Sending email to users on your own domain shouldn't pose any issues, if for example users are signing up to services like Mastodon using an EMail account on the same Eilean.
-
Sending mail will require TCP port 25 to be unblocked by your network provider, and your IP address to not be blacklisted (e.g. check [here](https://mxtoolbox.com/blacklists.aspx)).
-
-
not managed:
-
- multiple domains
-
- multiple servers /load balance
-
-
### Matrix
-
-
TODO
-
-
### Mastodon
-
-
TODO
-
-
### Gitea
-
-
TODO
+
For a list of options, use `man eilean-configuration.nix`.
-
### Wireguard/Headscale
-
-
TODO
+12
docs/mailserver.md
···
+
+
+
Hosting email allows for an easy, and cheap, SMTP server for services that require it.
+
Receiving EMail shouldn't pose an issues.
+
Sending email to users on your own domain shouldn't pose any issues, if for example users are signing up to services like Mastodon using an EMail account on the same Eilean.
+
Sending mail will require TCP port 25 to be unblocked by your network provider, and your IP address to not be blacklisted (e.g. check [here](https://mxtoolbox.com/blacklists.aspx)).
+
+
not managed:
+
- multiple domains
+
- multiple servers /load balance
+
+
docs/mastodon.md

This is a binary file and will not be displayed.

docs/matrix.md

This is a binary file and will not be displayed.

docs/nameserver.md

This is a binary file and will not be displayed.

+83
docs/nix/nix.md
···
+
+
# Nix
+
+
Nix is a software deployment system that uses cryptographic hashes to compute unique paths for components (i.e., packages) that are stored in a read-only directory: the Nix store, at `/nix/store/<hash>-<name>`.
+
This provides several benefits, including concurrent installation of multiple versions of a package, atomic upgrades, and multiple user environments.
+
+
Nix uses a declarative domain-specific language (DSL), also called Nix, to build and configure software.
+
The Nix DSL is a functional language with a syntax Turing complete but lacks a type system.
+
We use the DSL to write derivations for software, which describe how to build said software with input components and a build script.
+
This Nix expression is then 'instantiated' to create 'store derivations' (`.drv` files), which is the low-level representation of how to build a single component.
+
This store derivation is 'realised' into a built artefact, hereafter referred to as 'building'.
+
+
Possibly the simplest Nix derivation uses `bash` to create a single file containing `Hello, World!`:
+
```nix
+
{ pkgs ? import <nixpkgs> { } }:
+
+
builtins.derivation {
+
name = "hello";
+
system = builtins.currentSystem;
+
builder = "${nixpkgs.bash}/bin/bash";
+
args = [ "-c" ''echo "Hello, World!" > $out'' ];
+
}
+
```
+
Note that `derivation` is a function that we're calling with one argument, which is a set of attributes.
+
+
We can instantiate this Nix derivation to create a store derivation:
+
```
+
$ nix-instantiate default.nix
+
/nix/store/5d4il3h1q4cw08l6fnk4j04a19dsv71k-hello.drv
+
$ nix show-derivation /nix/store/5d4il3h1q4cw08l6fnk4j04a19dsv71k-hello.drv
+
{
+
"/nix/store/5d4il3h1q4cw08l6fnk4j04a19dsv71k-hello.drv": {
+
"outputs": {
+
"out": {
+
"path": "/nix/store/4v1dx6qaamakjy5jzii6lcmfiks57mhl-hello"
+
}
+
},
+
"inputSrcs": [],
+
"inputDrvs": {
+
"/nix/store/mnyhjzyk43raa3f44pn77aif738prd2m-bash-5.1-p16.drv": [
+
"out"
+
]
+
},
+
"system": "x86_64-linux",
+
"builder": "/nix/store/2r9n7fz1rxq088j6mi5s7izxdria6d5f-bash-5.1-p16/bin/bash",
+
"args": [ "-c", "echo \"Hello, World!\" > $out" ],
+
"env": {
+
"builder": "/nix/store/2r9n7fz1rxq088j6mi5s7izxdria6d5f-bash-5.1-p16/bin/bash",
+
"name": "hello",
+
"out": "/nix/store/4v1dx6qaamakjy5jzii6lcmfiks57mhl-hello",
+
"system": "x86_64-linux"
+
}
+
}
+
}
+
```
+
+
And build the store derivation:
+
```sh
+
$ nix-store --realise /nix/store/5d4il3h1q4cw08l6fnk4j04a19dsv71k-hello.drv
+
/nix/store/4v1dx6qaamakjy5jzii6lcmfiks57mhl-hello
+
$ cat /nix/store/4v1dx6qaamakjy5jzii6lcmfiks57mhl-hello
+
Hello, World!
+
```
+
+
Most Nix tooling does these two steps together:
+
```
+
nix-build default.nix
+
this derivation will be built:
+
/nix/store/q5hg3vqby8a9c8pchhjal3la9n7g1m0z-hello.drv
+
building '/nix/store/q5hg3vqby8a9c8pchhjal3la9n7g1m0z-hello.drv'...
+
/nix/store/zyrki2hd49am36jwcyjh3xvxvn5j5wml-hello
+
```
+
+
Nix realisations (hereafter referred to as 'builds') are done in isolation to ensure reproducibility.
+
Projects often rely on interacting with package managers to make sure all dependencies are available and may implicitly rely on system configuration at build time.
+
To prevent this, every Nix derivation is built in isolation (without network access or access to the global file system) with only other Nix derivations as inputs.
+
+
> The name Nix is derived from the Dutch word *niks*, meaning nothing; build actions do not see anything that has not been explicitly declared as an input.
+
+
For more information, see:
+
- The Nix paper: https://edolstra.github.io/pubs/nspfssd-lisa2004-final.pdf
+
- The Nix PhD thesis: https://edolstra.github.io/pubs/phd-thesis.pdf
+
+19
docs/nix/nixos.md
···
+
+
# NixOS
+
+
[NixOS](nixos.org) is a Linux distribution built with [Nix](./nix.md) from a modular, purely functional specification.
+
It has no traditional filesystem hierarchy (FSH), like `/bin`, `/lib`, `/usr`, but instead stores all components in `/nix/store`.
+
The system configuration is managed by Nix and configured with Nix expressions.
+
[NixOS modules](https://nixos.org/manual/nixos/stable/index.html#sec-writing-modules) are Nix files containing chunks of system configuration that can be composed to build a full NixOS system.
+
While many NixOS modules are provided in the [Nixpkgs](./nixpkgs.md) repository, they can also be written by an individual user.
+
For example, the expression used to deploy a DNS server is a NixOS module.
+
Together these modules form the configuration which builds the Linux system as a Nix derivation.
+
+
NixOS minimises global mutable state that -- without knowing it -- you might rely on being set up in a certain way.
+
For example, you might follow instructions to run a series of shell commands and edit some files to get a piece of software working.
+
You may subsequently be unable to reproduce the result because you've forgotten some intricacy or are now using a different version of the software.
+
Nix forces you to encode this in a reproducible way, which is extremely useful for replicating software configurations and deployments, aiming to solve the 'It works on my machine' problem.
+
Docker is often used to fix this configuration problem, but Nix aims to be more reproducible.
+
+
Nix provides safe and reliable atomic upgrades and rollbacks.
+
And every new system configuration build creates a GRUB entry, so you can boot previous systems even from your UEFI/BIOS.
+19
docs/nix/nixpkgs.md
···
+
+
### Nixpkgs
+
+
Nixpkgs^[ [github.com/nixos/nixpkgs](https://github.com/nixos/nixpkgs) ] is a large repository of software packaged in [Nix](./nix.md), where every package is a Nix derivation.
+
It also stores all the default [./NixOS](./nixos.md) modules.
+
+
There is also a command line package manager that installs packages from Nixpkgs, which is why people sometimes refer to Nix as a package manager.
+
+
While Nix, and therefore Nix package management, is primarily source-based (since derivations describe how to build software from source), binary deployment is an optimisation of this.
+
Since packages are built in isolation and entirely determined by their inputs, binaries can be transparently deployed by downloading them from a remote server instead of building the derivation locally.
+
+
Nix supports atomic upgrades and rollbacks.
+
The pointers to the new packages are only updated when the install succeeds.
+
+
Due to every
+
+
+
While Nixpkgs also has one global coherent package set, one can use multiple instances of Nixpkgs (i.e., channels) at once to support partial upgrades, as the Nix store allows multiple versions of a dependency to be stored.
+
This also supports atomic upgrades, as all the software's old versions can be kept until garbage collection.
-83
docs/nix.md
···
-
-
# Nix
-
-
Nix is a software deployment system that uses cryptographic hashes to compute unique paths for components (i.e., packages) that are stored in a read-only directory: the Nix store, at `/nix/store/<hash>-<name>`.
-
This provides several benefits, including concurrent installation of multiple versions of a package, atomic upgrades, and multiple user environments.
-
-
Nix uses a declarative domain-specific language (DSL), also called Nix, to build and configure software.
-
The Nix DSL is a functional language with a syntax Turing complete but lacks a type system.
-
We use the DSL to write derivations for software, which describe how to build said software with input components and a build script.
-
This Nix expression is then 'instantiated' to create 'store derivations' (`.drv` files), which is the low-level representation of how to build a single component.
-
This store derivation is 'realised' into a built artefact, hereafter referred to as 'building'.
-
-
Possibly the simplest Nix derivation uses `bash` to create a single file containing `Hello, World!`:
-
```nix
-
{ pkgs ? import <nixpkgs> { } }:
-
-
builtins.derivation {
-
name = "hello";
-
system = builtins.currentSystem;
-
builder = "${nixpkgs.bash}/bin/bash";
-
args = [ "-c" ''echo "Hello, World!" > $out'' ];
-
}
-
```
-
Note that `derivation` is a function that we're calling with one argument, which is a set of attributes.
-
-
We can instantiate this Nix derivation to create a store derivation:
-
```
-
$ nix-instantiate default.nix
-
/nix/store/5d4il3h1q4cw08l6fnk4j04a19dsv71k-hello.drv
-
$ nix show-derivation /nix/store/5d4il3h1q4cw08l6fnk4j04a19dsv71k-hello.drv
-
{
-
"/nix/store/5d4il3h1q4cw08l6fnk4j04a19dsv71k-hello.drv": {
-
"outputs": {
-
"out": {
-
"path": "/nix/store/4v1dx6qaamakjy5jzii6lcmfiks57mhl-hello"
-
}
-
},
-
"inputSrcs": [],
-
"inputDrvs": {
-
"/nix/store/mnyhjzyk43raa3f44pn77aif738prd2m-bash-5.1-p16.drv": [
-
"out"
-
]
-
},
-
"system": "x86_64-linux",
-
"builder": "/nix/store/2r9n7fz1rxq088j6mi5s7izxdria6d5f-bash-5.1-p16/bin/bash",
-
"args": [ "-c", "echo \"Hello, World!\" > $out" ],
-
"env": {
-
"builder": "/nix/store/2r9n7fz1rxq088j6mi5s7izxdria6d5f-bash-5.1-p16/bin/bash",
-
"name": "hello",
-
"out": "/nix/store/4v1dx6qaamakjy5jzii6lcmfiks57mhl-hello",
-
"system": "x86_64-linux"
-
}
-
}
-
}
-
```
-
-
And build the store derivation:
-
```sh
-
$ nix-store --realise /nix/store/5d4il3h1q4cw08l6fnk4j04a19dsv71k-hello.drv
-
/nix/store/4v1dx6qaamakjy5jzii6lcmfiks57mhl-hello
-
$ cat /nix/store/4v1dx6qaamakjy5jzii6lcmfiks57mhl-hello
-
Hello, World!
-
```
-
-
Most Nix tooling does these two steps together:
-
```
-
nix-build default.nix
-
this derivation will be built:
-
/nix/store/q5hg3vqby8a9c8pchhjal3la9n7g1m0z-hello.drv
-
building '/nix/store/q5hg3vqby8a9c8pchhjal3la9n7g1m0z-hello.drv'...
-
/nix/store/zyrki2hd49am36jwcyjh3xvxvn5j5wml-hello
-
```
-
-
Nix realisations (hereafter referred to as 'builds') are done in isolation to ensure reproducibility.
-
Projects often rely on interacting with package managers to make sure all dependencies are available and may implicitly rely on system configuration at build time.
-
To prevent this, every Nix derivation is built in isolation (without network access or access to the global file system) with only other Nix derivations as inputs.
-
-
> The name Nix is derived from the Dutch word *niks*, meaning nothing; build actions do not see anything that has not been explicitly declared as an input.
-
-
For more information, see:
-
- The Nix paper: https://edolstra.github.io/pubs/nspfssd-lisa2004-final.pdf
-
- The Nix PhD thesis: https://edolstra.github.io/pubs/phd-thesis.pdf
-
-19
docs/nixos.md
···
-
-
# NixOS
-
-
[NixOS](nixos.org) is a Linux distribution built with [Nix](./nix.md) from a modular, purely functional specification.
-
It has no traditional filesystem hierarchy (FSH), like `/bin`, `/lib`, `/usr`, but instead stores all components in `/nix/store`.
-
The system configuration is managed by Nix and configured with Nix expressions.
-
[NixOS modules](https://nixos.org/manual/nixos/stable/index.html#sec-writing-modules) are Nix files containing chunks of system configuration that can be composed to build a full NixOS system.
-
While many NixOS modules are provided in the [Nixpkgs](./nixpkgs.md) repository, they can also be written by an individual user.
-
For example, the expression used to deploy a DNS server is a NixOS module.
-
Together these modules form the configuration which builds the Linux system as a Nix derivation.
-
-
NixOS minimises global mutable state that -- without knowing it -- you might rely on being set up in a certain way.
-
For example, you might follow instructions to run a series of shell commands and edit some files to get a piece of software working.
-
You may subsequently be unable to reproduce the result because you've forgotten some intricacy or are now using a different version of the software.
-
Nix forces you to encode this in a reproducible way, which is extremely useful for replicating software configurations and deployments, aiming to solve the 'It works on my machine' problem.
-
Docker is often used to fix this configuration problem, but Nix aims to be more reproducible.
-
-
Nix provides safe and reliable atomic upgrades and rollbacks.
-
And every new system configuration build creates a GRUB entry, so you can boot previous systems even from your UEFI/BIOS.
-19
docs/nixpkgs.md
···
-
-
### Nixpkgs
-
-
Nixpkgs^[ [github.com/nixos/nixpkgs](https://github.com/nixos/nixpkgs) ] is a large repository of software packaged in [Nix](./nix.md), where every package is a Nix derivation.
-
It also stores all the default [./NixOS](./nixos.md) modules.
-
-
There is also a command line package manager that installs packages from Nixpkgs, which is why people sometimes refer to Nix as a package manager.
-
-
While Nix, and therefore Nix package management, is primarily source-based (since derivations describe how to build software from source), binary deployment is an optimisation of this.
-
Since packages are built in isolation and entirely determined by their inputs, binaries can be transparently deployed by downloading them from a remote server instead of building the derivation locally.
-
-
Nix supports atomic upgrades and rollbacks.
-
The pointers to the new packages are only updated when the install succeeds.
-
-
Due to every
-
-
-
While Nixpkgs also has one global coherent package set, one can use multiple instances of Nixpkgs (i.e., channels) at once to support partial upgrades, as the Nix store allows multiple versions of a dependency to be stored.
-
This also supports atomic upgrades, as all the software's old versions can be kept until garbage collection.
docs/secrets.md

This is a binary file and will not be displayed.

+52
docs/website.md
···
+
+
### Hosting a website
+
+
To host a simple static website stored at `/var/www` at your domain, you can create a `website.nix`:
+
+
```
+
{ config, ... }:
+
+
{
+
services.nginx.virtualHosts."${config.networking.domain}" = {
+
enableACME = true;
+
forceSSL = true;
+
root = "/var/www";
+
};
+
}
+
```
+
+
And import it in `configuration.nix`.
+
+
If you want to build your website with Nix it's possible to add it as a reproducible package.
+
+
```
+
{ config, pkgs, ... }:
+
+
{
+
services.nginx.virtualHosts."${config.networking.domain}" = {
+
enableACME = true;
+
forceSSL = true;
+
root =
+
let website = pkgs.stdenv.mkDerivation rec {
+
name = "website";
+
+
src = pkgs.stdenv.fetchFromGitHub {
+
owner = "<user>";
+
repo = "website";
+
rev = "<hash>";
+
sha256 = "";
+
};
+
+
buildInputs = with pkgs [
+
# dependencies
+
];
+
+
installPhase = ''
+
mkdir $out
+
cp -r * $out
+
'';
+
};
+
in website;
+
};
+
}
+
```
eilean-donan.jpg

This is a binary file and will not be displayed.

+282 -4
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": 1696604326,
-
"narHash": "sha256-YXUNI0kLEcI5g8lqGMb0nh67fY9f2YoJsILafh6zlMo=",
+
"lastModified": 1732981179,
+
"narHash": "sha256-F7thesZPvAMSwjRu0K8uFshTk3ZZSNAsXTIFvXBT+34=",
"owner": "nixos",
"repo": "nixpkgs",
-
"rev": "87828a0e03d1418e848d3dd3f3014a632e4a4f64",
+
"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"
}
}
},
+32 -3
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 ];
+
imports = [
+
./modules/default.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".
+
'';
+
}];
+
};
+
}
+21 -20
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
-
./matrix.nix
+
./fail2ban.nix
+
./matrix/synapse.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;
-
};
-
secretsDir = mkOption {
-
type = path;
-
};
-
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}";
-
networking.firewall.allowedTCPPorts = lib.mkIf config.services.nginx.enable [
+
# 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
];
+23 -34
modules/dns.nix
···
{ config, lib, ... }:
-
let cfg = config.eilean; in
-
{
-
+
with lib;
+
let cfg = config.eilean;
+
in {
+
options.eilean.dns = {
-
enable = lib.mkEnableOption "dns";
-
nameservers = lib.mkOption {
-
type = lib.types.listOf lib.types.string;
+
enable = mkEnableOption "dns";
+
nameservers = mkOption {
+
type = types.listOf types.str;
default = [ "ns1" "ns2" ];
};
};
-
-
config.eilean.services.dns = lib.mkIf cfg.dns.enable {
+
+
config.eilean.services.dns = mkIf cfg.dns.enable {
enable = true;
zones.${config.networking.domain} = {
-
soa.serial = lib.mkDefault 0;
+
soa.serial = mkDefault 0;
records = builtins.concatMap (ns: [
{
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 = "A";
-
data = cfg.serverIpv6;
-
}
-
]) cfg.dns.nameservers ++
-
[
-
{
-
name = "www";
-
type = "CNAME";
-
data = "@";
+
type = "AAAA";
+
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.*
+
'';
+
};
+
};
+
}
+58 -42
modules/gitea.nix
···
{ pkgs, config, lib, ... }:
+
with lib;
let
cfg = config.eilean;
domain = config.networking.domain;
+
subdomain = "git.${domain}";
in {
options.eilean.gitea = {
-
enable = lib.mkEnableOption "gitea";
-
sshPort = lib.mkOption {
-
type = lib.types.int;
+
enable = mkEnableOption "gitea";
+
sshPort = mkOption {
+
type = types.int;
default = 3001;
};
+
databasePasswordFile = mkOption {
+
type = types.nullOr types.path;
+
default = null;
+
};
};
-
config = lib.mkIf cfg.gitea.enable {
+
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
+
}/";
};
};
};
···
enable = true;
user = "git";
appName = "git | ${domain}";
-
mailerPasswordFile = "${config.eilean.secretsDir}/email-pswd-unhashed";
-
domain = "git.${domain}";
-
rootUrl = "https://git.${domain}/";
+
mailerPasswordFile = cfg.mailserver.systemAccountPasswordFile;
settings = {
+
server = {
+
ROOT_URL = "https://${subdomain}/";
+
DOMAIN = subdomain;
+
};
mailer = {
ENABLED = true;
FROM = "git@${domain}";
MAILER_TYPE = "smtp";
HOST = "mail.${domain}:465";
-
USER = "misc@${domain}";
+
USER = "system@${domain}";
IS_TLS_ENABLED = true;
};
repository.DEFAULT_BRANCH = "main";
···
};
database = {
type = "postgres";
-
passwordFile = "${config.eilean.secretsDir}/gitea-db";
+
passwordFile = cfg.gitea.databasePasswordFile;
user = "git";
name = "git";
#createDatabase = true;
···
# https://github.com/NixOS/nixpkgs/issues/103446
systemd.services.gitea.serviceConfig = {
ReadWritePaths = [ "/var/lib/postfix/queue/maildrop" ];
-
NoNewPrivileges = lib.mkForce false;
-
PrivateDevices = lib.mkForce false;
-
PrivateUsers = lib.mkForce false;
-
ProtectHostname = lib.mkForce false;
-
ProtectClock = lib.mkForce false;
-
ProtectKernelTunables = lib.mkForce false;
-
ProtectKernelModules = lib.mkForce false;
-
ProtectKernelLogs = lib.mkForce false;
-
RestrictAddressFamilies = lib.mkForce [ ];
-
LockPersonality = lib.mkForce false;
-
MemoryDenyWriteExecute = lib.mkForce false;
-
RestrictRealtime = lib.mkForce false;
-
RestrictSUIDSGID = lib.mkForce false;
-
SystemCallArchitectures = lib.mkForce "";
-
SystemCallFilter = lib.mkForce [];
+
NoNewPrivileges = mkForce false;
+
PrivateDevices = mkForce false;
+
PrivateUsers = mkForce false;
+
ProtectHostname = mkForce false;
+
ProtectClock = mkForce false;
+
ProtectKernelTunables = mkForce false;
+
ProtectKernelModules = mkForce false;
+
ProtectKernelLogs = mkForce false;
+
RestrictAddressFamilies = mkForce [ ];
+
LockPersonality = mkForce false;
+
MemoryDenyWriteExecute = mkForce false;
+
RestrictRealtime = mkForce false;
+
RestrictSUIDSGID = mkForce false;
+
SystemCallArchitectures = 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
+
}
'';
};
+22 -24
modules/headscale.nix
···
{ pkgs, config, lib, ... }:
-
let
-
cfg = config.eilean;
+
with lib;
+
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}";
};
};
-
config = lib.mkIf cfg.headscale.enable {
+
config = mkIf cfg.headscale.enable {
# To set up:
# `headscale namespaces create <namespace_name>`
# To add a node:
···
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" ];
};
};
-
services.nginx.virtualHosts.${cfg.headscale.domain} = {
-
forceSSL = true;
-
enableACME = true;
-
locations."/" = {
-
proxyPass = with config.services.headscale;
-
"http://${address}:${toString port}";
-
proxyWebsockets = true;
+
services.nginx = {
+
enable = true;
+
virtualHosts.${cfg.headscale.domain} = {
+
forceSSL = true;
+
enableACME = true;
+
locations."/" = {
+
proxyPass = with config.services.headscale;
+
"http://${address}:${toString port}";
+
proxyWebsockets = true;
+
};
};
};
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" hash;
-
in
-
lib.mapAttrs (name: value:
-
if value.hashedPasswordFile == null then
-
builtins.toString (mkHashFile name value.hashedPassword)
-
else value.hashedPasswordFile) cfg.loginAccounts;
-
}
-4
modules/mailserver/debug.nix
···
-
{ config, lib, ... }:
-
{
-
mailserver.policydSPFExtraConfig = lib.mkIf config.mailserver.debug "debugLevel = 4";
-
}
-1043
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";
-
};
-
-
hashedPassword = mkOption {
-
type = with types; nullOr str;
-
default = null;
-
example = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
-
description = ''
-
The user's hashed password. Use `htpasswd` as follows
-
-
```
-
nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
-
```
-
-
Warning: this is stored in plaintext in the Nix store!
-
Use `hashedPasswordFile` instead.
-
'';
-
};
-
-
hashedPasswordFile = mkOption {
-
type = with types; nullOr path;
-
default = null;
-
example = "/run/keys/user1-passwordhash";
-
description = ''
-
A file containing the user's hashed password. Use `htpasswd` as follows
-
-
```
-
nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
-
```
-
'';
-
};
-
-
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 = {
-
hashedPassword = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
-
};
-
user2 = {
-
hashedPassword = "$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
-324
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:
-
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}:${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.hashedPassword != null || acct.hashedPasswordFile != null);
-
message = "${acct.name} must provide either a hashed password or a password hash file";
-
}) (lib.attrValues loginAccounts));
-
-
# warn for accounts that specify both password and file
-
warnings = (map
-
(acct: "${acct.name} specifies both a password hash and hash file; hash file will be used")
-
(lib.filter
-
(acct: (acct.hashedPassword != null && acct.hashedPasswordFile != 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;
-
};
-
};
-
}
+57 -34
modules/mailserver.nix
···
-
{ config, lib, ... }:
+
{ pkgs, config, lib, ... }:
+
with lib;
let
cfg = config.eilean;
domain = config.networking.domain;
+
subdomain = "mail.${domain}";
in {
-
options.eilean.mailserver.enable = lib.mkEnableOption "mailserver";
+
options.eilean.mailserver = {
+
enable = mkEnableOption "mailserver";
+
systemAccountPasswordFile = mkOption {
+
type = types.nullOr types.str;
+
default = null;
+
};
+
};
+
+
config = mkIf cfg.mailserver.enable {
+
security.acme-eon.certs."${subdomain}" = lib.mkIf cfg.acme-eon {
+
group = "turnserver";
+
reloadServices = [ "postfix.service" "dovecot.service" ];
+
};
-
config = lib.mkIf cfg.mailserver.enable {
mailserver = {
enable = true;
-
fqdn = "mail.${domain}";
+
fqdn = subdomain;
domains = [ "${domain}" ];
-
# A list of all login accounts. To create the password hashes, use
-
# nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
loginAccounts = {
-
"${cfg.username}@${domain}" = {
-
hashedPasswordFile = "${config.eilean.secretsDir}/email-pswd";
-
aliases = [
-
"dns@${domain}"
-
"postmaster@${domain}"
-
];
-
};
-
"misc@${domain}" = {
-
hashedPasswordFile = "${config.eilean.secretsDir}/email-pswd";
-
aliases = [
-
"git@${domain}"
-
"mastodon@${domain}"
-
];
-
catchAll = [ "${domain}" ];
-
};
+
"system@${domain}" = {
+
passwordFile = cfg.mailserver.systemAccountPasswordFile;
+
aliases = [
+
(mkIf cfg.gitea.enable "git@${domain}")
+
(mkIf cfg.mastodon.enable "mastodon@${domain}")
+
];
+
};
};
# 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;
};
+
services.nginx.enable = true;
services.nginx.virtualHosts."${config.mailserver.fqdn}".extraConfig = ''
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=none\"";
+
value = ''"v=DMARC1; p=reject"'';
}
];
};
+35 -31
modules/mastodon.nix
···
{ pkgs, config, lib, ... }:
+
with lib;
let
cfg = config.eilean;
domain = config.networking.domain;
+
subdomain = "mastodon.${domain}";
in {
-
options.eilean.mastodon.enable = lib.mkEnableOption "mastodon";
+
options.eilean.mastodon = { enable = mkEnableOption "mastodon"; };
-
config = lib.mkIf cfg.mastodon.enable {
+
config = mkIf cfg.mastodon.enable {
services.mastodon = {
enable = true;
enableUnixSocket = false;
webProcesses = 1;
webThreads = 3;
sidekiqThreads = 5;
+
streamingProcesses = 3;
smtp = {
#createLocally = false;
-
user = "misc@${domain}";
+
user = "system@${domain}";
port = 465;
host = "mail.${domain}";
authenticate = true;
-
passwordFile = "${config.eilean.secretsDir}/email-pswd-unhashed";
+
passwordFile = cfg.mailserver.systemAccountPasswordFile;
fromAddress = "mastodon@${domain}";
};
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;
recommendedProxySettings = true;
virtualHosts = {
# relies on root domain being set up
-
"${domain}".locations."/.well-known/host-meta".extraConfig = ''
-
return 301 https://mastodon.${domain}$request_uri;
-
'';
-
"mastodon.${domain}" = {
+
"${domain}".locations = {
+
"/.well-known/host-meta".extraConfig = ''
+
return 301 https://${subdomain}$request_uri;
+
'';
+
"/.well-known/webfinger".extraConfig = ''
+
return 301 https://${subdomain}$request_uri;
+
'';
+
};
+
"${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}";
-
proxyWebsockets = true;
-
};
-
-
locations."/api/v1/streaming/" = {
-
proxyPass = "http://127.0.0.1:${builtins.toString config.services.mastodon.streamingPort}/";
+
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;
+
}];
};
}
+188
modules/matrix/mautrix-instagram.nix
···
+
{ 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 { };
+
appservicePort = 29319;
+
+
mkDefaults = lib.mapAttrsRecursive (n: v: lib.mkDefault v);
+
defaultConfig = {
+
homeserver.address = "http://localhost:8448";
+
meta.mode = "instagram";
+
appservice = {
+
hostname = "[::]";
+
port = appservicePort;
+
database.type = "sqlite3";
+
database.uri = "${dataDir}/mautrix-instagram.db";
+
id = "instagram";
+
bot.username = "instagrambot";
+
bot.displayname = "Instagram Bridge Bot";
+
bot.avatar = "mxc://maunium.net/JxjlbZUlCPULEeHZSwleUXQv";
+
as_token = "";
+
hs_token = "";
+
};
+
bridge = {
+
username_template = "instagram_{{.}}";
+
double_puppet_server_map = { };
+
login_shared_secret_map = { };
+
permissions."*" = "relay";
+
relay.enabled = true;
+
};
+
logging = {
+
min_level = "info";
+
writers = lib.singleton {
+
type = "stdout";
+
format = "pretty-colored";
+
time_format = " ";
+
};
+
};
+
};
+
+
in {
+
options.services.mautrix-instagram = {
+
enable = lib.mkEnableOption (lib.mdDoc
+
"mautrix-instagram, a puppeting/relaybot bridge between Matrix and Instagram.");
+
+
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/instagram/blob/master/example-config.yaml).
+
'';
+
example = {
+
appservice = {
+
database = {
+
type = "postgres";
+
uri = "postgresql:///mautrix_instagram?host=/run/postgresql";
+
};
+
id = "instagram";
+
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 {
+
+
users.users.mautrix-instagram = {
+
isSystemUser = true;
+
group = "mautrix-instagram";
+
home = dataDir;
+
description = "Mautrix-Instagram bridge user";
+
};
+
+
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;
+
}
+
]);
+
+
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;
+
+
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-meta}/bin/mautrix-meta \
+
--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 = {
+
User = "mautrix-instagram";
+
Group = "mautrix-instagram";
+
StateDirectory = baseNameOf dataDir;
+
WorkingDirectory = dataDir;
+
ExecStart = ''
+
${pkgs.mautrix-meta}/bin/mautrix-meta \
+
--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 ];
+
};
+
};
+
}
+188
modules/matrix/mautrix-messenger.nix
···
+
{ 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 { };
+
appservicePort = 29320;
+
+
mkDefaults = lib.mapAttrsRecursive (n: v: lib.mkDefault v);
+
defaultConfig = {
+
homeserver.address = "http://localhost:8448";
+
meta.mode = "messenger";
+
appservice = {
+
hostname = "[::]";
+
port = appservicePort;
+
database.type = "sqlite3";
+
database.uri = "${dataDir}/mautrix-messenger.db";
+
id = "messenger";
+
bot.username = "messengerbot";
+
bot.displayname = "Messenger Bridge Bot";
+
bot.avatar = "mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak";
+
as_token = "";
+
hs_token = "";
+
};
+
bridge = {
+
username_template = "messenger_{{.}}";
+
double_puppet_server_map = { };
+
login_shared_secret_map = { };
+
permissions."*" = "relay";
+
relay.enabled = true;
+
};
+
logging = {
+
min_level = "info";
+
writers = lib.singleton {
+
type = "stdout";
+
format = "pretty-colored";
+
time_format = " ";
+
};
+
};
+
};
+
+
in {
+
options.services.mautrix-messenger = {
+
enable = lib.mkEnableOption (lib.mdDoc
+
"mautrix-messenger, a puppeting/relaybot bridge between Matrix and Messenger.");
+
+
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/messenger/blob/master/example-config.yaml).
+
'';
+
example = {
+
appservice = {
+
database = {
+
type = "postgres";
+
uri = "postgresql:///mautrix_messenger?host=/run/postgresql";
+
};
+
id = "messenger";
+
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 {
+
+
users.users.mautrix-messenger = {
+
isSystemUser = true;
+
group = "mautrix-messenger";
+
home = dataDir;
+
description = "Mautrix-Messenger bridge user";
+
};
+
+
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;
+
}
+
]);
+
+
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;
+
+
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-meta}/bin/mautrix-meta \
+
--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 = {
+
User = "mautrix-messenger";
+
Group = "mautrix-messenger";
+
StateDirectory = baseNameOf dataDir;
+
WorkingDirectory = dataDir;
+
ExecStart = ''
+
${pkgs.mautrix-meta}/bin/mautrix-meta \
+
--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 ];
+
};
+
};
+
}
+249
modules/matrix/synapse.nix
···
+
{ config, pkgs, lib, ... }:
+
+
with lib;
+
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";
+
turn = mkOption {
+
type = types.bool;
+
default = true;
+
};
+
registrationSecretFile = mkOption {
+
type = types.nullOr types.str;
+
default = null;
+
};
+
bridges = {
+
whatsapp = mkOption {
+
type = types.bool;
+
default = false;
+
description = "Enable WhatsApp bridge.";
+
};
+
signal = mkOption {
+
type = types.bool;
+
default = false;
+
description = "Enable Signal bridge.";
+
};
+
instagram = mkOption {
+
type = types.bool;
+
default = false;
+
description = "Enable Instagram bridge.";
+
};
+
messenger = mkOption {
+
type = types.bool;
+
default = false;
+
description = "Enable Facebook Messenger bridge.";
+
};
+
};
+
};
+
+
config = mkIf cfg.matrix.enable {
+
services.postgresql.enable = true;
+
services.postgresql.package = pkgs.postgresql_13;
+
services.postgresql.initialScript = pkgs.writeText "synapse-init.sql" ''
+
CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse';
+
CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse"
+
TEMPLATE template0
+
LC_COLLATE = "C"
+
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,
+
# but the rest make sense as well
+
recommendedTlsSettings = true;
+
recommendedOptimisation = true;
+
recommendedGzipSettings = true;
+
recommendedProxySettings = true;
+
+
virtualHosts = {
+
# This host section can be placed on a different host than the rest,
+
# i.e. to delegate from the host being accessible as ${domain}
+
# to another host actually running the Matrix homeserver.
+
"${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" = "${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}';
+
'';
+
};
+
+
# Reverse proxy for Matrix client-server and server-server communication
+
"${subdomain}" = {
+
enableACME = lib.mkIf (!cfg.acme-eon) true;
+
forceSSL = true;
+
+
# Or do a redirect instead of the 404, or whatever is appropriate for you.
+
# But do not put a Matrix Web client here! See the Element web section below.
+
locations."/".extraConfig = ''
+
return 404;
+
'';
+
+
# forward all Matrix API calls to the synapse Matrix homeserver
+
locations."~ ^(\\/_matrix|\\/_synapse\\/client)" = {
+
proxyPass = "http://127.0.0.1:8008";
+
};
+
};
+
};
+
};
+
+
services.matrix-synapse = {
+
enable = true;
+
settings = mkMerge [
+
{
+
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;
+
}];
+
}];
+
max_upload_size = "100M";
+
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:${realm}:3478?transport=udp"
+
"turn:${realm}:3478?transport=tcp"
+
"turns:${realm}:5349?transport=udp"
+
"turns:${realm}:5349?transport=tcp"
+
];
+
turn_user_lifetime = "1h";
+
})
+
];
+
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}'
+
'';
+
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 =
+
# 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://${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}:${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://${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}:${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://${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}:${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://${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}:${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.${domain}.records = [{
+
name = "matrix";
+
type = "CNAME";
+
value = cfg.domainName;
+
}];
+
};
+
}
-134
modules/matrix.nix
···
-
{ config, pkgs, lib, ... }:
-
-
let cfg = config.eilean; in
-
{
-
options.eilean.matrix = {
-
enable = lib.mkEnableOption "matrix";
-
turn = lib.mkOption {
-
type = lib.types.bool;
-
default = true;
-
};
-
};
-
-
config = lib.mkIf cfg.matrix.enable {
-
services.postgresql.enable = true;
-
services.postgresql.package = pkgs.postgresql_13;
-
services.postgresql.initialScript = pkgs.writeText "synapse-init.sql" ''
-
CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse';
-
CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse"
-
TEMPLATE template0
-
LC_COLLATE = "C"
-
LC_CTYPE = "C";
-
'';
-
-
services.nginx = {
-
enable = true;
-
# only recommendedProxySettings and recommendedGzipSettings are strictly required,
-
# but the rest make sense as well
-
recommendedTlsSettings = true;
-
recommendedOptimisation = true;
-
recommendedGzipSettings = true;
-
recommendedProxySettings = true;
-
-
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}
-
# to another host actually running the Matrix homeserver.
-
"${config.networking.domain}" = {
-
enableACME = 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 ''
-
add_header Content-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"; };
-
};
-
# ACAO required to allow element-web on any URL to request this json file
-
in ''
-
add_header Content-Type application/json;
-
add_header Access-Control-Allow-Origin *;
-
return 200 '${builtins.toJSON client}';
-
'';
-
};
-
-
# Reverse proxy for Matrix client-server and server-server communication
-
"matrix.${config.networking.domain}" = {
-
enableACME = true;
-
forceSSL = true;
-
-
# Or do a redirect instead of the 404, or whatever is appropriate for you.
-
# But do not put a Matrix Web client here! See the Element web section below.
-
locations."/".extraConfig = ''
-
return 404;
-
'';
-
-
# 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 /
-
};
-
};
-
};
-
};
-
-
services.matrix-synapse = {
-
enable = true;
-
settings = lib.mkMerge [
-
{
-
server_name = config.networking.domain;
-
enable_registration = true;
-
registration_requires_token = true;
-
auto_join_rooms = [ "#freumh:freumh.org" ];
-
registration_shared_secret_path = "${config.eilean.secretsDir}/matrix-shared-secret";
-
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";
-
}
-
(lib.mkIf cfg.matrix.turn {
-
turn_uris = with config.services.coturn; [
-
"turn:${realm}:3478?transport=udp"
-
"turn:${realm}:3478?transport=tcp"
-
"turns:${realm}:5349?transport=udp"
-
"turns:${realm}:5349?transport=tcp"
-
];
-
turn_user_lifetime = "1h";
-
})
-
];
-
extraConfigFiles = [ "${config.eilean.secretsDir}/matrix-turn-shared-secret" ];
-
};
-
-
eilean.turn.enable = lib.mkIf cfg.matrix.turn true;
-
-
eilean.dns.enable = true;
-
eilean.services.dns.zones.${config.networking.domain}.records = [
-
{
-
name = "matrix";
-
type = "CNAME";
-
data = "vps";
-
}
-
];
-
};
-
}
+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;
+
}];
+
};
+
}
+35 -13
modules/services/dns/bind.nix
···
{ pkgs, config, lib, ... }:
-
let cfg = config.eilean.services.dns; in {
-
services.bind = 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 = "${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:
+
let
+
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;
}
+17 -29
modules/services/dns/default.nix
···
{ pkgs, config, lib, ... }:
with lib;
-
let
zoneOptions.options = {
ttl = mkOption {
···
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;
-
};
-
};
-
in mkOption {
-
type = with types; listOf (submodule recordOpts);
-
default = [ ];
+
type = mkOption { type = types.str; };
+
value = mkOption { type = types.str; };
};
+
in mkOption {
+
type = with types; listOf (submodule recordOpts);
+
default = [ ];
+
};
};
-
in
-
{
-
imports = [ ./bind.nix ];
+
in {
+
imports = [ ./bind.nix ./eon.nix ];
options.eilean.services.dns = {
-
enable = lib.mkEnableOption "DNS server";
+
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 = lib.mkIf config.eilean.services.dns.openFirewall {
+
config.networking.firewall = mkIf config.eilean.services.dns.openFirewall {
allowedTCPPorts = [ 53 ];
allowedUDPPorts = [ 53 ];
};
+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)}
'';
}
+62 -29
modules/turn.nix
···
{ config, pkgs, lib, ... }:
+
with lib;
let
cfg = config.eilean;
domain = config.networking.domain;
-
in
-
{
-
options.eilean.turn.enable = lib.mkEnableOption "TURN server";
+
subdomain = "turn.${domain}";
+
staticAuthSecretFile = "/run/coturn/static-auth-secret";
+
in {
+
options.eilean.turn = { enable = mkEnableOption "TURN server"; };
+
+
config = mkIf cfg.turn.enable {
+
security.acme-eon.certs."${subdomain}" = lib.mkIf cfg.acme-eon {
+
group = "turnserver";
+
reloadServices = [ "coturn" ];
+
};
-
config = lib.mkIf cfg.turn.enable {
-
services.coturn = rec {
+
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 = "${config.eilean.secretsDir}/coturn";
-
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";
+
static-auth-secret-file = staticAuthSecretFile;
+
realm = subdomain;
+
relay-ips = with config.eilean; [ serverIpv4 serverIpv6 ];
+
cert = "${certDir}/fullchain.pem";
+
pkey = "${certDir}/key.pem";
+
};
+
+
systemd.services = {
+
coturn-static-auth-secret-generator = {
+
description = "Generate coturn static auth secret file";
+
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 -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" ]
+
++ 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.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;
+
}];
};
}
+82 -53
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;
···
type = with types; nullOr int;
default = null;
};
+
privateKeyFile = mkOption {
+
type = types.nullOr types.str;
+
default = null;
+
};
};
};
-
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 = "${config.eilean.secretsDir}/wireguard-key-${hostName}";
-
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;
-
};
-
};
-
}
+31
pkgs/mautrix-meta.nix
···
+
{ lib, buildGoModule, fetchFromGitHub, olm }:
+
+
let version = "0.4.4";
+
in buildGoModule rec {
+
name = "mautrix-meta";
+
inherit version;
+
+
src = fetchFromGitHub {
+
owner = "mautrix";
+
repo = "meta";
+
rev = "v${version}";
+
hash = "sha256-S8x3TGQEs+oh/3Q1Gz00M8dOcjjuHSgzVhqlbikZ8QE=";
+
};
+
+
buildInputs = [ olm ];
+
+
vendorHash = "sha256-sUnvwPJQOoVzxbo2lS3CRcTrWsPjgYPsKClVw1wZJdM=";
+
+
doCheck = false;
+
+
excludedPackages = "cmd/lscli";
+
+
meta = with lib; {
+
homepage = "https://github.com/mautrix/meta";
+
description =
+
" A Matrix-Facebook Messenger and Instagram DM puppeting bridge.";
+
license = licenses.agpl3Plus;
+
mainProgram = "mautrix-meta";
+
};
+
}
+
+4 -7
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";
···
# mastodon.enable = true;
# gitea.enable = true;
# headscale.enable = true;
-
-
# secretsDir = "/secrets";
};
# This value determines the NixOS release from which the default
···
# 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?
-
}
+
}
+24 -10
template/flake.lock
···
"nodes": {
"eilean": {
"inputs": {
-
"nixpkgs": [
-
"nixpkgs"
-
]
+
"nixpkgs": "nixpkgs"
},
"locked": {
-
"lastModified": 1677678055,
-
"narHash": "sha256-Sf+Hn8tMPudNu+MEWcPaGBs5mqg+b72nB520RTZlLmE=",
+
"lastModified": 1702895145,
+
"narHash": "sha256-fr0yXkfLXbs6W3CSAYXzu+d0KZ76lJORZSdXuu9IVWA=",
"owner": "RyanGibb",
"repo": "eilean-nix",
-
"rev": "0b0a552480c78be16466c7b4b0e7d90de1862fd9",
+
"rev": "8cb0bc49c1d252a78512f4d85a914061c22c8684",
"type": "github"
},
"original": {
···
},
"nixpkgs": {
"locked": {
-
"lastModified": 1677676435,
-
"narHash": "sha256-6FxdcmQr5JeZqsQvfinIMr0XcTyTuR7EXX0H3ANShpQ=",
+
"lastModified": 1696604326,
+
"narHash": "sha256-YXUNI0kLEcI5g8lqGMb0nh67fY9f2YoJsILafh6zlMo=",
"owner": "nixos",
"repo": "nixpkgs",
-
"rev": "a08d6979dd7c82c4cef0dcc6ac45ab16051c1169",
+
"rev": "87828a0e03d1418e848d3dd3f3014a632e4a4f64",
+
"type": "github"
+
},
+
"original": {
+
"owner": "nixos",
+
"ref": "nixos-unstable",
+
"repo": "nixpkgs",
+
"type": "github"
+
}
+
},
+
"nixpkgs_2": {
+
"locked": {
+
"lastModified": 1703068421,
+
"narHash": "sha256-WSw5Faqlw75McIflnl5v7qVD/B3S2sLh+968bpOGrWA=",
+
"owner": "NixOS",
+
"repo": "nixpkgs",
+
"rev": "d65bceaee0fb1e64363f7871bc43dc1c6ecad99f",
"type": "github"
},
"original": {
···
"root": {
"inputs": {
"eilean": "eilean",
-
"nixpkgs": "nixpkgs"
+
"nixpkgs": "nixpkgs_2"
}
}
},
+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;
+
}
+
];
};
-
}
+
};
+
}