Commit files for public release
All checks were successful
CI / build (push) Successful in 13m53s

This commit is contained in:
Fabian Hauser 2024-10-02 16:52:04 +03:00
commit fef2377502
174 changed files with 7423 additions and 0 deletions

View file

@ -0,0 +1,6 @@
# Backup Module
This module creates a host-based backup job `system-${target-hostname}` (currently with borg).
The module has sensible defaults for a whole system, note however that individual services/paths must be included or excluded added manually.
Target hosts should use the [Backup Server Module](../backup-server).

View file

@ -0,0 +1,103 @@
{
config,
lib,
options,
pkgs,
self,
...
}:
let
cfg = config.qois.backup-client;
defaultIncludePaths = [
"/etc"
"/home"
"/root"
];
defaultExcludePaths = [
"/root/.cache"
"/root/.config/borg"
];
defaultSopsPasswordFile = "system/backup/password";
in
with lib;
{
options.qois.backup-client =
let
pathsType = with types; listOf str;
in
{
enable = mkEnableOption "Enable this host to execute backups.";
targets = mkOption {
type = with types; listOf (enum (attrNames config.qois.meta.hosts));
default = [
"cyprianspitz"
];
description = "Target hosts to make backups to. Must be configured to receive backups in the backplane network.";
};
includePaths = mkOption {
type = pathsType;
default = [ ];
description = "Paths that are included in backup. The backup module always includes: ${concatStringsSep ", " defaultIncludePaths}";
};
excludePaths = mkOption {
type = pathsType;
default = [ ];
description = "Paths that are excluded in backup. The backup module always excludes: ${concatStringsSep ", " defaultExcludePaths}";
};
passwordFile = mkOption {
type = with types; nullOr str;
default = null;
example = "config.sops.secrets.${defaultSopsPasswordFile}.path";
description = "Path to password file. Taken from sops host secret ${defaultSopsPasswordFile} by default, must be randomly generated per host.";
};
networkName = mkOption {
type = types.enum (attrNames config.qois.meta.network.virtual);
default = "backplane";
description = "Name of virtual network through which the backups should be done";
};
};
config.services.borgbackup.jobs = mkIf cfg.enable (
builtins.listToAttrs (
map (backupHost: {
name = "system-${backupHost}";
value = {
repo = "borg@${config.qois.meta.network.virtual.${cfg.networkName}.hosts.${backupHost}.v4.ip}:.";
environment.BORG_RSH = "ssh -i /etc/ssh/ssh_host_ed25519_key";
paths = defaultIncludePaths ++ cfg.includePaths;
exclude = defaultExcludePaths ++ cfg.excludePaths;
doInit = true;
encryption = {
mode = "repokey";
passCommand =
let
passFile =
if cfg.passwordFile != null then
cfg.passwordFile
else
config.sops.secrets.${defaultSopsPasswordFile}.path;
in
"cat ${passFile}";
};
startAt = "07:06";
persistentTimer = true;
};
}) cfg.targets
)
);
config.sops.secrets = mkIf (cfg.enable && cfg.passwordFile == null) {
${defaultSopsPasswordFile} = {
restartUnits = map (target: "borgbackup-job-system-${target}.service") cfg.targets;
};
};
}

View file

@ -0,0 +1,3 @@
# Backup Server Module
This backup module creates borg repositories for all the hosts configured with hosts.

View file

@ -0,0 +1,53 @@
{
config,
lib,
options,
pkgs,
self,
...
}:
let
cfg = config.qois.backup-server or { };
in
with lib;
{
options.qois.backup-server = {
enable = mkEnableOption "Enable backup hosting";
backupStorageRoot = mkOption {
type = with types; nullOr str;
default = "/mnt/backup";
example = "/mnt/nas/backup";
description = "Path where backups are stored if this host is used as a backup target.";
};
hosts = options.qois.meta.hosts // {
default = config.qois.meta.hosts;
};
};
config = lib.mkIf cfg.enable {
services.borgbackup.repos =
let
hasSshKey = hostName: cfg.hosts.${hostName}.sshKey != null;
mkRepo =
hostName:
(
let
name = "system-${hostName}";
in
{
inherit name;
value = {
path = "${cfg.backupStorageRoot}/${name}";
authorizedKeys = [ cfg.hosts.${hostName}.sshKey ];
};
}
);
hostsWithSshKeys = lib.filter hasSshKey (lib.attrNames cfg.hosts);
in
lib.listToAttrs (map mkRepo hostsWithSshKeys);
};
}

