modules: move 'services' into subfolder
This commit is contained in:
parent
274b909971
commit
9b568beb9a
27 changed files with 1 additions and 2 deletions
|
|
@ -10,6 +10,7 @@
|
|||
./media.nix
|
||||
./nix.nix
|
||||
./packages.nix
|
||||
./services
|
||||
./users.nix
|
||||
];
|
||||
}
|
||||
|
|
|
|||
72
modules/services/adblock.nix
Normal file
72
modules/services/adblock.nix
Normal 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
106
modules/services/backup.nix
Normal 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
39
modules/services/blog.nix
Normal 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;
|
||||
};
|
||||
}
|
||||
56
modules/services/calibre-web.nix
Normal file
56
modules/services/calibre-web.nix
Normal 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
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
30
modules/services/default.nix
Normal file
30
modules/services/default.nix
Normal 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
196
modules/services/drone.nix
Normal 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") { };
|
||||
};
|
||||
}
|
||||
53
modules/services/flood.nix
Normal file
53
modules/services/flood.nix
Normal 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}";
|
||||
};
|
||||
};
|
||||
}
|
||||
77
modules/services/gitea.nix
Normal file
77
modules/services/gitea.nix
Normal 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
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
44
modules/services/indexers.nix
Normal file
44
modules/services/indexers.nix
Normal 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}/";
|
||||
};
|
||||
};
|
||||
}
|
||||
37
modules/services/jellyfin.nix
Normal file
37
modules/services/jellyfin.nix
Normal 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
87
modules/services/lohr.nix
Normal 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
189
modules/services/matrix.nix
Normal 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
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
67
modules/services/miniflux.nix
Normal file
67
modules/services/miniflux.nix
Normal 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}/";
|
||||
};
|
||||
};
|
||||
}
|
||||
75
modules/services/nextcloud.nix
Normal file
75
modules/services/nextcloud.nix
Normal 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
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
44
modules/services/nginx.nix
Normal file
44
modules/services/nginx.nix
Normal 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.
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
43
modules/services/pirate.nix
Normal file
43
modules/services/pirate.nix
Normal 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; };
|
||||
};
|
||||
}
|
||||
44
modules/services/podgrab.nix
Normal file
44
modules/services/podgrab.nix
Normal 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}";
|
||||
};
|
||||
};
|
||||
}
|
||||
28
modules/services/postgresql-backup.nix
Normal file
28
modules/services/postgresql-backup.nix
Normal 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")
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
50
modules/services/quassel.nix
Normal file
50
modules/services/quassel.nix
Normal 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";
|
||||
};
|
||||
};
|
||||
}
|
||||
25
modules/services/rss-bridge.nix
Normal file
25
modules/services/rss-bridge.nix
Normal 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;
|
||||
};
|
||||
};
|
||||
}
|
||||
28
modules/services/sabnzbd.nix
Normal file
28
modules/services/sabnzbd.nix
Normal 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}";
|
||||
};
|
||||
};
|
||||
}
|
||||
23
modules/services/ssh-server.nix
Normal file
23
modules/services/ssh-server.nix
Normal 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
22
modules/services/tlp.nix
Normal 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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
92
modules/services/transmission.nix
Normal file
92
modules/services/transmission.nix
Normal 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 ];
|
||||
};
|
||||
};
|
||||
}
|
||||
241
modules/services/wireguard.nix
Normal file
241
modules/services/wireguard.nix
Normal 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 [ ];
|
||||
})
|
||||
]);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue