modules: move 'services' into subfolder

This commit is contained in:
Bruno BELANYI 2021-05-29 16:46:51 +02:00
parent 274b909971
commit 9b568beb9a
27 changed files with 1 additions and 2 deletions

View file

@ -10,6 +10,7 @@
./media.nix
./nix.nix
./packages.nix
./services
./users.nix
];
}

View file

@ -0,0 +1,72 @@
{ config, lib, pkgs, ... }:
let
wgCfg = config.my.services.wireguard;
cfg = config.my.services.adblock;
in
{
options.my.services.adblock = with lib; {
enable = mkEnableOption "Hosts-based adblock using unbound";
forwardAddresses = mkOption {
type = with types; listOf str;
default = [
"1.0.0.1@853#cloudflare-dns.com"
"1.1.1.1@853#cloudflare-dns.com"
];
example = [
"8.8.4.4"
"8.8.8.8"
];
description = "Which DNS servers to forward queries to";
};
interfaces = mkOption {
type = with types; listOf str;
default = [
"0.0.0.0"
"::"
];
example = literalExample ''
[
"127.0.0.1"
]
'';
description = "Which addresses to listen on";
};
};
config = lib.mkIf cfg.enable {
# Allow wireguard clients to connect to it
networking.firewall.interfaces."${wgCfg.iface}" = {
allowedUDPPorts = [ 53 ];
allowedTCPPorts = [ 53 ];
};
services.unbound = {
enable = true;
settings = {
server = {
access-control = [
"127.0.0.0/24 allow"
"${wgCfg.net.v4.subnet}.0/${toString wgCfg.net.v4.mask} allow"
"${wgCfg.net.v6.subnet}::0/${toString wgCfg.net.v6.mask} allow"
];
interface = cfg.interfaces;
so-reuseport = true;
tls-cert-bundle = "/etc/ssl/certs/ca-certificates.crt";
tls-upstream = true;
include = "${pkgs.ambroisie.unbound-zones-adblock}/hosts";
};
forward-zone = [{
name = ".";
forward-addr = cfg.forwardAddresses;
}];
};
};
};
}

106
modules/services/backup.nix Normal file
View file

@ -0,0 +1,106 @@
# Backups using Backblaze B2 and `restic`
{ config, pkgs, lib, ... }:
let
cfg = config.my.services.backup;
excludeArg = with builtins; with pkgs; "--exclude-file=" +
(writeText "excludes.txt" (concatStringsSep "\n" cfg.exclude));
in
{
options.my.services.backup = with lib; {
enable = mkEnableOption "Enable backups for this host";
repository = mkOption {
type = types.str;
example = "/mnt/backup-hdd";
description = "The repository to back up to";
};
passwordFile = mkOption {
type = types.str;
example = "/var/lib/restic/password.txt";
description = "Read the repository's password from this path";
};
credentialsFile = mkOption {
type = types.str;
example = "/var/lib/restic/creds.env";
description = ''
Credential file as an 'EnvironmentFile' (see `systemd.exec(5)`)
'';
};
paths = mkOption {
type = with types; listOf str;
default = [ ];
example = [
"/var/lib"
"/home"
];
description = "Paths to backup";
};
exclude = mkOption {
type = with types; listOf str;
default = [ ];
example = [
# very large paths
"/var/lib/docker"
"/var/lib/systemd"
"/var/lib/libvirt"
# temporary files created by `cargo` and `go build`
"**/target"
"/home/*/go/bin"
"/home/*/go/pkg"
];
description = "Paths to exclude from backup";
};
pruneOpts = mkOption {
type = with types; listOf str;
default = [
"--keep-last 10"
"--keep-hourly 24"
"--keep-daily 7"
"--keep-weekly 5"
"--keep-monthly 12"
"--keep-yearly 100"
];
example = [ "--keep-last 5" "--keep-weekly 2" ];
description = ''
List of options to give to the `forget` subcommand after a backup.
'';
};
timerConfig = mkOption {
# NOTE: I do not know how to cleanly set the type
default = {
OnCalendar = "daily";
};
example = {
OnCalendar = "00:05";
RandomizedDelaySec = "5h";
};
description = ''
When to run the backup. See man systemd.timer for details.
'';
};
};
config = lib.mkIf cfg.enable {
services.restic.backups.backblaze = {
# Take care of included and excluded files
paths = cfg.paths;
extraOptions = with builtins; with lib;[
(optionalString ((length cfg.exclude) != 0) excludeArg)
];
# Take care of creating the repository if it doesn't exist
initialize = true;
# Hijack S3-related env to give B2 API key
s3CredentialsFile = cfg.credentialsFile;
inherit (cfg) passwordFile pruneOpts timerConfig repository;
};
};
}

39
modules/services/blog.nix Normal file
View file

@ -0,0 +1,39 @@
# My blog setup
{ config, lib, ... }:
let
cfg = config.my.services.blog;
domain = config.networking.domain;
makeHostInfo = name: {
name = "${name}.${domain}";
value = "/var/www/${name}";
};
hostsInfo = [
{
name = domain;
value = "/var/www/blog";
}
] ++ builtins.map makeHostInfo [ "cv" "dev" "key" ];
hosts = builtins.listToAttrs hostsInfo;
makeVirtualHost = with lib.attrsets;
name: root: nameValuePair "${name}" {
forceSSL = true;
useACMEHost = domain;
inherit root;
# Make my blog the default landing site
default = (name == domain);
};
in
{
options.my.services.blog = {
enable = lib.mkEnableOption "Blog hosting";
};
config = lib.mkIf cfg.enable {
services.nginx.virtualHosts = with lib.attrsets;
mapAttrs' makeVirtualHost hosts;
};
}

View file