View file

@ -0,0 +1,10 @@
{
config,
pkgs,
inputs,
...
}:
{
imports = inputs.self.lib.loadSubmodulesFrom ./.;
}

View file

@ -0,0 +1,8 @@
# Git CI Runner
Runner for the [Forgejo git instance](../git/README.md).
Currently registers a default runner with ubuntu OS.
## Create Secret Token
To create a new token for registration, follow the steps outlined in the [Forgejo documentation](https://forgejo.org/docs/latest/user/actions/#forgejo-runner).

View file

@ -0,0 +1,52 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.qois.git-ci-runner;
defaultInstanceName = "default";
in
with lib;
{
options.qois.git-ci-runner = {
enable = mkEnableOption "Enable qois git ci-runner service";
domain = mkOption {
type = types.str;
default = "git.qo.is";
description = "Domain, under which the service is served.";
};
};
config = mkIf cfg.enable {
sops.secrets."forgejo/runner-token/${defaultInstanceName}".restartUnits = [
"gitea-runner-${defaultInstanceName}.service"
];
services.gitea-actions-runner = {
package = pkgs.forgejo-runner;
instances.${defaultInstanceName} = {
enable = true;
name = "${config.networking.hostName}-${defaultInstanceName}";
url = "https://${cfg.domain}";
tokenFile = config.sops.secrets."forgejo/runner-token/${defaultInstanceName}".path;
labels = [
"ubuntu-latest:docker://gitea/runner-images:ubuntu-latest"
"ubuntu-22.04:docker://ghcr.io/catthehacker/ubuntu:act-22.04"
"docker:docker://code.forgejo.org/oci/alpine:3.20"
];
settings = {
log.level = "warn";
runner = {
capacity = 30;
};
cache.enable = true; # TODO: This should probably be a central cache server?
};
};
};
};
}

View file

@ -0,0 +1,44 @@
# Git
## Configuration for Git Clients
### Authentication
To use oauth authentication, your git configuration should have something like:
```ini
[credential]
helper = "libsecret"
helper = "cache --timeout 21600"
helper = "/usr/bin/git-credential-oauth" # See https://github.com/hickford/git-credential-oauth
```
On NixOS with HomeManager, this can be achieved by following home-manager config:
```nix
programs.git.extraConfig.credential.helper = [ "libsecret" "cache --timeout 21600" ];
programs.git-credential-oauth.enable = true;
```
## Administration
### Create Accounts
Accounts can be created by an admin in the [administrator area](https://git.qo.is/admin).
- use their full `firstname.lastname@qo.is` email so users may be connected to a LDAP database in the future
- Username should be in form of "firstnamelastname" (Forgejo doesn't support usernames with dots)
To create a new admin user from the commandline, run:
```bash
sudo -u forgejo 'nix run nixpkgs#forgejo -- admin user create --config ~custom/conf/app.ini --admin --email "xy.z@qo.is" --username firstnamelastname --password Chur7000'
```
## Backup / Restore
1. `systemctl stop forgejo.service`
2. Import Postgresql Database Backup
3. Restore `/var/lib/forgejo`
4. `systemctl start forgejo.service`

View file

@ -0,0 +1,81 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.qois.git;
in
with lib;
{
options.qois.git = {
enable = mkEnableOption "Enable qois git service";
domain = mkOption {
type = types.str;
default = "git.qo.is";
description = "Domain, under which the service is served.";
};
};
config = mkIf cfg.enable {
qois.postgresql.enable = true;
services.forgejo = {
enable = true;
database.type = "postgres";
lfs.enable = true;
settings = {
DEFAULT.APP_NAME = cfg.domain;
server = {
DOMAIN = cfg.domain;
ROOT_URL = "https://${cfg.domain}";
PROTOCOL = "http+unix";
DISABLE_SSH = true;
};
"ssh.minimum_key_sizes".RSA = 2047;
session.COOKIE_SECURE = true;
service.DISABLE_REGISTRATION = true;
mailer = {
ENABLED = true;
PROTOCOL = "sendmail";
FROM = "git@qo.is";
SENDMAIL_PATH = "${pkgs.msmtp}/bin/sendmail";
# Note: The sendmail passwordeval has to use the coreutil cat (that is in the services path)
# instead of the busybox one due to filtered syscalls.
SENDMAIL_ARGS = "--passwordeval 'cat ${config.sops.secrets."msmtp/password".path}'";
};
log.LEVEL = "Warn";
};
};
qois.backup-client.includePaths = [ config.services.forgejo.stateDir ];
users.users.forgejo.extraGroups = [ "postdrop" ];
systemd.services.forgejo.serviceConfig.ReadOnlyPaths = [
config.sops.secrets."msmtp/password".path
];
networking.hosts."127.0.0.1" = [ cfg.domain ];
services.nginx = {
enable = true;
virtualHosts.${cfg.domain} = {
kTLS = true;
forceSSL = true;
enableACME = true;
extraConfig = ''
client_max_body_size 512M;
'';
locations."/" = {
proxyPass = "http://unix:${config.services.forgejo.settings.server.HTTP_ADDR}";
proxyWebsockets = true;
};
};
};
};
}

View file

@ -0,0 +1,169 @@
{
config,
pkgs,
lib,
...
}:
with lib;
let
# We assume that all static pages are hosted on lindberg-webapps
staticPages = pipe config.qois.static-page.pages [
(mapAttrsToList (name: { domain, domainAliases, ... }: [ domain ] ++ domainAliases))
flatten
(map (name: {
inherit name;
value = "lindberg-webapps";
}))
listToAttrs
];
defaultDomains = staticPages // {
"cloud.qo.is" = "lindberg-nextcloud";
"build.qo.is" = "lindberg-build";
"gitlab-runner.qo.is" = "lindberg-build";
"nixpkgs-cache.qo.is" = "lindberg-build";
"attic.qo.is" = "lindberg-build";
"vault.qo.is" = "lindberg-webapps";
"git.qo.is" = "lindberg-webapps";
"kokus.raphael.li" = "lindberg-rzimmermann";
"auth.raphael.li" = "lindberg-rzimmermann";
"toolia.raphael.li" = "lindberg-rzimmermann";
"ha.raphael.li" = "lindberg-rzimmermann";
"www.raphael.li" = "lindberg-rzimmermann";
"vpn.qo.is" = "cyprianspitz-headscale";
};
getBackplaneIp = hostname: config.qois.meta.network.virtual.backplane.hosts.${hostname}.v4.ip;
defaultHostmap =
lib.pipe
[
"lindberg-nextcloud"
"lindberg-build"
"lindberg-webapps"
]
[
(map (name: {
inherit name;
value = getBackplaneIp name;
}))
lib.listToAttrs
];
defaultExtraConfig =
let
headscalePort = toString 46084;
rzimmermannIp = "10.247.0.113";
in
''
# lindberg-rzimmermann (uses send-proxy-v2)
backend lindberg-rzimmermann-https
mode tcp
server s1 ${rzimmermannIp}:443 send-proxy-v2
backend lindberg-rzimmermann-http
mode http
server s1 ${rzimmermannIp}:80
# cyprianspitz headscale
backend cyprianspitz-headscale-http
mode http
server s1 ${getBackplaneIp "cyprianspitz"}:${headscalePort}
backend cyprianspitz-headscale-https
mode tcp
server s1 ${getBackplaneIp "cyprianspitz"}:${headscalePort}
'';
cfg = config.qois.loadbalancer;
in
{
options.qois.loadbalancer = with lib; {
enable = mkEnableOption "Enable services http+s loadbalancing";
domains = mkOption {
description = "Domain to hostname mappings";
type = with lib.types; attrsOf str;
default = defaultDomains;
};
hostmap = mkOption {
description = "Hostname to IP mappings for TLS-TCP and http forwarding";
type = with lib.types; attrsOf str;
default = defaultHostmap;
};
extraConfig = mkOption {
description = "Additional haproxy mapping configs. Amended to services.haproxy.config. Make sure indentations are correct.";
type = types.nullOr types.lines;
default = defaultExtraConfig;
};
};
config =
with lib;
mkIf cfg.enable {
networking.firewall.allowedTCPPorts = [
80
443
];
services.haproxy =
let
domainMappingFile = pipe cfg.domains [
(mapAttrsToList (host: backend: "${host} ${backend}"))
concatLines
(pkgs.writeText "haproxy_backend_map")
];
genHttpBackend = hostName: ip: ''
# Mapping for ${hostName}
backend ${hostName}-https
mode tcp
server s1 ${ip}:443
backend ${hostName}-http
mode http
server s1 ${ip}:80
'';
httpBackends = pipe cfg.hostmap [
(mapAttrsToList genHttpBackend)
concatLines
];
in
{
enable = true;
config = ''
defaults
mode http
retries 3
maxconn 2000
timeout connect 5000
timeout client 50000
timeout server 50000
frontend http
mode http
bind *:80
use_backend %[req.hdr(host),lower,map_dom(${domainMappingFile})]-http
frontend https
bind *:443
mode tcp
tcp-request inspect-delay 5s
tcp-request content accept if { req_ssl_hello_type 1 }
use_backend %[req.ssl_sni,lower,map_dom(${domainMappingFile})]-https
## Generated Backends:
${httpBackends}
## extraConfig
${cfg.extraConfig}
'';
};
};
}

View file

@ -0,0 +1,6 @@
# Static Pages
This module enables static nginx sites, with data served from "/var/lib/nginx/$domain/root".
To deploy the site, a user `nginx-$domain` is added, of which a `root` profile in the home folder can be deployed, e.g. with deploy-rs.

View file

@ -0,0 +1,26 @@
{
config,
pkgs,
lib,
...
}:
{
qois.static-page.pages = {
"fabianhauser.ch" = {
domainAliases = [
"www.fabianhauser.ch"
"fabianhauser.nl"
"www.fabianhauser.nl"
"www.fh2.ch"
"fh2.ch"
];
authorizedKeys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFsSCoClNpgW7x6YngP/CEFbyR8GEJ3V8NdUFvZ/6lj6 ci@git.qo.is"
];
};
"docs-ops.qo.is".authorizedKeys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBS65v7n5ozOUjYGuO/dgLC9C5MUGL5kTnQnvWAYP5B3 ci@git.qo.is"
];
};
}

