nixos-config/hosts/plover/default.nix

574 lines
17 KiB
Nix
Raw Normal View History

{ config, options, lib, pkgs, modulesPath, ... }:
2022-11-23 05:27:01 +00:00
let
2022-11-25 13:27:23 +00:00
inherit (builtins) toString;
2022-12-12 12:34:23 +00:00
domain = config.networking.domain;
2022-12-19 12:30:30 +00:00
subdomain = prefix: "${prefix}.${domain}";
passwordManagerDomain = subdomain "pass";
codeForgeDomain = subdomain "code";
authDomain = subdomain "auth";
2022-12-26 05:23:40 +00:00
ldapDomain = subdomain "ldap";
2022-12-16 14:24:59 +00:00
certs = config.security.acme.certs;
# This should be set from service module from nixpkgs.
vaultwardenUser = config.users.users.vaultwarden.name;
# However, this is set on our own.
vaultwardenDbName = "vaultwarden";
2022-12-16 14:25:50 +00:00
2022-12-17 14:23:36 +00:00
# This is also set on our own.
keycloakUser = config.services.keycloak.database.username;
keycloakDbName = if config.services.keycloak.database.createLocally then keycloakUser else config.services.keycloak.database.username;
2022-12-16 14:25:50 +00:00
# The head of the Borgbase hostname.
borgbase-remote = "cr6pf13r";
2022-11-23 05:27:01 +00:00
in
{
imports = [
2022-11-25 13:27:23 +00:00
./hardware-configuration.nix
2022-11-26 06:13:17 +00:00
# The users for this host.
(lib.getUser "nixos" "admin")
2022-11-26 06:13:17 +00:00
(lib.getUser "nixos" "plover")
# Hardened profile from nixpkgs.
"${modulesPath}/profiles/hardened.nix"
2022-11-23 05:27:01 +00:00
];
networking = {
domain = "foodogsquared.one";
firewall.allowedTCPPorts = [
22 # Secure Shells.
80 # HTTP servers.
433 # HTTPS servers.
2022-12-26 05:23:40 +00:00
389 # LDAP servers.
636 # LDAPS servers.
];
};
2022-11-23 05:27:01 +00:00
sops.secrets =
let
getKey = key: {
inherit key;
sopsFile = ./secrets/secrets.yaml;
};
2022-12-02 04:33:51 +00:00
getSecrets = secrets:
lib.mapAttrs'
(secret: config:
2022-11-23 05:27:01 +00:00
lib.nameValuePair
"plover/${secret}"
2022-12-02 04:33:51 +00:00
((getKey secret) // config))
secrets;
2022-12-26 09:45:54 +00:00
giteaUserGroup = config.users.users."${config.services.gitea.user}".group;
# It is hardcoded but as long as the module is stable that way.
vaultwardenUserGroup = config.users.groups.vaultwarden.name;
postgresUserGroup = config.users.groups.postgres.name;
2022-11-23 05:27:01 +00:00
in
2022-12-26 09:45:54 +00:00
getSecrets {
"ssh-key" = { };
"lego/env" = { };
"gitea/db/password".owner = giteaUserGroup;
"gitea/smtp/password".owner = giteaUserGroup;
"vaultwarden/env".owner = vaultwardenUserGroup;
"borg/patterns/keys" = { };
"borg/password" = { };
"keycloak/db/password".owner = postgresUserGroup;
};
2022-11-23 05:27:01 +00:00
# All of the keys required to deploy the secrets. Don't know how to make the
# GCP KMS key work though without manually going into the instance and
# configure it there.
2022-11-23 05:27:01 +00:00
sops.age.keyFile = "/var/lib/sops-nix/key.txt";
2022-12-02 04:33:51 +00:00
profiles.server = {
enable = true;
headless.enable = true;
hardened-config.enable = true;
cleanup.enable = true;
};
2022-12-03 00:09:26 +00:00
# DNS-related settings. This is nice for automating them putting DNS records
# and other types of stuff.
2022-12-26 05:23:40 +00:00
security.acme = {
defaults = {
dnsProvider = "porkbun";
credentialsFile = config.sops.secrets."plover/lego/env".path;
};
certs = {
"${ldapDomain}".group = config.services.openldap.group;
};
2022-12-03 00:09:26 +00:00
};
2022-12-02 04:33:51 +00:00
services.openssh.hostKeys = [{
path = config.sops.secrets."plover/ssh-key".path;
type = "ed25519";
}];
# The main server where it will tie all of the services in one neat little
# place.
2022-11-23 05:27:01 +00:00
services.nginx = {
enable = true;
enableReload = true;
package = pkgs.nginxMainline;
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
2022-11-26 06:13:17 +00:00
# Server blocks with no forcing of SSL are static sites so it is pretty
# much OK.
2022-11-23 05:27:01 +00:00
virtualHosts = {
# Vaultwarden instance.
"${passwordManagerDomain}" = {
forceSSL = true;
enableACME = true;
2022-12-11 10:11:07 +00:00
locations =
let
address = config.services.vaultwarden.config.ROCKET_ADDRESS;
port = config.services.vaultwarden.config.ROCKET_PORT;
websocketPort = config.services.vaultwarden.config.WEBSOCKET_PORT;
in
{
"/" = {
proxyPass = "http://${address}:${toString port}";
proxyWebsockets = true;
};
"/notifications/hub" = {
proxyPass = "http://${address}:${toString websocketPort}";
proxyWebsockets = true;
};
"/notifications/hub/negotiate" = {
proxyPass = "http://${address}:${toString port}";
proxyWebsockets = true;
};
2022-11-23 05:27:01 +00:00
};
};
2022-11-25 13:27:23 +00:00
# Gitea instance.
"${codeForgeDomain}" = {
2022-11-25 13:27:23 +00:00
forceSSL = true;
2022-11-23 05:27:01 +00:00
enableACME = true;
locations = let
keycloakPath = path: "http://localhost:${toString config.services.gitea.httpPort}${path}";
in {
"/realms".proxyPass = keycloakPath "/realms";
"/resources".proxyPass = keycloakPath "/resources";
"/robots.txt".proxyPass = keycloakPath "/robots.txt";
2022-11-23 05:27:01 +00:00
};
};
2022-12-12 12:32:21 +00:00
# Keycloak instance.
2022-12-19 12:30:30 +00:00
"${authDomain}" = {
2022-12-12 12:32:21 +00:00
forceSSL = true;
enableACME = true;
locations."/" = {
2022-12-19 12:31:01 +00:00
proxyPass = "http://localhost:${toString config.services.keycloak.settings.http-port}";
2022-12-12 12:32:21 +00:00
};
};
2022-11-23 05:27:01 +00:00
};
};
# Enable database services that is used in all of the services here so far.
services.postgresql = {
enable = true;
package = pkgs.postgresql_15;
enableTCPIP = true;
2022-12-04 00:55:55 +00:00
# Create per-user schema as documented from Usage Patterns. This is to make
# use of the secure schema usage pattern they encouraged to do.
#
# Now, you just have to keep in mind about applications making use of them.
# Most of them should have the setting to set the schema to be used. If
# not, then screw them (or just file an issue and politely ask for the
# feature).
2022-12-11 10:11:07 +00:00
initialScript =
let
# This will be run once anyways so it is acceptable to create users
# "forcibly".
2022-12-11 10:11:07 +00:00
perUserSchemas = lib.lists.map
(user: ''
CREATE USER ${user.name};
CREATE SCHEMA AUTHORIZATION ${user.name};
'')
2022-12-11 10:11:07 +00:00
config.services.postgresql.ensureUsers;
in pkgs.writeText "plover-initial-postgresql-script" ''
${lib.concatStringsSep "\n" perUserSchemas}
'';
2022-12-04 00:55:55 +00:00
settings = {
# Still doing the secure schema usage pattern.
search_path = "\"$user\"";
};
# There's no database and user checks for Vaultwarden service.
2022-12-17 14:23:36 +00:00
ensureDatabases = [ vaultwardenDbName keycloakDbName ];
ensureUsers = [
{
name = vaultwardenUser;
2022-12-02 04:33:51 +00:00
ensurePermissions = {
"DATABASE ${vaultwardenDbName}" = "ALL PRIVILEGES";
2022-12-04 00:55:55 +00:00
"SCHEMA ${vaultwardenDbName}" = "ALL PRIVILEGES";
2022-12-02 04:33:51 +00:00
};
}
{
name = config.services.gitea.user;
ensurePermissions = {
"SCHEMA ${config.services.gitea.user}" = "ALL PRIVILEGES";
};
}
2022-12-17 14:23:36 +00:00
{
name = keycloakUser;
ensurePermissions = {
"DATABASE ${keycloakDbName}" = "ALL PRIVILEGES";
"SCHEMA ${keycloakDbName}" = "ALL PRIVILEGES";
};
}
];
};
2022-12-12 12:32:21 +00:00
2022-12-26 05:23:40 +00:00
# How to overkill your multi-purpose single-user-oriented server that is
# typically accessed from the web with a single step.
services.openldap = let
openldapPackage = config.services.openldap.package;
in {
enable = true;
mutableConfig = true;
urlList = [ "ldap:///" "ldaps:///" "ldapi://" ];
settings = {
attrs = {
olcLogLevel = [ "stats" ];
olcTLSCACertificateFile = "${certs.${ldapDomain}.directory}/fullchain.pem";
olcTLSCertificateFile = "${certs.${ldapDomain}.directory}/chain.pem";
olcTLSCertificateKeyFile = "${certs.${ldapDomain}.directory}/key.pem";
};
children = {
"olcDatabase={-1}frontend".attrs = {
objectClass = "olcDatabaseConfig";
olcDatabase = "{-1}frontend";
olcAccess = [ "{0}to * by dn.exact=uidNumber=0+gidNumber=0,cn=peercred,cn=external,cn=auth manage stop by * none stop" ];
};
"olcDatabase={0}config".attrs = {
objectClass = "olcDatabaseConfig";
olcDatabase = "{0}config";
olcAccess = [ "{0}to * by * none break" ];
};
"olcDatabase={1}mdb".attrs = {
objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
olcDatabase = "{1}mdb";
olcDbDirectory = "/var/lib/openldap/ldap";
olcDbIndex = [
"objectClass eq"
"cn pres,eq"
"uid pres,eq"
"sn pres,eq,subany"
];
olcSuffix = "dc=foodogsquared,dc=one";
olcRootDN = "cn=Manager,dc=foodogsquared,dc=one";
olcAccess = [ "{0}to * by * read break" ];
};
"cn=schema".includes = [
"${openldapPackage}/etc/schema/core.ldif"
"${openldapPackage}/etc/schema/cosine.ldif"
"${openldapPackage}/etc/schema/inetorgperson.ldif"
];
};
};
};
2022-12-12 12:32:21 +00:00
# Hey, the hub for your application sign-in.
services.keycloak = {
enable = true;
# Pls change at first login.
initialAdminPassword = "wow what is this thing";
database = {
type = "postgresql";
createLocally = true;
passwordFile = config.sops.secrets."plover/keycloak/db/password".path;
};
settings = {
2022-12-19 12:31:01 +00:00
host = "127.0.0.1";
db-schema = keycloakDbName;
2022-12-19 12:31:01 +00:00
http-enabled = true;
http-port = 8759;
https-port = 8760;
2022-12-19 12:30:30 +00:00
hostname = authDomain;
2022-12-12 12:32:21 +00:00
hostname-strict-backchannel = true;
2022-12-19 12:31:01 +00:00
proxy = "passthrough";
2022-12-12 12:32:21 +00:00
};
2022-12-19 12:31:01 +00:00
sslCertificate = "${certs."${authDomain}".directory}/fullchain.pem";
sslCertificateKey = "${certs."${authDomain}".directory}/key.pem";
2022-12-12 12:32:21 +00:00
};
# Modifying it a little bit for per-user schema.
systemd.services.keycloak = {
path = [ config.services.postgresql.package ];
preStart = ''
psql -tAc "SELECT 1 FROM information_schema.schemata WHERE schema_name='${keycloakDbName}';" \
grep -q 1 || psql -tAc "CREATE SCHEMA IF NOT EXISTS keycloak;"
'';
};
# With a database comes a dumping.
services.postgresqlBackup = {
enable = true;
compression = "zstd";
compressionLevel = 11;
# Start at every 3 days starting from the first day of the month.
startAt = "*-*-1/3";
};
2022-11-23 05:27:01 +00:00
# My code forge.
services.gitea = {
enable = true;
appName = "foodogsquared's code forge";
database = {
type = "postgres";
2022-12-02 04:33:51 +00:00
passwordFile = config.sops.secrets."plover/gitea/db/password".path;
2022-11-23 05:27:01 +00:00
};
domain = codeForgeDomain;
rootUrl = "https://${codeForgeDomain}";
# Allow Gitea to take a dump.
dump = {
enable = true;
interval = "Sunday";
};
# There are a lot of services in port 3000 so we'll change it.
httpPort = 8432;
2022-11-23 05:27:01 +00:00
lfs.enable = true;
2022-12-02 04:33:51 +00:00
mailerPasswordFile = config.sops.secrets."plover/gitea/smtp/password".path;
2022-11-23 05:27:01 +00:00
settings = {
"repository.pull_request" = {
WORK_IN_PROGRESS_PREFIXES = "WIP:,[WIP],DRAFT,[DRAFT]";
ADD_CO_COMMITTERS_TRAILERS = true;
};
ui = {
DEFAULT_THEME = "auto";
2022-11-23 05:27:01 +00:00
EXPLORE_PAGING_SUM = 15;
GRAPH_MAX_COMMIT_NUM = 200;
};
"ui.meta" = {
AUTHOR = "foodogsquared's code forge";
2022-12-04 00:55:55 +00:00
DESCRIPTION = "foodogsquared's personal projects and some archived and mirrored codebases.";
2022-11-23 05:27:01 +00:00
KEYWORDS = "foodogsquared,gitea,self-hosted";
};
# It's a personal instance so nah...
service.DISABLE_REGISTRATION = true;
repository = {
ENABLE_PUSH_CREATE_USER = true;
DEFAULT_PRIVATE = "public";
DEFAULT_PRIVATE_PUSH_CREATE = true;
};
"markup.asciidoc" = {
ENABLED = true;
NEED_POSTPROCESS = true;
FILE_EXTENSIONS = ".adoc,.asciidoc";
2022-12-04 00:55:55 +00:00
RENDER_COMMAND = "${pkgs.asciidoctor}/bin/asciidoctor --out-file=- -";
2022-11-23 05:27:01 +00:00
IS_INPUT_FILE = false;
};
2022-12-02 04:33:51 +00:00
# Mailer service.
mailer = {
ENABLED = true;
PROTOCOL = "smtp+starttls";
2022-12-02 04:33:51 +00:00
SMTP_ADDRESS = "smtp.sendgrid.net";
SMTP_PORT = 587;
USER = "apikey";
FROM = "bot+gitea@foodogsquared.one";
2022-12-02 04:33:51 +00:00
SEND_AS_PLAIN_TEXT = true;
SENDMAIL_PATH = "${pkgs.system-sendmail}/bin/sendmail";
2022-12-02 04:33:51 +00:00
};
2022-11-23 05:27:01 +00:00
# Well, collaboration between forges is nice...
federation.ENABLED = true;
# Enable mirroring feature...
mirror.ENABLED = true;
2022-12-04 00:55:55 +00:00
# Session configuration.
session.COOKIE_SECURE = true;
# Some more database configuration.
database.SCHEMA = config.services.gitea.user;
# Run various periodic services.
"cron.update_mirrors".SCHEDULE = "@every 12h";
2022-11-23 05:27:01 +00:00
other = {
SHOW_FOOTER_VERSION = true;
ENABLE_SITEMAP = true;
ENABLE_FEED = true;
};
};
};
# Disk space is always assumed to be limited so we're really only limited with 2 dumps.
systemd.services.gitea-dump.serviceConfig = {
ExecStartPre = pkgs.writeShellScript "gitea-dump-limit" ''
find ${config.services.gitea.dump.backupDir} -mtime 14 -maxdepth 1 -type f -delete
'';
};
2022-11-23 05:27:01 +00:00
# An alternative implementation of Bitwarden written in Rust. The project
# being written in Rust is a insta-self-hosting material right there.
services.vaultwarden = {
enable = true;
dbBackend = "postgresql";
2022-12-02 04:33:51 +00:00
environmentFile = config.sops.secrets."plover/vaultwarden/env".path;
2022-11-23 05:27:01 +00:00
config = {
DOMAIN = "https://${passwordManagerDomain}";
# Configuring the server.
ROCKET_ADDRESS = "127.0.0.1";
ROCKET_PORT = 8222;
ROCKET_LOG = "critical";
# Ehh... It's only a few (or even one) users anyways so nah. Since this
# instance will not configure SMTP server, this pretty much means
# invitation is only via email at this point.
SHOW_PASSWORD_HINT = false;
# Configuring some parts of account management which is almost
# nonexistent because this is just intended for me (at least right now).
SIGNUPS_ALLOWED = false;
SIGNUPS_VERIFY = true;
# Invitations...
2022-11-23 05:27:01 +00:00
INVITATIONS_ALLOWED = true;
INVITATION_ORG_NAME = "foodogsquared's Vaultwarden";
2022-11-23 05:27:01 +00:00
# Notifications...
WEBSOCKET_ENABLED = true;
WEBSOCKET_PORT = 3012;
WEBSOCKET_ADDRESS = "0.0.0.0";
# Enabling web vault with whatever nixpkgs comes in.
WEB_VAULT_ENABLED = true;
# Configuring the database. Take note it is required to create a password
# for the user.
DATABASE_URL = "postgresql://${vaultwardenUser}@/${vaultwardenDbName}";
# Mailer service configuration (except the user and password).
SMTP_HOST = "smtp.sendgrid.net";
SMTP_PORT = 587;
SMTP_FROM_NAME = "Vaultwarden";
SMTP_FROM = "bot+vaultwarden@foodogsquared.one";
2022-11-23 05:27:01 +00:00
};
};
2022-12-02 23:40:21 +00:00
# Of course, what is a server without a backup? A professionally-handled
2022-12-16 14:25:50 +00:00
# production system. However, we're not professionals so we do have backups.
services.borgbackup.jobs =
let
jobCommonSettings = { patternFiles ? [ ], patterns ? [ ], paths ? [ ] }: {
inherit paths;
compression = "zstd,11";
dateFormat = "+%F-%H-%M-%S-%z";
doInit = true;
encryption = {
mode = "repokey-blake2";
passCommand = "cat ${config.sops.secrets."plover/borg/password".path}";
};
extraCreateArgs =
let
args = lib.flatten [
(builtins.map
(patternFile: "--patterns-from ${lib.escapeShellArg patternFile}")
patternFiles)
(builtins.map
(pattern: "--pattern ${lib.escapeShellArg pattern}")
patterns)
];
in
lib.concatStringsSep " " args;
extraInitArgs = "--make-parent-dirs";
persistentTimer = true;
preHook = ''
extraCreateArgs="$extraCreateArgs --stats"
'';
prune.keep = {
weekly = 4;
monthly = 12;
yearly = 6;
};
2022-12-16 14:25:50 +00:00
repo = "${borgbase-remote}@${borgbase-remote}.repo.borgbase.com:repo";
startAt = "monthly";
environment.BORG_RSH = "ssh -i ${config.sops.secrets."plover/ssh-key".path}";
2022-12-02 23:40:21 +00:00
};
in
{
# Backup for host-specific files. They don't change much so it is
# acceptable for it to be backed up monthly.
host-backup = jobCommonSettings {
patternFiles = [
config.sops.secrets."plover/borg/patterns/keys".path
];
};
# Backups for various services.
services-backup = jobCommonSettings
{
paths = [
# Vaultwarden
"/var/lib/bitwarden_rs"
# Gitea
config.services.gitea.dump.backupDir
# PostgreSQL database dumps
config.services.postgresqlBackup.location
];
} // { startAt = "weekly"; };
2022-12-02 23:40:21 +00:00
};
programs.ssh.extraConfig = ''
2022-12-16 14:25:50 +00:00
Host ${borgbase-remote}.repo.borgbase.com
2022-12-02 23:40:21 +00:00
IdentityFile ${config.sops.secrets."plover/ssh-key".path}
'';
systemd.tmpfiles.rules = let
# To be used similarly to $GITEA_CUSTOM variable.
giteaCustomDir = "${config.services.gitea.stateDir}/custom";
in [
"L+ ${giteaCustomDir}/templates/home.tmpl - - - - ${./files/gitea/home.tmpl}"
"L+ ${giteaCustomDir}/public/img/logo.svg - - - - ${./files/gitea/logo.svg}"
"L+ ${giteaCustomDir}/public/img/logo.png - - - - ${./files/gitea/logo.png}"
];
2022-11-23 05:27:01 +00:00
system.stateVersion = "22.11";
}