idiosyn/nixos/config/solitary.nix

739 lines
23 KiB
Nix
Raw Normal View History

2024-08-18 13:47:18 +02:00
{ 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 ];
}