View file

@ -0,0 +1,145 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.qois.static-page;
in
with lib;
{
imports = [ ./default-pages.nix ];
options.qois.static-page =
let
pageType =
{ name, ... }:
{
options = {
domain = mkOption {
type = types.str;
default = name;
description = ''
Primary domain, under which the site is served.
Only ASCII Domains are supported at this time.
Note that changing this changes the root folder of the vhost in /var/lib/nginx-$domain/root and the ssh user to "nginx-$domain".
'';
};
domainAliases = mkOption {
type = types.listOf types.str;
default = [ ];
description = "Domain aliases which are forwarded to the primary domain";
};
authorizedKeys = mkOption {
type = types.listOf types.str;
default = [ ];
description = "SSH keys for deployment";
};
};
}
;
in
{
enable = mkEnableOption "Enable static-page hosting";
pages = mkOption {
type = types.attrsOf (types.submodule (pageType));
};
};
config = mkIf cfg.enable (
let
pageConfigs = concatMapAttrs (
name: page:
let
home = "/var/lib/nginx-${page.domain}";
in
{
"${page.domain}" = page // {
inherit home;
user = "${config.services.nginx.user}-${page.domain}";
root = "${home}/root";
};
}
) cfg.pages;
in
{
networking.hosts."127.0.0.1" = pipe pageConfigs [
attrValues
(map (page: [ page.domain ] ++ page.domainAliases))
flatten
];
users = {
groups = concatMapAttrs (
name:
{ user, ... }:
{
"${user}" = { };
}
) pageConfigs;
users =
{
${config.services.nginx.user}.extraGroups = mapAttrsToList (domain: getAttr "user") pageConfigs;
}
// (concatMapAttrs (
name:
{
user,
home,
authorizedKeys,
...
}:
{
${user} = {
inherit home;
isSystemUser = true;
useDefaultShell = true;
homeMode = "750";
createHome = true;
group = user;
openssh.authorizedKeys.keys = authorizedKeys;
};
}
) pageConfigs);
};
services.nginx = {
enable = true;
virtualHosts =
let
defaultVhostConfig = {
enableACME = true;
forceSSL = true;
kTLS = true;
};
mkVhost =
{ root, ... }:
defaultVhostConfig
// {
inherit root;
};
mkAliasVhost =
{ domainAliases, domain, ... }:
if (domainAliases == [ ]) then
{ }
else
({
"${head domainAliases}" = defaultVhostConfig // {
serverAliases = tail domainAliases;
globalRedirect = domain;
};
});
aliasVhosts = concatMapAttrs (name: mkAliasVhost) pageConfigs;
in
aliasVhosts // (mapAttrs (name: mkVhost) pageConfigs);
};
}
);
}