@ -0,0 +1,56 @@
{ config, lib, ... }:
let
cfg = config.my.services.calibre-web;
domain = config.networking.domain;
calibreDomain = "library.${domain}";
in
{
options.my.services.calibre-web = with lib; {
enable = mkEnableOption "Calibre-web server";
port = mkOption {
type = types.port;
default = 8083;
example = 8080;
description = "Internal port for webui";
};
libraryPath = mkOption {
type = with types; either path str;
example = /data/media/library;
description = "Path to the Calibre library to use";
};
};
config = lib.mkIf cfg.enable {
services.calibre-web = {
enable = true;
listen = {
ip = "127.0.0.1";
port = cfg.port;
};
group = "media";
options = {
calibreLibrary = cfg.libraryPath;
enableBookConversion = true;
};
};
services.nginx.virtualHosts."${calibreDomain}" = {
forceSSL = true;
useACMEHost = domain;
locations."/".proxyPass = "http://127.0.0.1:${toString cfg.port}/";
};
my.services.backup = {
paths = [
"/var/lib/calibre-web" # For `app.db` and `gdrive.db`
cfg.libraryPath
];
};
};
}

View file

@ -0,0 +1,30 @@
{ ... }:
{
imports = [
./adblock.nix
./backup.nix
./blog.nix
./calibre-web.nix
./drone.nix
./flood.nix
./gitea.nix
./indexers.nix
./jellyfin.nix
./lohr.nix
./matrix.nix
./miniflux.nix
./nextcloud.nix
./nginx.nix
./pirate.nix
./podgrab.nix
./postgresql-backup.nix
./quassel.nix
./rss-bridge.nix
./sabnzbd.nix
./ssh-server.nix
./tlp.nix
./transmission.nix
./wireguard.nix
];
}

196
modules/services/drone.nix Normal file
View file

@ -0,0 +1,196 @@
# A docker-based CI/CD system
#
# Inspired by [1]
# [1]: https://github.com/Mic92/dotfiles/blob/master/nixos/eve/modules/drone.nix
{ config, lib, pkgs, ... }:
let
cfg = config.my.services.drone;
domain = config.networking.domain;
droneDomain = "drone.${domain}";
hasRunner = (name: builtins.elem name cfg.runners);
execPkg = pkgs.drone-runner-exec;
dockerPkg = pkgs.drone-runner-docker;
in
{
options.my.services.drone = with lib; {
enable = mkEnableOption "Drone CI";
runners = mkOption {
type = with types; listOf (enum [ "exec" "docker" ]);
default = [ ];
example = [ "exec" "docker" ];
description = "Types of runners to enable";
};
admin = mkOption {
type = types.str;
default = "ambroisie";
example = "admin";
description = "Name of the admin user";
};
port = mkOption {
type = types.port;
default = 3030;
example = 8080;
description = "Internal port of the Drone UI";
};
secretFile = mkOption {
type = types.str;
example = "/run/secrets/drone-gitea.env";
description = "Secrets to inject into Drone server";
};
sharedSecretFile = mkOption {
type = types.str;
example = "/run/secrets/drone-rpc.env";
description = "Shared RPC secret to inject into server and runners";
};
};
config = lib.mkIf cfg.enable {
systemd.services.drone-server = {
wantedBy = [ "multi-user.target" ];
after = [ "postgresql.service" ];
serviceConfig = {
EnvironmentFile = [
cfg.secretFile
cfg.sharedSecretFile
];
Environment = [
"DRONE_DATABASE_DATASOURCE=postgres:///drone?host=/run/postgresql"
"DRONE_SERVER_HOST=${droneDomain}"
"DRONE_SERVER_PROTO=https"
"DRONE_DATABASE_DRIVER=postgres"
"DRONE_SERVER_PORT=:${toString cfg.port}"
"DRONE_USER_CREATE=username:${cfg.admin},admin:true"
"DRONE_JSONNET_ENABLED=true"
"DRONE_STARLARK_ENABLED=true"
];
ExecStart = "${pkgs.drone}/bin/drone-server";
User = "drone";
Group = "drone";
};
};
users.users.drone = {
isSystemUser = true;
createHome = true;
group = "drone";
};
users.groups.drone = { };
services.postgresql = {
ensureDatabases = [ "drone" ];
ensureUsers = [{
name = "drone";
ensurePermissions = {
"DATABASE drone" = "ALL PRIVILEGES";
};
}];
};
services.nginx.virtualHosts."${droneDomain}" = {
forceSSL = true;
useACMEHost = domain;
locations."/".proxyPass = "http://127.0.0.1:${toString cfg.port}";
};
# Docker runner
systemd.services.drone-runner-docker = lib.mkIf (hasRunner "docker") {
wantedBy = [ "multi-user.target" ];
after = [ "docker.socket" ]; # Needs the socket to be available
# might break deployment
restartIfChanged = false;
confinement.enable = true;
serviceConfig = {
Environment = [
"DRONE_SERVER_HOST=${droneDomain}"
"DRONE_SERVER_PROTO=https"
"DRONE_RUNNER_CAPACITY=10"
"CLIENT_DRONE_RPC_HOST=127.0.0.1:${toString cfg.port}"
];
BindPaths = [
"/var/run/docker.sock"
];
EnvironmentFile = [
cfg.sharedSecretFile
];
ExecStart = "${dockerPkg}/bin/drone-runner-docker";
User = "drone-runner-docker";
Group = "drone-runner-docker";
};
};
# Make sure it is activated in that case
virtualisation.docker.enable = lib.mkIf (hasRunner "docker") true;
users.users.drone-runner-docker = lib.mkIf (hasRunner "docker") {
isSystemUser = true;
group = "drone-runner-docker";
extraGroups = [ "docker" ]; # Give access to the daemon
};
users.groups.drone-runner-docker = lib.mkIf (hasRunner "docker") { };
# Exec runner
systemd.services.drone-runner-exec = lib.mkIf (hasRunner "exec") {
wantedBy = [ "multi-user.target" ];
# might break deployment
restartIfChanged = false;
confinement.enable = true;
confinement.packages = with pkgs; [
git
gnutar
bash
nixUnstable
gzip
];
path = with pkgs; [
git
gnutar
bash
nixUnstable
gzip
];
serviceConfig = {
Environment = [
"DRONE_SERVER_HOST=${droneDomain}"
"DRONE_SERVER_PROTO=https"
"DRONE_RUNNER_CAPACITY=10"
"CLIENT_DRONE_RPC_HOST=127.0.0.1:${toString cfg.port}"
"NIX_REMOTE=daemon"
"PAGER=cat"
];
BindPaths = [
"/nix/var/nix/daemon-socket/socket"
"/run/nscd/socket"
];
BindReadOnlyPaths = [
"/etc/resolv.conf:/etc/resolv.conf"
"/etc/resolvconf.conf:/etc/resolvconf.conf"
"/etc/passwd:/etc/passwd"
"/etc/group:/etc/group"
"/nix/var/nix/profiles/system/etc/nix:/etc/nix"
"${config.environment.etc."ssl/certs/ca-certificates.crt".source}:/etc/ssl/certs/ca-certificates.crt"
"${config.environment.etc."ssh/ssh_known_hosts".source}:/etc/ssh/ssh_known_hosts"
"/etc/machine-id"
# channels are dynamic paths in the nix store, therefore we need to bind mount the whole thing
"/nix/"
];
EnvironmentFile = [
cfg.sharedSecretFile
];
ExecStart = "${execPkg}/bin/drone-runner-exec";
User = "drone-runner-exec";
Group = "drone-runner-exec";
};
};
users.users.drone-runner-exec = lib.mkIf (hasRunner "exec") {
isSystemUser = true;
group = "drone-runner-exec";
};
users.groups.drone-runner-exec = lib.mkIf (hasRunner "exec") { };
};
}

