idiosyn/nixos/config/solitary.nix
2024-08-21 21:06:51 +02:00

738 lines
23 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{ self, ... }: { lib, config, pkgs, ... }:
with lib; let
ports = {
acme = 1360;
nginx = 8080;
synapse = 8008;
syncv3 = 8009;
unbound = 8484;
};
security-txt = pkgs.writeText "security.txt" ''
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
Canonical: https://solitary.social/.well-known/security.txt
Contact: mailto:mvs@nya.yt
Encryption: openpgp4fpr:950623eb2f52402e0cf56ccbee49e25700058dd6
Preferred-Languages: en, de
-----BEGIN PGP SIGNATURE-----
iHUEARYKAB0WIQRbjUmg6ccaTk940M7ZkbGDPEZ7AwUCYz2XrwAKCRDZkbGDPEZ7
A4w5AQD3Mzb5Bi8CERe3j3NjQhgeEkMVBcfM3RumuWdjs6i+LgD9HHuY3Bp6ljtR
LnLJRZt4Q8CYKoPaYkSO0vBaYKmUnwY=
=iQpI
-----END PGP SIGNATURE-----
'';
in {
imports = with self.nixosModules; [
default
headless
acme-ocsp
];
boot.loader.grub = {
enable = true;
device = "/dev/vda";
};
boot.kernelPackages = let
inherit (self.packages.x86_64-linux) linux-hardened;
in pkgs.linuxPackagesFor (linux-hardened.override {
instSetArch = "x86-64-v3";
extraConfig = linux-hardened.profile.paravirt;
});
environment.etc."machine-id".text = "1c97ae368741530de77aad42b5a6ae42";
ephemeral.device = "UUID=07a91cc3-4dd4-48e6-81d7-eb5d31fcf720";
ephemeral.boot.device = "UUID=24c72e0c-b467-4def-a641-ae09100465f0";
ephemeral.boot.fsType = "ext4";
i18n.supportedLocales = [ "C.UTF-8/UTF-8" "en_EU.UTF-8/UTF-8" "en_GB.UTF-8/UTF-8" ];
networking = {
hostName = "solitary";
domain = "social";
firewall.allowedTCPPorts = [ 22 80 443 853 ];
firewall.allowedUDPPorts = [ 443 ];
};
security.acme = {
certs.${config.networking.fqdn} = {
email = "mvs@nya.yt";
listenHTTP = "127.0.0.1:${toString ports.acme}";
reloadServices = [ "haproxy.service" "unbound.service" ];
extraDomainNames = [
"cache.solitary.social"
"matrix.solitary.social"
"media.solitary.social"
"resolve.solitary.social"
"syncv3.solitary.social"
];
};
};
services.akkoma.enable = true;
services.akkoma.extraStatic."emoji/blobs.gg" = pkgs.akkoma-emoji.blobs_gg;
services.akkoma.extraStatic."static/terms-of-service.html" = pkgs.writeText "terms-of-service.html" ''
<h2>Commitments</h2>
<p>This is currently a singleuser instance and therefore I decided to formulate what would be <em>Terms of Service</em> for a multiuser user instance as commitments. These are still incomplete and subject to expansion in the future.</p>
<ul>
<li>I shall observe and respect your boundaries.</li>
<li>I shall respect your right to disengage, and support you if you wish to disengage from others.</li>
<li>I shall accept that you may not want to be confronted with certain content and tag my posts appropriately.</li>
</ul>
'';
services.akkoma.extraStatic."favicon.png" = let
rev = "697a8211b0f427a921e7935a35d14bb3e32d0a2c";
in pkgs.stdenvNoCC.mkDerivation {
name = "favicon.png";
src = pkgs.fetchurl {
url = "https://raw.githubusercontent.com/TilCreator/NixOwO/${rev}/NixOwO_plain.svg";
hash = "sha256-tWhHMfJ3Od58N9H5yOKPMfM56hYWSOnr/TGCBi8bo9E=";
};
nativeBuildInputs = with pkgs; [ librsvg ];
dontUnpack = true;
installPhase = ''
rsvg-convert -o $out -w 96 -h 96 $src
'';
};
services.akkoma.config = let
elixir = pkgs.formats.elixirConf { };
in with elixir.lib; {
":pleroma" = {
":instance" = {
name = "solitary.social";
email = "mvs+solitary.social@nya.yt";
notify_email = "akkoma@solitary.social";
description = "Singleuser fediverse instance";
instance_thumbnail = "/instance/thumbnail.avif";
limit = 5120;
description_limit = 5120;
remote_limit = 131072;
upload_limit = 160 * 1024 * 1024;
avatar_upload_limit = 2097152;
background_upload_limit = 4194304;
banner_upload_limit = 4194304;
registrations_open = false;
account_approval_required = true;
remote_post_retention_days = 180;
user_bio_length = 5120;
user_name_length = 64;
max_account_fields = 8;
cleanup_attachments = true;
};
"Pleroma.Web.Endpoint" = {
secret_key_base._secret = "/var/lib/secrets/akkoma/key-base";
signing_salt._secret = "/var/lib/secrets/akkoma/signing-salt";
live_view.signing_salt._secret = "/var/lib/secrets/akkoma/liveview-salt";
};
"Pleroma.Emails.Mailer" = {
enabled = true;
adapter = mkRaw "Swoosh.Adapters.SMTP";
relay = "localhost";
dkim = {
a = "ed25519-sha256";
s = "akkoma";
d = config.networking.fqdn;
private_key = mkTuple [
(mkAtom ":pem_plain")
(mkRaw ''File.read!("/var/lib/akkoma/dkim.pem")'')
];
};
};
":database".rum_enabled = true;
":media_proxy" = {
enabled = true;
base_url = "https://cache.solitary.social";
proxy_opts.redirect_on_failure = true;
proxy_opts.max_body_length = 64 * 1024 * 1024;
};
":media_preview_proxy" = {
enabled = false;
thumbnail_max_width = 1920;
thumbnail_max_height = 1080;
min_content_length = 128 * 1024;
};
"Pleroma.Upload".base_url = "https://media.solitary.social";
"Pleroma.Upload".filters = map mkRaw [
"Pleroma.Upload.Filter.Exiftool.ReadDescription"
"Pleroma.Upload.Filter.Exiftool.StripMetadata"
"Pleroma.Upload.Filter.Dedupe"
"Pleroma.Upload.Filter.AnonymizeFilename"
];
":mrf".policies = map mkRaw [
"Pleroma.Web.ActivityPub.MRF.SimplePolicy"
"Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy"
];
":mrf_simple" = {
reject = mkMap {
"bae.st" = "harassment";
"brighteon.social" = "incompatible";
"detroitriotcity.com" = "incompatible";
"freeatlantis.com" = "incompatible";
"freespeechextremist.com" = "incompatible";
"gab.com" = "incompatible";
"gleasonator.com" = "incompatible";
"kitsunemimi.club" = "incompatible";
"poa.st" = "incompatible";
"seal.cafe" = "harassment";
"social.quodverum.com" = "incompatible";
"spinster.xyz" = "incompatible";
"truthsocial.co.in" = "incompatible";
"varishangout.net" = "incompatible";
"activitypub-troll.cf" = "security";
"misskey-forkbomb.cf" = "security";
"repl.co" = "security";
};
followers_only = mkMap {
"bitcoinhackers.org" = "annoying";
};
};
":mrf_object_age".threshold = 90 * 24 * 3600;
":frontend_configurations" = {
pleroma_fe = mkMap {
collapseMessageWithSubject = true;
hideSiteFavicon = true;
streaming = true;
webPushNotifications = true;
useStreamingApi = true;
scopeCopy = true;
showFeaturesPanel = false;
subjectLineBehavior = "masto";
alwaysShowSubjectInput = true;
postContentType = "text/markdown";
modalOnRepeat = true;
minimalScopesMode = true;
redirectRootNoLogin = "/mkl";
translationLanguage = "EN";
};
};
":restrict_unauthenticated" = {
timelines = mkMap {
local = false;
federated = true;
};
};
":translator" = {
enabled = true;
module = mkRaw "Pleroma.Akkoma.Translators.DeepL";
};
":deepl" = {
tier = mkAtom ":free";
api_key._secret = "/var/lib/secrets/akkoma/deepl";
};
};
":web_push_encryption".":vapid_details" = {
subject = "mailto:mvs+solitary.social@nya.yt";
public_key = "BPwdJZjBeZw_ZkWU_RQ48RdPI2pHIhMAYaNJc6xut4nQRi2YSaKnfP_kLrXzRjETQh5VJsDI-azYCeEhtk-C33s";
private_key._secret = "/var/lib/secrets/akkoma/vapid";
};
":joken".":default_signer"._secret = "/var/lib/secrets/akkoma/jwt-signer";
};
services.haproxy.enable = true;
services.haproxy.config =
let
ciphers = "ECDHE+CHACHA20:ECDHE+AESGCM";
cipherSuites = "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256";
options = "ssl-min-ver TLSv1.2 no-tls-tickets";
acmeDir = config.security.acme.certs.${config.networking.fqdn}.directory;
compTypes = [
"application/javascript"
"application/json"
"image/svg+xml"
"text/css"
"text/html"
"text/javascript"
"text/plain"
];
in ''
global
expose-experimental-directives
log stderr format short alert notice
maxconn 9216
nbthread 2
cpu-map auto:1/all 0-63
ssl-default-bind-ciphers ${ciphers}
ssl-default-bind-ciphersuites ${cipherSuites}
ssl-default-bind-options prefer-client-ciphers ${options}
ssl-default-server-ciphers ${ciphers}
ssl-default-server-ciphersuites ${cipherSuites}
ssl-default-server-options ${options}
defaults
log global
mode http
option abortonclose
option checkcache
option forwardfor
option http-keep-alive
option http-restrict-req-hdr-names reject
option httpchk
option splice-auto
option tcp-smart-connect
timeout connect 5s
timeout client 30s
timeout http-request 5s
timeout client-fin 5s
timeout tunnel 1h
timeout server 30s
timeout check 5s
cache default
total-max-size 16
max-age 240
frontend http
bind :::443 v4v6 defer-accept tfo ssl crt ${acmeDir}/full.pem allow-0rtt alpn h2,http/1.1
bind quic6@:443 v4v6 ssl crt ${acmeDir}/full.pem allow-0rtt alpn h3
bind :::80 v4v6 defer-accept tfo
acl replay-safe method GET HEAD OPTIONS req.body_size eq 0
acl host-solitary hdr(host),host_only solitary.social
acl host-cache hdr(host),host_only cache.solitary.social
acl host-media hdr(host),host_only media.solitary.social
acl host-matrix hdr(host),host_only matrix.solitary.social
acl host-syncv3 hdr(host),host_only syncv3.solitary.social
acl host-resolve hdr(host),host_only resolve.solitary.social
acl path-acme path_dir /.well-known/acme-challenge
acl path-security.txt path /.well-known/security.txt
acl path-matrix-well-known path_dir /.well-known/matrix
acl path-proxy path_dir /proxy
acl path-media path_dir /media
#http-request normalize-uri fragment-strip
#http-request normalize-uri path-strip-dot
#http-request normalize-uri path-strip-dotdot full
#http-request normalize-uri path-merge-slashes
#http-request normalize-uri percent-decode-unreserved strict
#http-request normalize-uri percent-to-uppercase strict
#http-request normalize-uri query-sort-by-name
http-request redirect scheme https code 301 unless { ssl_fc } or path-acme
http-request wait-for-handshake unless replay-safe
http-response set-tos 20 if path-acme # AF22 (lowlatency, med drop)
http-response set-tos 20 if host-resolve # AF22 (lowlatency, med drop)
http-response set-tos 10 if host-matrix # AF11 (highthroughput, low drop)
http-response set-tos 10 if host-syncv3 # AF11 (highthroughput, low drop)
http-response set-tos 12 if host-solitary # AF12 (highthroughput, med drop)
http-response set-tos 14 if host-media # AF13 (highthroughput, high drop)
http-response set-tos 14 if host-cache # AF13 (highthroughput, high drop)
http-request cache-use default
http-request set-header X-Forwarded-Proto %[ssl_fc,iif(https,http)]
acl no-coep res.hdr(Cross-Origin-Embedder-Policy) -m len 0
acl no-coop res.hdr(Cross-Origin-Opener-Policy) -m len 0
acl no-corp res.hdr(Cross-Origin-Resource-Policy) -m len 0
acl no-csp res.hdr(Content-Security-Policy) -m len 0
acl no-rp res.hdr(Referrer-Policy) -m len 0
http-response set-header Alt-Svc "h3=\":443\""
http-response set-header Cross-Origin-Embedder-Policy require-corp if no-coep
http-response set-header Cross-Origin-Opener-Policy same-origin if no-coop
http-response set-header Cross-Origin-Resource-Policy same-origin if no-corp
http-response set-header Content-Security-Policy "default-src 'self'; frame-ancestors 'none'" if no-csp
http-response set-header Referrer-Policy same-origin if no-rp
http-response set-header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"
http-response set-header X-Frame-Options DENY
http-response set-header X-Content-Type-Options nosniff
http-response set-header X-XSS-Protection "1; mode=block"
compression algo gzip
compression type ${concatStringsSep " " compTypes}
http-response cache-store default
http-request redirect code 308 location https://media.solitary.social%[capture.req.uri,regsub("^/media","")] if host-solitary path-media
http-request redirect code 308 location https://media.solitary.social%[capture.req.uri,regsub("^/media","")] if host-media path-media
http-request redirect code 308 location https://cache.solitary.social%[capture.req.uri] if host-solitary path-proxy
http-request set-path "/media%[path]" if host-media !path-media
use_backend acme if path-acme
use_backend security.txt if path-security.txt
use_backend unbound if host-resolve
use_backend synapse if host-matrix
use_backend syncv3 if host-syncv3
use_backend wellknown-matrix if host-solitary path-matrix-well-known
use_backend nginx if host-cache
use_backend akkoma if host-solitary
use_backend akkoma if host-media
default_backend notfound
backend acme
server acme 127.0.0.1:${toString ports.acme}
retry-on all-retryable-errors
backend akkoma
server akkoma /run/akkoma/socket
backend nginx
server nginx [::1]:${toString ports.nginx} tfo proto h2
retry-on conn-failure empty-response response-timeout
backend synapse
server synapse [::1]:${toString ports.synapse}
backend syncv3
server syncv3 [::1]:${toString ports.syncv3}
backend unbound
server unbound [::1]:${toString ports.unbound} tfo ssl ssl-min-ver TLSv1.3 alpn h2 allow-0rtt ca-file ${acmeDir}/chain.pem
retry-on conn-failure empty-response response-timeout 0rtt-rejected
backend security.txt
http-request return status 200 content-type text/plain file ${security-txt} if { path /.well-known/security.txt }
backend wellknown-matrix
http-request return status 200 content-type application/json file ${pkgs.writeText "client.json" (builtins.toJSON {
"m.homeserver".base_url = config.services.matrix-synapse.settings.public_baseurl;
"m.identity_server".base_url = "https://vector.im";
"org.matrix.msc3575.proxy".url = "https://syncv3.solitary.social";
})} if { path /.well-known/matrix/client }
http-request return status 200 content-type application/json file ${pkgs.writeText "server.json" (builtins.toJSON {
"m.server" = "matrix.solitary.social:443";
})} if { path /.well-known/matrix/server }
backend notfound
http-request return status 404
'';
services.matrix-synapse.enable = true;
services.matrix-synapse.settings = {
database_type = "psycopg2";
server_name = "solitary.social";
public_baseurl = "https://matrix.solitary.social/";
default_identity_server = "https://vector.im";
enable_registration = false;
listeners = [ {
bind_addresses = [ "::1" ];
port = ports.synapse;
type = "http";
tls = false;
x_forwarded = true;
resources = [ {
names = [ "client" "federation" ];
compress = true;
} ];
} ];
log_config = ./log_config.yaml;
};
services.nginx = {
enable = true;
package = pkgs.tengine;
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedProxySettings = true;
commonHttpConfig = ''
charset utf-8;
proxy_cache_path /var/cache/nginx/cache/akkoma_media_cache
levels= keys_zone=akkoma_media_cache:16m max_size=16g
inactive=1y use_temp_path=off;
access_log off;
set_real_ip_from ::1;
real_ip_header X-Forwarded-For;
'';
};
services.matrix-sliding-sync = {
enable = true;
environmentFile = "/etc/keys/sliding-sync.env";
settings = {
SYNCV3_BINDADDR = "[::1]:${toString ports.syncv3}";
SYNCV3_LOG_LEVEL = "warn";
SYNCV3_SERVER = "https://matrix.solitary.social";
};
};
services.nginx.virtualHosts."cache.solitary.social" = {
listen = [ {
addr = "[::1]";
port = ports.nginx;
extraParameters = [ "http2" "fastopen=512" ];
} ];
locations."/" = {
proxyPass = "http://unix:/run/akkoma/socket";
extraConfig = ''
proxy_cache akkoma_media_cache;
slice 1m;
proxy_cache_key $host$uri$is_args$args$slice_range;
proxy_set_header Range $slice_range;
proxy_buffering on;
proxy_cache_lock on;
proxy_ignore_client_abort on;
proxy_cache_valid 200 1y;
proxy_cache_valid 206 301 304 1h;
proxy_cache_use_stale error timeout invalid_header updating;
'';
};
};
services.postfix = {
enable = true;
destination = [ ];
localRecipients = [ ];
networks = [ "localhost" ];
hostname = config.networking.fqdn;
masterConfig.smtp_inet.name = mkForce "localhost:smtp";
};
services.postgresql = {
enable = true;
package = pkgs.postgresql_14;
extraPlugins = with pkgs.postgresql_14.pkgs; [
rum
];
initialScript = pkgs.writeText "init.psql" ''
CREATE ROLE "matrix-synapse";
CREATE DATABASE "matrix-synapse" OWNER "matrix-synapse"
TEMPLATE template0
ENCODING 'utf8'
LOCALE 'C';
'';
};
services.unbound = {
enable = true;
package = pkgs.unbound-with-systemd.override {
withDoH = true;
withTFO = true;
};
enableRootTrustAnchor = true;
};
services.unbound.settings = {
server = let
acmeDir = config.security.acme.certs.${config.networking.fqdn}.directory;
num-threads = 2;
in {
inherit num-threads;
interface = [
"::1@53"
"127.0.0.1@53"
"::1@${toString ports.unbound}"
"::@853"
"0.0.0.0@853"
];
so-reuseport = true;
ip-dscp = 20;
outgoing-range = 8192;
edns-buffer-size = 1472;
udp-upstream-without-downstream = true;
num-queries-per-thread = 4096;
incoming-num-tcp = 1024;
outgoing-num-tcp = 16;
stream-wait-size = "64m";
msg-cache-size = "128m";
msg-cache-slabs = num-threads;
rrset-cache-size = "256m";
rrset-cache-slabs = num-threads;
infra-cache-slabs = num-threads;
key-cache-slabs = num-threads;
cache-min-ttl = 60;
cache-max-negative-ttl = 360;
prefer-ip6 = true;
tls-service-pem = "${acmeDir}/fullchain.pem";
tls-service-key = "${acmeDir}/key.pem";
https-port = ports.unbound;
http-query-buffer-size = "64m";
http-response-buffer-size = "64m";
access-control = [ "::/0 allow" "0.0.0.0/0 allow" ];
harden-dnssec-stripped = true;
hide-identity = true;
hide-version = true;
prefetch = true;
prefetch-key = true;
serve-expired-client-timeout = 1800;
};
};
systemd = let
backendServices = [
"akkoma.service"
"matrix-synapse.service"
"nginx.service"
"unbound.service"
];
in {
services.akkoma.confinement.enable = false;
services.akkoma.serviceConfig.BindReadOnlyPaths = [ "/var/lib/akkoma:/var/lib/akkoma:norbind" ];
services.haproxy = {
confinement.enable = false;
wants = [ "acme-finished-${config.networking.fqdn}.service" ]
++ backendServices;
after = [ "acme-selfsigned-${config.networking.fqdn}.service" ]
++ backendServices;
before = [ "acme-${config.networking.fqdn}.service" ];
reloadTriggers = [ "${config.security.acme.certs.${config.networking.fqdn}.directory}/cert.ocsp" ];
serviceConfig = {
BindReadOnlyPaths = [
"/etc/haproxy.cfg"
"/etc/hosts"
"/etc/resolv.conf"
"/run/akkoma"
config.security.acme.certs."solitary.social".directory
security-txt
];
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
SocketBindAllow = [ "tcp:80" "tcp:443" "udp:443" ];
SocketBindDeny = "any";
};
};
services.matrix-sliding-sync = {
after = [ "matrix-synapse.service" ];
serviceConfig.ReadOnlyPaths = [
"/run/postgres"
];
};
services.nginx = {
confinement.enable = true;
after = [ "akkoma.service" ];
serviceConfig = {
BindReadOnlyPaths = [
"/etc/hosts"
"/etc/resolv.conf"
"/run"
];
BindPaths = [
"/var/cache/nginx"
];
ProtectSystem = mkForce false;
SocketBindAllow = [ "tcp:${toString ports.nginx}" ];
SocketBindDeny = "any";
RestrictNetworkInterfaces = [ "lo" ];
};
};
services.unbound = {
wants = [ "acme-finished-${config.networking.fqdn}.service" ];
after = [ "acme-selfsigned-${config.networking.fqdn}.service" ];
};
services.synapse-compress-state = {
confinement.enable = true;
after = [ "postgresql.service" ];
description = "Compress Synapse state tables";
serviceConfig = {
Type = "oneshot";
ExecStart = ''
${pkgs.matrix-synapse-tools.rust-synapse-compress-state}/bin/synapse_auto_compressor \
-p "host=/run/postgresql \
user=${config.services.matrix-synapse.settings.database.args.database} \
dbname=${config.services.matrix-synapse.settings.database.args.database}" \
-c 512 -n 128
'';
User = "matrix-synapse";
WorkingDirectory = "/tmp";
BindReadOnlyPaths = [
"/run/postgresql"
];
ProtectProc = "noaccess";
ProcSubset = "pid";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
PrivateIPC = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
RestrictAddressFamilies = [ "AF_UNIX" ];
RestrictNamespaces = true;
LockPersonality = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
RemoveIPC = true;
CapabilityBoundingSet = null;
NoNewPrivileges = true;
SystemCallFilter = [ "@system-service" "~@resources" "~@privileged" ];
SystemCallArchitectures = "native";
UMask = "0077";
};
};
timers.synapse-compress-state = {
enable = true;
description = "Compress Synapse state tables daily";
timerConfig = {
OnCalendar = "04:00";
};
wantedBy = [ "timers.target" ];
};
};
users.users.${config.services.haproxy.user}.extraGroups = [ config.security.acme.certs.${config.networking.fqdn}.group ];
users.users.${config.services.unbound.user}.extraGroups = [ config.security.acme.certs.${config.networking.fqdn}.group ];
}