View file

@ -0,0 +1,136 @@
{
config,
pkgs,
lib,
...
}:
with lib;
let
cfg = config.qois.vpn-server;
cfgLoadbalancer = config.qois.loadbalancer;
defaultDnsRecords = mapAttrs (
name: value: mkIf (cfgLoadbalancer.hostmap ? ${value}) cfgLoadbalancer.hostmap.${value}
) cfgLoadbalancer.domains;
in
{
options.qois.vpn-server = {
enable = mkEnableOption "Enable vpn server services";
dnsRecords = mkOption {
description = "DNS records to add to Hosts";
type = with types; attrsOf str;
default = defaultDnsRecords;
};
wheelUsers = mkOption {
description = "Usernames that can change configurations";
type = with types; listOf str;
default = [ ];
};
};
config = mkIf cfg.enable ({
environment.systemPackages = [ pkgs.headscale ];
qois.backup-client.includePaths =
with config.services.headscale.settings;
(
[
db_path
private_key_path
noise.private_key_path
]
++ derp.paths
);
networking.firewall.checkReversePath = "loose";
networking.firewall.allowedUDPPorts = [
41641
];
services.headscale =
let
vnet = config.qois.meta.network.virtual;
vpnNet = vnet.vpn;
vpnNetPrefix = "${vpnNet.v4.id}/${builtins.toString vpnNet.v4.prefixLength}";
backplaneNetPrefix = "${vnet.backplane.v4.id}/${builtins.toString vnet.backplane.v4.prefixLength}";
in
{
enable = true;
address = vnet.backplane.hosts.cyprianspitz.v4.ip;
port = 46084;
settings = {
server_url = "https://${vpnNet.domain}:443";
tls_letsencrypt_challenge_type = "TLS-ALPN-01";
tls_letsencrypt_hostname = vpnNet.domain;
dns_config = {
nameservers = [ vnet.backplane.hosts.calanda.v4.ip ];
domains = [
vpnNet.domain
vnet.backplane.domain
];
magic_dns = true;
base_domain = vpnNet.domain;
extra_records = pipe cfg.dnsRecords [
attrsToList
(map (val: val // { type = "A"; }))
];
};
ip_prefixes = [ vpnNetPrefix ];
acl_policy_path = pkgs.writeTextFile {
name = "acls";
text = builtins.toJSON {
hosts = {
"clients" = vpnNetPrefix;
};
groups = {
"group:wheel" = cfg.wheelUsers;
};
tagOwners = {
"tag:srv" = [ "srv" ]; # srv tag ist owned by srv user
};
autoApprovers = {
exitNode = [
"tag:srv"
"group:wheel"
];
routes = {
${backplaneNetPrefix} = [ "tag:srv" ];
};
};
acls = [
# Allow all communication from and to srv tagged hosts
{
action = "accept";
src = [
"tag:srv"
"srv"
];
dst = [ "*:*" ];
}
{
action = "accept";
src = [ "*" ];
dst = [
"tag:srv:*"
"srv:*"
];
}
# Allow access to all connected hosts for wheels
{
action = "accept";
src = [ "group:wheel" ];
dst = [ "*:*" ];
}
];
};
};
};
};
});
}