View file

@ -0,0 +1,53 @@
# A nice UI for various torrent clients
{ config, lib, pkgs, ... }:
let
cfg = config.my.services.flood;
domain = config.networking.domain;
webuiDomain = "flood.${domain}";
in
{
options.my.services.flood = with lib; {
enable = mkEnableOption "Flood UI";
port = mkOption {
type = types.port;
default = 9092;
example = 3000;
description = "Internal port for Flood UI";
};
stateDir = mkOption {
type = types.str;
default = "flood";
example = "floodUI";
description = "Directory under `/var/run` for storing Flood's files";
};
};
config = lib.mkIf cfg.enable {
systemd.services.flood = {
description = "Flood torrent UI";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = lib.concatStringsSep " " [
"${pkgs.flood}/bin/flood"
"--port ${builtins.toString cfg.port}"
"--rundir /var/lib/${cfg.stateDir}"
];
DynamicUser = true;
StateDirectory = cfg.stateDir;
ReadWritePaths = "";
};
};
services.nginx.virtualHosts."${webuiDomain}" = {
forceSSL = true;
useACMEHost = domain;
locations."/".proxyPass = "http://127.0.0.1:${toString cfg.port}";
};
};
}

View file

@ -0,0 +1,77 @@
# A low-ressource, full-featured git forge.
{ config, lib, ... }:
let
cfg = config.my.services.gitea;
domain = config.networking.domain;
giteaDomain = "gitea.${config.networking.domain}";
in
{
options.my.services.gitea = with lib; {
enable = mkEnableOption "Gitea";
port = mkOption {
type = types.port;
default = 3042;
example = 8080;
description = "Internal port";
};
};
config = lib.mkIf cfg.enable {
services.gitea = {
enable = true;
appName = "Ambroisie's forge";
httpPort = cfg.port;
domain = giteaDomain;
rootUrl = "https://${giteaDomain}";
user = "git";
lfs.enable = true;
useWizard = false;
disableRegistration = true;
# only send cookies via HTTPS
cookieSecure = true;
database = {
type = "postgres"; # Automatic setup
user = "git"; # User needs to be the same as gitea user
};
# NixOS module uses `gitea dump` to backup repositories and the database,
# but it produces a single .zip file that's not very backup friendly.
# I configure my backup system manually below.
dump.enable = false;
};
users.users.git = {
description = "Gitea Service";
home = config.services.gitea.stateDir;
useDefaultShell = true;
group = "git";
# The service for gitea seems to hardcode the group as
# gitea, so, uh, just in case?
extraGroups = [ "gitea" ];
isSystemUser = true;
};
users.groups.git = { };
# Proxy to Gitea
services.nginx.virtualHosts."${giteaDomain}" = {
forceSSL = true;
useACMEHost = domain;
locations."/".proxyPass = "http://127.0.0.1:${toString cfg.port}/";
};
my.services.backup = {
paths = [
config.services.gitea.lfs.contentDir
config.services.gitea.repositoryRoot
];
};
};
}

View file

@ -0,0 +1,44 @@
# Torrent and usenet meta-indexers
{ config, lib, ... }:
let
cfg = config.my.services.indexers;
domain = config.networking.domain;
jackettDomain = "jackett.${config.networking.domain}";
nzbhydraDomain = "nzbhydra.${config.networking.domain}";
jackettPort = 9117;
nzbhydraPort = 5076;
in
{
options.my.services.indexers = with lib; {
jackett.enable = mkEnableOption "Jackett torrent meta-indexer";
nzbhydra.enable = mkEnableOption "NZBHydra2 torrent meta-indexer";
};
config = {
services.jackett = lib.mkIf cfg.jackett.enable {
enable = true;
};
services.nginx.virtualHosts."${jackettDomain}" =
lib.mkIf cfg.jackett.enable {
forceSSL = true;
useACMEHost = domain;
locations."/".proxyPass = "http://127.0.0.1:${toString jackettPort}/";
};
services.nzbhydra2 = lib.mkIf cfg.nzbhydra.enable {
enable = true;
};
services.nginx.virtualHosts."${nzbhydraDomain}" =
lib.mkIf cfg.nzbhydra.enable {
forceSSL = true;
useACMEHost = domain;
locations."/".proxyPass = "http://127.0.0.1:${toString nzbhydraPort}/";
};
};
}

View file

@ -0,0 +1,37 @@
# A FLOSS media server
{ config, lib, ... }:
let
cfg = config.my.services.jellyfin;
domain = config.networking.domain;
jellyfinDomain = "jellyfin.${config.networking.domain}";
in
{
options.my.services.jellyfin = {
enable = lib.mkEnableOption "Jellyfin Media Server";
};
config = lib.mkIf cfg.enable {
services.jellyfin = {
enable = true;
group = "media";
};
# Proxy to Jellyfin
services.nginx.virtualHosts."${jellyfinDomain}" = {
forceSSL = true;
useACMEHost = domain;
locations."/" = {
proxyPass = "http://127.0.0.1:8096/";
extraConfig = ''
proxy_buffering off;
'';
};
locations."/socket" = {
proxyPass = "http://127.0.0.1:8096/";
proxyWebsockets = true;
};
};
};
}

87
modules/services/lohr.nix Normal file
View file

@ -0,0 +1,87 @@
# A simple Gitea webhook to mirror all my repositories
{ config, lib, pkgs, ... }:
let
cfg = config.my.services.lohr;
settingsFormat = pkgs.formats.yaml { };
domain = config.networking.domain;
lohrDomain = "lohr.${config.networking.domain}";
lohrPkg = pkgs.ambroisie.lohr;
in
{
options.my.services.lohr = with lib; {
enable = mkEnableOption "Automatic gitea repositories mirroring";
port = mkOption {
type = types.port;
default = 9192;
example = 8080;
description = "Internal port of the Lohr service";
};
setting = mkOption rec {
type = settingsFormat.type;
apply = recursiveUpdate default;
default = {
default_remotes = [
"git@github.com:ambroisie"
"git@git.sr.ht:~ambroisie"
];
};
description = "Global settings configuration file";
};
sharedSecretFile = mkOption {
type = types.str;
example = "/run/secrets/lohr.env";
description = "Shared secret between lohr and Gitea hook";
};
};
config = lib.mkIf cfg.enable {
systemd.services.lohr = {
wantedBy = [ "multi-user.target" ];
serviceConfig = {
EnvironmentFile = [
cfg.sharedSecretFile
];
Environment = [
"ROCKET_PORT=${toString cfg.port}"
"ROCKET_LOG_LEVEL=normal"
"LOHR_HOME=/var/lib/lohr/"
"LOHR_CONFIG="
];
ExecStart =
let
configFile = settingsFormat.generate "lohr-config.yaml" cfg.setting;
in
"${lohrPkg}/bin/lohr --config ${configFile}";
StateDirectory = "lohr";
WorkingDirectory = "/var/lib/lohr";
User = "lohr";
Group = "lohr";
};
path = with pkgs; [
git
];
};
users.users.lohr = {
isSystemUser = true;
home = "/var/lib/lohr";
createHome = true;
group = "lohr";
};
users.groups.lohr = { };
services.nginx.virtualHosts."${lohrDomain}" = {
forceSSL = true;
useACMEHost = domain;
locations."/" = {
proxyPass = "http://127.0.0.1:${toString cfg.port}/";
};
};
};
}

189
modules/services/matrix.nix Normal file
View file

@ -0,0 +1,189 @@
# Matrix homeserver setup, using different endpoints for federation and client
# traffic. The main trick for this is defining two nginx servers endpoints for
# matrix.domain.com, each listening on different ports.
#
# Configuration shamelessly stolen from [1]
#
# [1]: https://github.com/alarsyo/nixos-config/blob/main/services/matrix.nix
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.my.services.matrix;
federationPort = { public = 8448; private = 11338; };
clientPort = { public = 443; private = 11339; };
domain = config.networking.domain;
in
{
options.my.services.matrix = with lib; {
enable = mkEnableOption "Matrix Synapse";
secret = mkOption {
type = types.str;
example = "deadbeef";
description = "Shared secret to register users";
};
};
config = lib.mkIf cfg.enable {
services.postgresql = {
enable = true;
package = pkgs.postgresql_12;
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.matrix-synapse = {
enable = true;
dataDir = "/var/lib/matrix-synapse";
server_name = domain;
public_baseurl = "https://matrix.${domain}";
enable_registration = false;
registration_shared_secret = cfg.secret;
listeners = [
# Federation
{
bind_address = "::1";
port = federationPort.private;
tls = false; # Terminated by nginx.
x_forwarded = true;
resources = [{ names = [ "federation" ]; compress = false; }];
}
# Client
{
bind_address = "::1";
port = clientPort.private;
tls = false; # Terminated by nginx.
x_forwarded = true;
resources = [{ names = [ "client" ]; compress = false; }];
}
];
};
services.nginx.virtualHosts = {
"matrix.${domain}" = {
forceSSL = true;
useACMEHost = domain;
locations =
let
proxyToClientPort = {
proxyPass = "http://[::1]:${toString clientPort.private}";
};
in
{
# 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.
"/".return = "404";
"/_matrix" = proxyToClientPort;
"/_synapse/client" = proxyToClientPort;
};
listen = [
{ addr = "0.0.0.0"; port = clientPort.public; ssl = true; }
{ addr = "[::]"; port = clientPort.public; ssl = true; }
];
};
# same as above, but listening on the federation port
"matrix.${domain}_federation" = rec {
forceSSL = true;
serverName = "matrix.${domain}";
useACMEHost = domain;
locations."/".return = "404";
locations."/_matrix" = {
proxyPass = "http://[::1]:${toString federationPort.private}";
};
listen = [
{ addr = "0.0.0.0"; port = federationPort.public; ssl = true; }
{ addr = "[::]"; port = federationPort.public; ssl = true; }
];
};
"${domain}" = {
forceSSL = true;
useACMEHost = domain;
locations."= /.well-known/matrix/server".extraConfig =
let
server = { "m.server" = "matrix.${domain}:${toString federationPort.public}"; };
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.${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}';
'';
};
# Element Web app deployment
"chat.${domain}" = {
useACMEHost = domain;
forceSSL = true;
root = pkgs.element-web.override {
conf = {
default_server_config = {
"m.homeserver" = {
"base_url" = "https://matrix.${domain}";
"server_name" = domain;
};
"m.identity_server" = {
"base_url" = "https://vector.im";
};
};
showLabsSettings = true;
defaultCountryCode = "FR"; # cocorico
roomDirectory = {
"servers" = [
"matrix.org"
"mozilla.org"
];
};
};
};
};
};
# For administration tools.
environment.systemPackages = [ pkgs.matrix-synapse ];
networking.firewall.allowedTCPPorts = [
clientPort.public
federationPort.public
];
my.services.backup = {
paths = [
config.services.matrix-synapse.dataDir
];
};
};
}

View file

@ -0,0 +1,67 @@
# A minimalist, opinionated feed reader
{ config, lib, ... }:
let
cfg = config.my.services.miniflux;
domain = config.networking.domain;
minifluxDomain = "reader.${config.networking.domain}";
in
{
options.my.services.miniflux = with lib; {
enable = mkEnableOption "Miniflux feed reader";
username = mkOption {
type = types.str;
default = "Ambroisie";
example = "username";
description = "Name of the admin user";
};
password = mkOption {
type = types.str;
example = "password";
description = "Password of the admin user";
};
privatePort = mkOption {
type = types.port;
default = 9876;
example = 8080;
description = "Internal port for webui";
};
};
config = lib.mkIf cfg.enable {
# The service automatically sets up the DB
services.miniflux = {
enable = true;
adminCredentialsFile =
# Insecure, I don't care.
builtins.toFile "credentials.env" ''
ADMIN_USERNAME=${cfg.username}
ADMIN_PASSWORD=${cfg.password}
'';
config = {
# Virtual hosts settings
BASE_URL = "https://${minifluxDomain}";
LISTEN_ADDR = "localhost:${toString cfg.privatePort}";
# I want fast updates
POLLING_FREQUENCY = "30";
BATCH_SIZE = "50";
# I am a hoarder
CLEANUP_ARCHIVE_UNREAD_DAYS = "-1";
CLEANUP_ARCHIVE_READ_DAYS = "-1";
};
};
# Proxy to Jellyfin
services.nginx.virtualHosts."${minifluxDomain}" = {
forceSSL = true;
useACMEHost = domain;
locations."/".proxyPass = "http://127.0.0.1:${toString cfg.privatePort}/";
};
};
}

View file

@ -0,0 +1,75 @@
# A self-hosted cloud.
{ config, lib, pkgs, ... }:
let
cfg = config.my.services.nextcloud;
domain = config.networking.domain;
nextcloudDomain = "nextcloud.${config.networking.domain}";
in
{
options.my.services.nextcloud = with lib; {
enable = mkEnableOption "Nextcloud";
maxSize = mkOption {
type = types.str;
default = "512M";
example = "1G";
description = "Maximum file upload size";
};
admin = mkOption {
type = types.str;
default = "Ambroisie";
example = "admin";
description = "Name of the admin user";
};
password = mkOption {
type = types.str;
example = "password";
description = "The admin user's password";
};
};
config = lib.mkIf cfg.enable {
services.nextcloud = {
enable = true;
package = pkgs.nextcloud21;
hostName = nextcloudDomain;
home = "/var/lib/nextcloud";
maxUploadSize = cfg.maxSize;
config = {
adminuser = cfg.admin;
adminpass = cfg.password; # Insecure, but I don't care
dbtype = "pgsql";
dbhost = "/run/postgresql";
overwriteProtocol = "https"; # Nginx only allows SSL
};
};
services.postgresql = {
enable = true;
ensureDatabases = [ "nextcloud" ];
ensureUsers = [
{
name = "nextcloud";
ensurePermissions."DATABASE nextcloud" = "ALL PRIVILEGES";
}
];
};
systemd.services."nextcloud-setup" = {
requires = [ "postgresql.service" ];
after = [ "postgresql.service" ];
};
services.nginx.virtualHosts."${nextcloudDomain}" = {
forceSSL = true;
useACMEHost = domain;
locations."/".proxyPass = "http://127.0.0.1:3000/";
};
my.services.backup = {
paths = [
config.services.nextcloud.home
];
};
};
}

View file

@ -0,0 +1,44 @@
# Configuration shamelessly stolen from [1]
#
# [1]: https://github.com/delroth/infra.delroth.net/blob/master/common/nginx.nix
{ config, pkgs, lib, ... }:
{
# Whenever something defines an nginx vhost, ensure that nginx defaults are
# properly set.
config = lib.mkIf ((builtins.attrNames config.services.nginx.virtualHosts) != [ ]) {
services.nginx = {
enable = true;
statusPage = true; # For monitoring scraping.
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedTlsSettings = true;
recommendedProxySettings = true;
};
networking.firewall.allowedTCPPorts = [ 80 443 ];
# Nginx needs to be able to read the certificates
users.users.nginx.extraGroups = [ "acme" ];
# Use DNS wildcard certificate
security.acme = {
email = "bruno.acme@belanyi.fr";
acceptTerms = true;
certs =
let
domain = config.networking.domain;
key = config.my.secrets.acme.key;
in
with pkgs;
{
"${domain}" = {
extraDomainNames = [ "*.${domain}" ];
dnsProvider = "gandiv5";
credentialsFile = writeText "key.env" key; # Unsecure, I don't care.
};
};
};
};
}

View file

@ -0,0 +1,43 @@
# The total autonomous media delivery system.
# Relevant link [1].
#
# [1]: https://youtu.be/I26Ql-uX6AM
{ config, lib, ... }:
let
cfg = config.my.services.pirate;
domain = config.networking.domain;
ports = {
sonarr = 8989;
radarr = 7878;
bazarr = 6767;
lidarr = 8686;
};
managers = with lib.attrsets;
(mapAttrs
(_: _: {
enable = true;
group = "media";
})
ports);
redirections = with lib.attrsets;
(mapAttrs'
(service: port: nameValuePair "${service}.${domain}" {
forceSSL = true;
useACMEHost = domain;
locations."/".proxyPass = "http://127.0.0.1:${builtins.toString port}/";
})
ports);
in
{
options.my.services.pirate = {
enable = lib.mkEnableOption "Media automation";
};
config = lib.mkIf cfg.enable {
services = managers // { nginx.virtualHosts = redirections; };
};
}

View file

@ -0,0 +1,44 @@
# A simple podcast fetcher
{ config, lib, pkgs, ... }:
let
cfg = config.my.services.podgrab;
domain = config.networking.domain;
podgrabDomain = "podgrab.${domain}";
in
{
options.my.services.podgrab = with lib; {
enable = mkEnableOption "Podgrab, a self-hosted podcast manager";
passwordFile = mkOption {
type = with types; nullOr str;
default = null;
example = "/run/secrets/password.env";
description = ''
The path to a file containing the PASSWORD environment variable
definition for Podgrab's authentification.
'';
};
port = mkOption {
type = types.port;
default = 8080;
example = 4242;
description = "The port on which Podgrab will listen for incoming HTTP traffic.";
};
};
config = lib.mkIf cfg.enable {
services.podgrab = {
enable = true;
inherit (cfg) passwordFile port;
};
services.nginx.virtualHosts."${podgrabDomain}" = {
forceSSL = true;
useACMEHost = domain;
locations."/".proxyPass = "http://127.0.0.1:${toString cfg.port}";
};
};
}

View file

@ -0,0 +1,28 @@
# Backup your data, kids!
{ config, lib, ... }:
let
cfg = config.my.services.postgresql-backup;
in
{
options.my.services.postgresql-backup = {
enable = lib.mkEnableOption "Backup SQL databases";
};
config = lib.mkIf cfg.enable {
services.postgresqlBackup = {
enable = true;
backupAll = true;
location = "/var/backup/postgresql";
};
my.services.backup = {
paths = [
config.services.postgresqlBackup.location
];
# No need to store previous backups thanks to `restic`
exclude = [
(config.services.postgresqlBackup.location + "/*.prev.sql.gz")
];
};
};
}

View file

@ -0,0 +1,50 @@
# An IRC client daemon
{ config, lib, ... }:
let
cfg = config.my.services.quassel;
domain = config.networking.domain;
in
{
options.my.services.quassel = with lib; {
enable = mkEnableOption "Quassel IRC client daemon";
port = mkOption {
type = types.port;
default = 4242;
example = 8080;
description = "The port number for Quassel";
};
};
config = lib.mkIf cfg.enable {
services.quassel = {
enable = true;
portNumber = cfg.port;
# Let's be secure
requireSSL = true;
certificateFile = config.security.acme.certs."${domain}".directory + "/full.pem";
# The whole point *is* to connect from other clients
interfaces = [ "0.0.0.0" ];
};
# Allow Quassel to read the certificates.
users.groups.acme.members = [ "quassel" ];
# Open port for Quassel
networking.firewall.allowedTCPPorts = [ cfg.port ];
# Create storage DB
services.postgresql = {
enable = true;
ensureDatabases = [ "quassel" ];
ensureUsers = [
{
name = "quassel";
ensurePermissions."DATABASE quassel" = "ALL PRIVILEGES";
}
];
# Insecure, I don't care.
# Because Quassel does not use the socket, I simply trust its connection
authentication = "host quassel quassel localhost trust";
};
};
}

View file

@ -0,0 +1,25 @@
# Get RSS feeds from websites that don't natively have one
{ config, lib, ... }:
let
cfg = config.my.services.rss-bridge;
domain = config.networking.domain;
rss-bridgeDomain = "rss-bridge.${config.networking.domain}";
in
{
options.my.services.rss-bridge = {
enable = lib.mkEnableOption "RSS-Bridge service";
};
config = lib.mkIf cfg.enable {
services.rss-bridge = {
enable = true;
whitelist = [ "*" ]; # Whitelist all
virtualHost = rss-bridgeDomain; # Setup virtual host
};
services.nginx.virtualHosts."${rss-bridgeDomain}" = {
forceSSL = true;
useACMEHost = domain;
};
};
}

View file

@ -0,0 +1,28 @@
# Usenet binary client.
{ config, lib, ... }:
let
cfg = config.my.services.sabnzbd;
domain = config.networking.domain;
sabnzbdDomain = "sabnzbd.${domain}";
port = 9090; # NOTE: not declaratively set...
in
{
options.my.services.sabnzbd = with lib; {
enable = mkEnableOption "SABnzbd binary news reader";
};
config = lib.mkIf cfg.enable {
services.sabnzbd = {
enable = true;
group = "media";
};
services.nginx.virtualHosts."${sabnzbdDomain}" = {
forceSSL = true;
useACMEHost = domain;
locations."/".proxyPass = "http://127.0.0.1:${toString port}";
};
};
}

View file

@ -0,0 +1,23 @@
# An SSH server, using 'mosh'
{ config, lib, ... }:
let
cfg = config.my.services.ssh-server;
in
{
options.my.services.ssh-server = {
enable = lib.mkEnableOption "SSH Server using 'mosh'";
};
config = lib.mkIf cfg.enable {
services.openssh = {
# Enable the OpenSSH daemon.
enable = true;
# Be more secure
permitRootLogin = "no";
passwordAuthentication = false;
};
# Opens the relevant UDP ports.
programs.mosh.enable = true;
};
}

22
modules/services/tlp.nix Normal file
View file

@ -0,0 +1,22 @@
# TLP power management
{ config, lib, ... }:
let
cfg = config.my.services.tlp;
in
{
options.my.services.tlp = {
enable = lib.mkEnableOption "TLP power management configuration";
};
config = lib.mkIf cfg.enable {
services.tlp = {
enable = true;
settings = {
# Keep charge between 60% and 80% to preserve battery life
START_CHARGE_THRESH_BAT0 = 60;
STOP_CHARGE_THRESH_BAT0 = 80;
};
};
};
}

View file

@ -0,0 +1,92 @@
# Small seedbox setup.
#
# Inspired by [1]
#
# [1]: https://github.com/delroth/infra.delroth.net/blob/master/roles/seedbox.nix
{ config, lib, ... }:
let
cfg = config.my.services.transmission;
domain = config.networking.domain;
webuiDomain = "transmission.${domain}";
in
{
options.my.services.transmission = with lib; {
enable = mkEnableOption "Transmission torrent client";
username = mkOption {
type = types.str;
default = "Ambroisie";
example = "username";
description = "Name of the transmission RPC user";
};
password = mkOption {
type = types.str;
example = "password";
description = "Password of the transmission RPC user";
};
downloadBase = mkOption {
type = types.str;
default = "/data/downloads";
example = "/var/lib/transmission/download";
description = "Download base directory";
};
privatePort = mkOption {
type = types.port;
default = 9091;
example = 8080;
description = "Internal port for webui";
};
peerPort = mkOption {
type = types.port;
default = 30251;
example = 32323;
description = "Peering port";
};
};
config = lib.mkIf cfg.enable {
services.transmission = {
enable = true;
group = "media";
downloadDirPermissions = "775";
settings = {
download-dir = "${cfg.downloadBase}/complete";
incomplete-dir = "${cfg.downloadBase}/incomplete";
peer-port = cfg.peerPort;
rpc-enabled = true;
rpc-port = cfg.privatePort;
rpc-authentication-required = true;
rpc-username = cfg.username;
rpc-password = cfg.password; # Insecure, but I don't care.
# Proxied behind Nginx.
rpc-whitelist-enabled = true;
rpc-whitelist = "127.0.0.1";
};
};
# Default transmission webui, I prefer combustion but its development
# seems to have stalled
services.nginx.virtualHosts."${webuiDomain}" = {
forceSSL = true;
useACMEHost = domain;
locations."/".proxyPass = "http://127.0.0.1:${toString cfg.privatePort}";
};
networking.firewall = {
allowedTCPPorts = [ cfg.peerPort ];
allowedUDPPorts = [ cfg.peerPort ];
};
};
}

View file

@ -0,0 +1,241 @@
# A simple, in-kernel VPN service
#
# Strongly inspired by [1].
# [1]: https://github.com/delroth/infra.delroth.net/blob/master/roles/wireguard-peer.nix
{ config, lib, pkgs, ... }:
let
cfg = config.my.services.wireguard;
hostName = config.networking.hostName;
peers = config.my.secrets.wireguard.peers;
thisPeer = peers."${hostName}";
thisPeerIsServer = thisPeer ? externalIp;
# Only connect to clients from server, and only connect to server from clients
otherPeers =
let
allOthers = lib.filterAttrs (name: _: name != hostName) peers;
shouldConnectToPeer = _: peer: thisPeerIsServer != (peer ? externalIp);
in
lib.filterAttrs shouldConnectToPeer allOthers;
extIface = config.my.hardware.networking.externalInterface;
mkInterface = clientAllowedIPs: {
listenPort = cfg.port;
address = with cfg.net; with lib; [
"${v4.subnet}.${toString thisPeer.clientNum}/${toString v4.mask}"
"${v6.subnet}::${toString thisPeer.clientNum}/${toHexString v6.mask}"
];
# Insecure, I don't care
privateKey = thisPeer.privateKey;
peers =
let
mkPeer = _: peer: lib.mkMerge [
{
inherit (peer) publicKey;
}
(lib.optionalAttrs thisPeerIsServer {
# Only forward from server to clients
allowedIPs = with cfg.net; [
"${v4.subnet}.${toString peer.clientNum}/32"
"${v6.subnet}::${toString peer.clientNum}/128"
];
})
(lib.optionalAttrs (!thisPeerIsServer) {
# Forward all traffic through wireguard to server
allowedIPs = clientAllowedIPs;
# Roaming clients need to keep NAT-ing active
persistentKeepalive = 10;
# We know that `peer` is a server, set up the endpoint
endpoint = "${peer.externalIp}:${toString cfg.port}";
})
];
in
lib.mapAttrsToList mkPeer otherPeers;
# Set up clients to use configured DNS servers
dns =
let
toInternalIps = peer: [
"${cfg.net.v4.subnet}.${toString peer.clientNum}"
"${cfg.net.v6.subnet}::${toString peer.clientNum}"
];
# We know that `otherPeers` is an attribute set of servers
internalIps = lib.flatten
(lib.mapAttrsToList (_: peer: toInternalIps peer) otherPeers);
internalServers = lib.optionals cfg.dns.useInternal internalIps;
in
lib.mkIf (!thisPeerIsServer)
(internalServers ++ cfg.dns.additionalServers);
};
in
{
options.my.services.wireguard = with lib; {
enable = mkEnableOption "Wireguard VPN service";
startAtBoot = mkEnableOption ''
Should the VPN service be started at boot. Must be true for the server to
work reliably.
'';
iface = mkOption {
type = types.str;
default = "wg";
example = "wg0";
description = "Name of the interface to configure";
};
port = mkOption {
type = types.port;
default = 51820;
example = 55555;
description = "Port to configure for Wireguard";
};
dns = {
useInternal = my.mkDisableOption ''
Use internal DNS servers from wireguard 'server'
'';
additionalServers = mkOption {
type = with types; listOf str;
default = [
"1.0.0.1"
"1.1.1.1"
];
example = [
"8.8.4.4"
"8.8.8.8"
];
description = "Which DNS servers to use in addition to adblock ones";
};
};
net = {
# FIXME: use new ip library to handle this more cleanly
v4 = {
subnet = mkOption {
type = types.str;
default = "10.0.0";
example = "10.100.0";
description = "Which prefix to use for internal IPs";
};
mask = mkOption {
type = types.int;
default = 24;
example = 28;
description = "The CIDR mask to use on internal IPs";
};
};
# FIXME: extend library for IPv6
v6 = {
subnet = mkOption {
type = types.str;
default = "fd42:42:42";
example = "fdc9:281f:04d7:9ee9";
description = "Which prefix to use for internal IPs";
};
mask = mkOption {
type = types.int;
default = 64;
example = 68;
description = "The CIDR mask to use on internal IPs";
};
};
};
internal = {
enable = mkEnableOption ''
Additional interface which does not route WAN traffic, but gives access
to wireguard peers.
Is useful for accessing DNS and other internal services, without having
to route all traffic through wireguard.
Is automatically disabled on server, and enabled otherwise.
'' // {
default = !thisPeerIsServer;
};
name = mkOption {
type = types.str;
default = "lan";
example = "internal";
description = "Which name to use for this interface";
};
startAtBoot = my.mkDisableOption ''
Should the internal VPN service be started at boot.
'';
};
};
config = lib.mkIf cfg.enable (lib.mkMerge [
# Normal interface should route all traffic from client through server
{
networking.wg-quick.interfaces."${cfg.iface}" = mkInterface [
"0.0.0.0/0"
"::/0"
];
}
# Additional inteface is only used to get access to "LAN" from wireguard
(lib.mkIf cfg.internal.enable {
networking.wg-quick.interfaces."${cfg.internal.name}" = mkInterface [
"${cfg.net.v4.subnet}.0/${toString cfg.net.v4.mask}"
"${cfg.net.v6.subnet}::/${toString cfg.net.v6.mask}"
];
})
# Expose port
{
networking.firewall.allowedUDPPorts = [ cfg.port ];
}
# Allow NATing wireguard traffic on server
(lib.mkIf thisPeerIsServer {
networking.nat = {
enable = true;
externalInterface = extIface;
internalInterfaces = [ cfg.iface ];
};
})
# Set up forwarding to WAN
(lib.mkIf thisPeerIsServer {
networking.wg-quick.interfaces."${cfg.iface}" = {
postUp = with cfg.net; ''
${pkgs.iptables}/bin/iptables -A FORWARD -i ${cfg.iface} -j ACCEPT
${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING \
-s ${v4.subnet}.${toString thisPeer.clientNum}/${toString v4.mask} \
-o ${extIface} -j MASQUERADE
${pkgs.iptables}/bin/ip6tables -A FORWARD -i ${cfg.iface} -j ACCEPT
${pkgs.iptables}/bin/ip6tables -t nat -A POSTROUTING \
-s ${v6.subnet}::${toString thisPeer.clientNum}/${toString v6.mask} \
-o ${extIface} -j MASQUERADE
'';
preDown = with cfg.net; ''
${pkgs.iptables}/bin/iptables -D FORWARD -i ${cfg.iface} -j ACCEPT
${pkgs.iptables}/bin/iptables -t nat -D POSTROUTING \
-s ${v4.subnet}.${toString thisPeer.clientNum}/${toString v4.mask} \
-o ${extIface} -j MASQUERADE
${pkgs.iptables}/bin/ip6tables -D FORWARD -i ${cfg.iface} -j ACCEPT
${pkgs.iptables}/bin/ip6tables -t nat -D POSTROUTING \
-s ${v6.subnet}::${toString thisPeer.clientNum}/${toString v6.mask} \
-o ${extIface} -j MASQUERADE
'';
};
})
# When not needed at boot, ensure that there are no reverse dependencies
(lib.mkIf (!cfg.startAtBoot) {
systemd.services."wg-quick-${cfg.iface}".wantedBy = lib.mkForce [ ];
})
# Same idea, for internal-only interface
(lib.mkIf (cfg.internal.enable && !cfg.internal.startAtBoot) {
systemd.services."wg-quick-${cfg.internal.name}".wantedBy = lib.mkForce [ ];
})
]);
}