Compare commits
9 Commits
2e9ea868b6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ac2ca3c8be | |||
| 9f7431bb36 | |||
| b80316267f | |||
| e64c3d1310 | |||
| 75be43140f | |||
| 38bd5b63f5 | |||
| 31de8eccb0 | |||
| cb8835bbfb | |||
| ea79663c96 |
@@ -1 +1 @@
|
|||||||
/nix/store/d8ysys9mxibkvpiskrvsdiybhwkdnj3d-nixos-system-debian-4gb-fsn1-1-24.05.20241230.b134951
|
/nix/store/p2h0fr7k47yrx3x0qkr3rwsp5nf5bj30-nixos-system-debian-4gb-fsn1-1-24.05.20241230.b134951
|
||||||
@@ -12,7 +12,6 @@
|
|||||||
../../modules/services/wireguard.nix
|
../../modules/services/wireguard.nix
|
||||||
../../modules/services/fail2ban.nix
|
../../modules/services/fail2ban.nix
|
||||||
../../modules/services/vaultwarden.nix
|
../../modules/services/vaultwarden.nix
|
||||||
../../modules/services/mailserver.nix
|
|
||||||
../../modules/services/rustdesk.nix
|
../../modules/services/rustdesk.nix
|
||||||
../../modules/services/wrxproxy.nix
|
../../modules/services/wrxproxy.nix
|
||||||
../../modules/services/watchtower.nix
|
../../modules/services/watchtower.nix
|
||||||
@@ -21,7 +20,7 @@
|
|||||||
../../modules/services/backup.nix
|
../../modules/services/backup.nix
|
||||||
../../modules/services/stalwart.nix
|
../../modules/services/stalwart.nix
|
||||||
../../modules/services/virtualcam.nix
|
../../modules/services/virtualcam.nix
|
||||||
../../modules/system/opencode.nix
|
../../modules/services/netdata.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
# Only 4GB RAM — limit nix builds to one core at a time to avoid OOM
|
# Only 4GB RAM — limit nix builds to one core at a time to avoid OOM
|
||||||
|
|||||||
@@ -57,12 +57,13 @@ in {
|
|||||||
"/var/lib/gitea"
|
"/var/lib/gitea"
|
||||||
"/var/lib/caddy"
|
"/var/lib/caddy"
|
||||||
"/var/lib/virtualcam"
|
"/var/lib/virtualcam"
|
||||||
|
"/var/lib/stalwart"
|
||||||
|
"/var/lib/bulwark"
|
||||||
"/var/lib/coredns"
|
"/var/lib/coredns"
|
||||||
"/etc/nixos"
|
"/etc/nixos"
|
||||||
];
|
];
|
||||||
exclude = [
|
exclude = [
|
||||||
"/home/admin/backups"
|
"/home/admin/backups"
|
||||||
"/home/admin/dms/mail-logs"
|
|
||||||
"/home/admin/.opencode"
|
"/home/admin/.opencode"
|
||||||
"/home/admin/.local"
|
"/home/admin/.local"
|
||||||
"/home/admin/.npm"
|
"/home/admin/.npm"
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ in {
|
|||||||
# Global Caddyfile snippets (shared across all virtual hosts).
|
# Global Caddyfile snippets (shared across all virtual hosts).
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
(admin_gate) {
|
(admin_gate) {
|
||||||
@notvpn not remote_ip 10.8.0.0/24
|
@notvpn not remote_ip 127.0.0.1 ::1 10.8.0.0/24 fd10:8::/64 49.13.92.205 2a01:4f8:c014:2585::1
|
||||||
respond @notvpn "Forbidden" 403
|
respond @notvpn "Forbidden" 403
|
||||||
}
|
}
|
||||||
# Restrict access to the server itself (loopback + public IPs) or the
|
# Restrict access to the server itself (loopback + public IPs) or the
|
||||||
# WireGuard VPN. Gitea reaches this from the server when publishing a
|
# WireGuard VPN. Gitea reaches this from the server when publishing a
|
||||||
# release, and VPN clients can download from it; the public cannot.
|
# release, and VPN clients can download from it; the public cannot.
|
||||||
(release_gate) {
|
(release_gate) {
|
||||||
@deny not remote_ip 127.0.0.1 ::1 10.8.0.0/24 49.13.92.205 2a01:4f8:c014:2585::1
|
@deny not remote_ip 127.0.0.1 ::1 10.8.0.0/24 fd10:8::/64 49.13.92.205 2a01:4f8:c014:2585::1
|
||||||
respond @deny "Forbidden" 403
|
respond @deny "Forbidden" 403
|
||||||
}
|
}
|
||||||
# Security headers applied to every response of every site that
|
# Security headers applied to every response of every site that
|
||||||
@@ -161,8 +161,23 @@ in {
|
|||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
import security_headers
|
import security_headers
|
||||||
${antiScrape}
|
${antiScrape}
|
||||||
# Bulwark webmail (JMAP client for Stalwart), running on host port 3002.
|
# Stalwart JMAP API (http-management listener on 8080): required so the
|
||||||
reverse_proxy 127.0.0.1:3002
|
# browser can reach /.well-known/jmap, /jmap/session and the JMAP
|
||||||
|
# upload/download/eventsource/ws paths at the public origin.
|
||||||
|
handle /.well-known/jmap {
|
||||||
|
reverse_proxy 127.0.0.1:8080
|
||||||
|
}
|
||||||
|
# NOTE: use handle (not handle_path) so the /jmap prefix is preserved.
|
||||||
|
handle /jmap {
|
||||||
|
reverse_proxy 127.0.0.1:8080
|
||||||
|
}
|
||||||
|
handle /jmap/* {
|
||||||
|
reverse_proxy 127.0.0.1:8080
|
||||||
|
}
|
||||||
|
handle {
|
||||||
|
# Bulwark webmail (JMAP client for Stalwart), running on host port 3002.
|
||||||
|
reverse_proxy 127.0.0.1:3002
|
||||||
|
}
|
||||||
encode zstd gzip
|
encode zstd gzip
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
@@ -230,7 +245,6 @@ in {
|
|||||||
import security_headers
|
import security_headers
|
||||||
import csp
|
import csp
|
||||||
${antiScrape}
|
${antiScrape}
|
||||||
import admin_gate
|
|
||||||
reverse_proxy 127.0.0.1:5000
|
reverse_proxy 127.0.0.1:5000
|
||||||
encode zstd gzip
|
encode zstd gzip
|
||||||
'';
|
'';
|
||||||
@@ -249,13 +263,27 @@ in {
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
"api.severijnse.eu" = {
|
||||||
|
extraConfig = ''
|
||||||
|
import security_headers
|
||||||
|
# Machine-facing license API for the Android app: no antiScrape
|
||||||
|
# bot-blocking (it would drop legitimate non-browser clients) and no
|
||||||
|
# admin_gate (endpoints are protected by the bearer token).
|
||||||
|
reverse_proxy 127.0.0.1:3004
|
||||||
|
encode zstd gzip
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
"releases.severijnse.eu" = {
|
"releases.severijnse.eu" = {
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
import security_headers
|
import security_headers
|
||||||
import release_gate
|
|
||||||
root * /srv/releases
|
root * /srv/releases
|
||||||
file_server browse
|
file_server browse
|
||||||
encode zstd gzip
|
encode zstd gzip
|
||||||
|
|
||||||
|
handle /private* {
|
||||||
|
import release_gate
|
||||||
|
}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
mail._domainkey.severijnse.eu. 300 IN TXT (
|
mail._domainkey.severijnse.eu. 300 IN TXT (
|
||||||
"v=DKIM1; k=rsa; "
|
"v=DKIM1; k=rsa; "
|
||||||
"p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAskpG7m4kninxRE4JF5KzpnBhLbOlGJL2RO/iDfzdz6sHEvxe78q9c5UnZ0OQddOSuQo4q48dJkXR/XzqY7Ak109lhoAx+Kr1neYsi8/"
|
"p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAskpG7m4kninxRE4JF5KzpnBhLbOlGJL2RO/iDfzdz6sHEvxe78q9c5UnZ0OQddOSuQo4q48dJkXR/XzqY7Ak109lhoAx+Kr1neYsi8K/"
|
||||||
"JaoTC8OURk365+/aBSmWXUCCBphCx43QWfC9h8GMQ6PUIqawkz5CcGPT7X7hPdwHQcd5Vn3CqmABptxdwshdkBjZs"
|
"JaoTC8OURk365+/aBSmWXUCCBphCx43QWfC9h8GMQ6PUIqawkz5CcGPT7X7hPdwHQcd5Vn3CqmABptxdwshdkBjZs"
|
||||||
"oi79BOo9ZrQSTY7iiLcOP7hVVC9Ad+ydlZ4MWGfy5BxgyTGrrtuSuLcM219oqdovIvr2EtXs8AMx5fyXplKE3R/"
|
"oi79BOo9ZrQSTY7iiLcOP7hVVC9Ad+ydlZ4MWGfy5BxgyTGrrtuSuLcM219oqdovIvr2EtXs8AMx5fyXplKE3R/"
|
||||||
"YlwF2Jcy50Gmb5y/E9pOaFjVv8HXUmKsvuhA2b8K+rt0WVHNc3dvbgZUl8bGAQIDAQAB"
|
"YlwF2Jcy50Gmb5y/E9pOaFjVv8HXUmKsvuhA2b8K+rt0WVHNc3dvbgZUl8bGAQIDAQAB"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
image = "crazymax/fail2ban:latest";
|
image = "crazymax/fail2ban:latest";
|
||||||
autoStart = true;
|
autoStart = true;
|
||||||
volumes = [
|
volumes = [
|
||||||
"/home/admin/dms/mail-logs:/var/log/mail:ro"
|
|
||||||
"/home/admin/fail2ban/data:/data:Z"
|
"/home/admin/fail2ban/data:/data:Z"
|
||||||
];
|
];
|
||||||
environment = {
|
environment = {
|
||||||
@@ -15,8 +14,4 @@
|
|||||||
"--cap-add=NET_ADMIN"
|
"--cap-add=NET_ADMIN"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
|
||||||
"d /var/log/mail 0755 root root -"
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
#
|
||||||
|
# Netdata
|
||||||
|
#
|
||||||
|
services.netdata = {
|
||||||
|
enable = true;
|
||||||
|
config = {
|
||||||
|
global = {
|
||||||
|
"memory mode" = "ram";
|
||||||
|
"debug log" = "none";
|
||||||
|
"access log" = "none";
|
||||||
|
"error log" = "syslog";
|
||||||
|
};
|
||||||
|
web = {
|
||||||
|
"bind to" = "127.0.0.1:19999";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.caddy.virtualHosts."netdata.severijnse.eu" = {
|
||||||
|
extraConfig = ''
|
||||||
|
import security_headers
|
||||||
|
import csp
|
||||||
|
reverse_proxy 127.0.0.1:19999
|
||||||
|
encode zstd gzip
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -22,6 +22,18 @@
|
|||||||
| tr -d '\n' > "${adminHashFile}"
|
| tr -d '\n' > "${adminHashFile}"
|
||||||
chmod 0600 "${adminHashFile}"
|
chmod 0600 "${adminHashFile}"
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
# The outbound DKIM key is the existing opendkim key from docker-mailserver
|
||||||
|
# (selector "mail", domain severijnse.eu, PKCS#8 RSA 2048). Install it into the
|
||||||
|
# stalwart-owned data dir so the server can read it and it is covered by the
|
||||||
|
# /var/lib/stalwart backup.
|
||||||
|
dkimKeyDir = "/var/lib/stalwart/dkim/severijnse.eu";
|
||||||
|
dkimKeySrc = "/home/admin/dms/config/opendkim/keys/severijnse.eu/mail.private";
|
||||||
|
writeDkimKey = pkgs.writeShellScript "stalwart-write-dkim-key" ''
|
||||||
|
set -euo pipefail
|
||||||
|
install -d -o stalwart -g stalwart -m 0750 ${dkimKeyDir}
|
||||||
|
install -o stalwart -g stalwart -m 0640 ${dkimKeySrc} ${dkimKeyDir}/mail.private
|
||||||
|
'';
|
||||||
in {
|
in {
|
||||||
# The hetzner host is built with nixos-24.05, which ships its own
|
# The hetzner host is built with nixos-24.05, which ships its own
|
||||||
# `services.stalwart-mail` module (for the old 0.8.x package). We want the
|
# `services.stalwart-mail` module (for the old 0.8.x package). We want the
|
||||||
@@ -41,16 +53,27 @@ in {
|
|||||||
stateVersion = "26.05";
|
stateVersion = "26.05";
|
||||||
package = unstablePkgs.stalwart;
|
package = unstablePkgs.stalwart;
|
||||||
|
|
||||||
# Temporary internal listeners while docker-mailserver still owns 25/143/465/587/993.
|
# Public listeners. The host firewall (networking.nix) already allows
|
||||||
|
# 25/143/465/587/993, so openFirewall stays off: the module would also open
|
||||||
|
# every other listener port incl. the 8080 webadmin.
|
||||||
openFirewall = false;
|
openFirewall = false;
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
# EHLO / hostname for the server (docs server.hostname).
|
# EHLO / hostname for the server (docs server.hostname).
|
||||||
server.hostname = "mail.severijnse.eu";
|
server.hostname = "mail.severijnse.eu";
|
||||||
|
|
||||||
|
# Public origin the JMAP/webadmin API is served from, advertised in the
|
||||||
|
# JMAP session (docs server/core/network.md "http.url"). Value must be a
|
||||||
|
# JScript expression, hence the single-quoted string literal. Without this
|
||||||
|
# Stalwart advertises http://mail.severijnse.eu:8080, which the browser
|
||||||
|
# refuses to fetch and breaks Bulwark webmail.
|
||||||
|
http.url = "'https://mail.severijnse.eu'";
|
||||||
|
|
||||||
certificate."mail-severijnse-eu" = {
|
certificate."mail-severijnse-eu" = {
|
||||||
cert = "%{file:${certDir}/mail.severijnse.eu.crt}%";
|
cert = "%{file:${certDir}/mail.severijnse.eu.crt}%";
|
||||||
private-key = "%{file:${certDir}/mail.severijnse.eu.key}%";
|
private-key = "%{file:${certDir}/mail.severijnse.eu.key}%";
|
||||||
|
# Docs server/tls/certificates.md: used when the client sends no SNI.
|
||||||
|
default = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
server.tls = {
|
server.tls = {
|
||||||
@@ -59,18 +82,32 @@ in {
|
|||||||
implicit = false;
|
implicit = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
# Temporary internal listeners (docs server/listener.md + protocol, tls.implicit override).
|
# Public listeners (docs server/listener.md + protocol, tls.implicit override).
|
||||||
|
# Bind "[::]:port" for dual-stack IPv4+IPv6 (docs: "to bind a listener to
|
||||||
|
# all interfaces"); listing both 0.0.0.0 and [::] makes the [::] bind fail
|
||||||
|
# with EADDRINUSE on kernels with net.ipv6.bindv6only=0.
|
||||||
|
# 143/587 use STARTTLS (server.tls.implicit=false default), 993/465 the
|
||||||
|
# implicit-TLS variants, 25 the plain (STARTTLS) MX port.
|
||||||
server.listener = {
|
server.listener = {
|
||||||
"imap" = {
|
"imap" = {
|
||||||
bind = ["127.0.0.1:1143"];
|
bind = ["[::]:143"];
|
||||||
protocol = "imap";
|
protocol = "imap";
|
||||||
};
|
};
|
||||||
|
"imaps" = {
|
||||||
|
bind = ["[::]:993"];
|
||||||
|
protocol = "imap";
|
||||||
|
tls.implicit = true;
|
||||||
|
};
|
||||||
|
"smtp" = {
|
||||||
|
bind = ["[::]:25"];
|
||||||
|
protocol = "smtp";
|
||||||
|
};
|
||||||
"smtp-submission" = {
|
"smtp-submission" = {
|
||||||
bind = ["127.0.0.1:1587"];
|
bind = ["[::]:587"];
|
||||||
protocol = "smtp";
|
protocol = "smtp";
|
||||||
};
|
};
|
||||||
"smtp-submissions" = {
|
"smtp-submissions" = {
|
||||||
bind = ["127.0.0.1:1465"];
|
bind = ["[::]:465"];
|
||||||
protocol = "smtp";
|
protocol = "smtp";
|
||||||
tls.implicit = true;
|
tls.implicit = true;
|
||||||
};
|
};
|
||||||
@@ -80,10 +117,29 @@ in {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Auth per inbound/auth.md: not required on the plain SMTP listener (port 25),
|
# Auth per docs mta/inbound/auth.md (AUTH stage): authentication is
|
||||||
# required everywhere else (IMAP + submission). Directory is the module default "internal".
|
# disabled on the plain SMTP listener (port 25) and required everywhere
|
||||||
session.auth.mechanisms = "[plain]";
|
# else (IMAP + submission). Only offer PLAIN/LOGIN over TLS, so clear-text
|
||||||
session.auth.directory = "'internal'";
|
# listeners (143/587 pre-STARTTLS) advertise no SASL mechanisms. This
|
||||||
|
# mirrors the code defaults (crates/common/src/config/smtp/session.rs).
|
||||||
|
# The JMAP/webadmin "http-management" listener (bind 127.0.0.1:8080) is
|
||||||
|
# plain HTTP and reachable only from localhost, where bulwark connects;
|
||||||
|
# its basic-auth uses the "plain" mechanism, so it must be exempted from
|
||||||
|
# the is_tls gate or webmail logins fail with "Authentication not allowed".
|
||||||
|
session.auth.mechanisms = [
|
||||||
|
{
|
||||||
|
"if" = "local_port != 25 && (is_tls || listener == 'http-management')";
|
||||||
|
"then" = "[plain, login]";
|
||||||
|
}
|
||||||
|
{"else" = false;}
|
||||||
|
];
|
||||||
|
session.auth.directory = [
|
||||||
|
{
|
||||||
|
"if" = "listener != 'smtp'";
|
||||||
|
"then" = "'internal'";
|
||||||
|
}
|
||||||
|
{"else" = false;}
|
||||||
|
];
|
||||||
session.auth.require = [
|
session.auth.require = [
|
||||||
{
|
{
|
||||||
"if" = "listener != 'smtp'";
|
"if" = "listener != 'smtp'";
|
||||||
@@ -92,6 +148,43 @@ in {
|
|||||||
{"else" = false;}
|
{"else" = false;}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# Outbound DKIM signing (docs mta/authentication/dkim/sign): sign with the
|
||||||
|
# "mail" signature on everything submitted via non-25 listeners; do not sign
|
||||||
|
# inbound mail received on the plain "smtp" listener.
|
||||||
|
auth.dkim.sign = [
|
||||||
|
{
|
||||||
|
"if" = "listener != 'smtp'";
|
||||||
|
"then" = "['mail']";
|
||||||
|
}
|
||||||
|
{"else" = false;}
|
||||||
|
];
|
||||||
|
|
||||||
|
# ARC sealing uses the same "mail" signature (docs mta/authentication/arc).
|
||||||
|
# The code default ('rsa-' + report.domain) would reference a signature
|
||||||
|
# name that does not exist and log "ARC sealer not found".
|
||||||
|
auth.arc.seal = "'mail'";
|
||||||
|
|
||||||
|
# Every *downstream* sign rule defaults to signing with
|
||||||
|
# ['rsa-<report.domain>', 'ed25519-<report.domain>'] (queue.rs/report.rs),
|
||||||
|
# names that do not exist here and log "DKIM signer not found" on DSNs and
|
||||||
|
# reports. Point them all at the real "mail" signature.
|
||||||
|
report.dsn.sign = "['mail']";
|
||||||
|
report.spf.sign = "['mail']";
|
||||||
|
report.dmarc.sign = "['mail']";
|
||||||
|
report.dmarc.aggregate.sign = "['mail']";
|
||||||
|
report.tls.aggregate.sign = "['mail']";
|
||||||
|
|
||||||
|
# Reuse the existing opendkim key (selector mail) so no DNS change is needed.
|
||||||
|
signature.mail = {
|
||||||
|
private-key = "%{file:/var/lib/stalwart/dkim/severijnse.eu/mail.private}%";
|
||||||
|
domain = "severijnse.eu";
|
||||||
|
selector = "mail";
|
||||||
|
headers = ["From" "To" "Date" "Subject" "Message-Id"];
|
||||||
|
algorithm = "rsa-sha256";
|
||||||
|
canonicalization = "relaxed/relaxed";
|
||||||
|
set-body-length = false;
|
||||||
|
};
|
||||||
|
|
||||||
# Fallback admin (auth/authorization/administrator.md): bootstrap admin with
|
# Fallback admin (auth/authorization/administrator.md): bootstrap admin with
|
||||||
# every permission, used to create the internal-directory accounts via the
|
# every permission, used to create the internal-directory accounts via the
|
||||||
# management REST API / CLI. Secret is a SHA-512-crypt hash, injected via
|
# management REST API / CLI. Secret is a SHA-512-crypt hash, injected via
|
||||||
@@ -102,8 +195,10 @@ in {
|
|||||||
secret = "%{file:/run/credentials/stalwart.service/stalwart-admin}%";
|
secret = "%{file:/run/credentials/stalwart.service/stalwart-admin}%";
|
||||||
};
|
};
|
||||||
|
|
||||||
# Route docs routing: /strategy.md + /routing.md:
|
# Route docs mta/outbound/routing.md + strategy.md: local domains → local
|
||||||
# local domains → local store, everything else → MX. local/mx are built-in.
|
# store, everything else → MX. The strategy names are defined explicitly
|
||||||
|
# (docs define queue.route.mx/local; the built-in fallback in
|
||||||
|
# core.rs:get_route_or_default only kicks in for undeclared names).
|
||||||
queue.strategy.route = [
|
queue.strategy.route = [
|
||||||
{
|
{
|
||||||
"if" = "is_local_domain('', rcpt_domain)";
|
"if" = "is_local_domain('', rcpt_domain)";
|
||||||
@@ -111,6 +206,13 @@ in {
|
|||||||
}
|
}
|
||||||
{"else" = "'mx'";}
|
{"else" = "'mx'";}
|
||||||
];
|
];
|
||||||
|
queue.route."mx" = {
|
||||||
|
type = "mx";
|
||||||
|
ip-lookup = "ipv4_then_ipv6";
|
||||||
|
};
|
||||||
|
queue.route."local" = {
|
||||||
|
type = "local";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,8 +220,8 @@ in {
|
|||||||
# ProtectSystem=strict). The TLS key tlsa-updater installs is 0640 root:root;
|
# ProtectSystem=strict). The TLS key tlsa-updater installs is 0640 root:root;
|
||||||
# regrant it to the stalwart group after every cert sync so stalwart can serve TLS.
|
# regrant it to the stalwart group after every cert sync so stalwart can serve TLS.
|
||||||
systemd.services.stalwart = {
|
systemd.services.stalwart = {
|
||||||
after = ["tlsa-update.service" "stalwart-admin-secret.service"];
|
after = ["tlsa-update.service" "stalwart-admin-secret.service" "stalwart-dkim.service"];
|
||||||
requires = ["tlsa-update.service" "stalwart-admin-secret.service"];
|
requires = ["tlsa-update.service" "stalwart-admin-secret.service" "stalwart-dkim.service"];
|
||||||
};
|
};
|
||||||
|
|
||||||
# Make the management CLI available for account creation and maildir import
|
# Make the management CLI available for account creation and maildir import
|
||||||
@@ -142,6 +244,17 @@ in {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Install the outbound DKIM key into the stalwart data dir before the service starts.
|
||||||
|
systemd.services.stalwart-dkim = {
|
||||||
|
description = "Install Stalwart outbound DKIM key";
|
||||||
|
wantedBy = ["multi-user.target"];
|
||||||
|
before = ["stalwart.service"];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
ExecStart = "${writeDkimKey}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
# LoadCredential: expose the materialized hash to stalwart only at
|
# LoadCredential: expose the materialized hash to stalwart only at
|
||||||
# /run/credentials/stalwart.service/stalwart-admin (see `credentials` option
|
# /run/credentials/stalwart.service/stalwart-admin (see `credentials` option
|
||||||
# in the upstream module; the value is the source path on disk).
|
# in the upstream module; the value is the source path on disk).
|
||||||
@@ -149,7 +262,12 @@ in {
|
|||||||
|
|
||||||
systemd.services.stalwart-cert-perm = {
|
systemd.services.stalwart-cert-perm = {
|
||||||
description = "Grant stalwart read access to its TLS private key";
|
description = "Grant stalwart read access to its TLS private key";
|
||||||
after = ["tlsa-update.service" "stalwart.service"];
|
# Belt-and-suspenders: tlsa-update already chgrps the key after every sync;
|
||||||
|
# this guarantees the group grant also exists at first boot, before stalwart
|
||||||
|
# starts (previously ordered after stalwart, so a fresh sync could leave a
|
||||||
|
# root:root key and webadmin reload would fail with EACCES).
|
||||||
|
after = ["tlsa-update.service"];
|
||||||
|
before = ["stalwart.service"];
|
||||||
partOf = ["tlsa-update.service"];
|
partOf = ["tlsa-update.service"];
|
||||||
wantedBy = ["multi-user.target"];
|
wantedBy = ["multi-user.target"];
|
||||||
path = [pkgs.coreutils];
|
path = [pkgs.coreutils];
|
||||||
@@ -172,7 +290,11 @@ in {
|
|||||||
"/var/lib/bulwark:/app/data:Z"
|
"/var/lib/bulwark:/app/data:Z"
|
||||||
];
|
];
|
||||||
environment = {
|
environment = {
|
||||||
JMAP_SERVER_URL = "http://127.0.0.1:8080";
|
# JMAP_SERVER_URL is the public origin (the browser uses it verbatim for
|
||||||
|
# /.well-known/jmap + the session apiUrl). HOSTNAME stays 127.0.0.1 so
|
||||||
|
# Next.js binds to loopback (caddy reverse_proxy's 127.0.0.1:3002); binding
|
||||||
|
# to the public hostname made the container unreachable for caddy (502).
|
||||||
|
JMAP_SERVER_URL = "https://mail.severijnse.eu";
|
||||||
HOSTNAME = "127.0.0.1";
|
HOSTNAME = "127.0.0.1";
|
||||||
PORT = "3002";
|
PORT = "3002";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
{pkgs, ...}: let
|
{pkgs, ...}: let
|
||||||
# Caddy's canonical certificate storage (XDG data dir). Renewals land here,
|
# Caddy's canonical certificate storage (XDG data dir). Renewals land here,
|
||||||
# owned caddy:caddy 0600 — the mail server's non-root Postfix/Dovecot cannot
|
# owned caddy:caddy 0600 — stalwart cannot read it directly, so we copy it
|
||||||
# read it directly, so we copy it into a world-readable distribution dir.
|
# into a distribution dir stalwart's service (user "stalwart") can reach.
|
||||||
caddyCertDir = "/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.severijnse.eu";
|
caddyCertDir = "/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.severijnse.eu";
|
||||||
# World-readable distribution dir mounted (RO) into the mail server container.
|
# Distribution dir read by stalwart (cert 0644, private key regranted to the
|
||||||
|
# "stalwart" group by systemd.services.stalwart-cert-perm).
|
||||||
distCertDir = "/var/lib/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.severijnse.eu";
|
distCertDir = "/var/lib/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.severijnse.eu";
|
||||||
zoneFile = "/var/lib/coredns/zones/severijnse.eu.db";
|
zoneFile = "/var/lib/coredns/zones/severijnse.eu.db";
|
||||||
|
# Records the SPKI hash applied at the last restart of stalwart, so cert
|
||||||
|
# renewals trigger exactly one restart and unchanged certs never do.
|
||||||
|
stateFile = "/var/lib/tlsa-update/.last-spki";
|
||||||
|
|
||||||
syncScript = pkgs.writeShellScript "tlsa-update" ''
|
syncScript = pkgs.writeShellScript "tlsa-update" ''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -22,13 +26,17 @@
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 1) Propagate Caddy's renewed certificate into the distribution dir the
|
# 1) Propagate Caddy's renewed certificate into the distribution dir stalwart
|
||||||
# mail server mounts. Caddy stores certs 0600 caddy:caddy. The cert is
|
# reads. Caddy stores certs 0600 caddy:caddy. The cert is world-readable;
|
||||||
# world-readable (Postfix/Dovecot read it as root before dropping
|
# the private key is restricted to root (0640) and stalwart-cert-perm
|
||||||
# privileges); the private key is restricted to root (0640) so it is not
|
# regrants it to the "stalwart" group so it is not exposed to other local
|
||||||
# exposed to other local users.
|
# users.
|
||||||
install -D -m 0644 "$SRC_CERT" "$DST_CERT"
|
install -D -m 0644 "$SRC_CERT" "$DST_CERT"
|
||||||
install -D -m 0640 "$SRC_KEY" "$DST_KEY"
|
install -D -m 0640 "$SRC_KEY" "$DST_KEY"
|
||||||
|
# Stalwart reads the key as user "stalwart" via %{file:...}%; regrant the
|
||||||
|
# group immediately so every sync leaves it readable (0640 root:stalwart)
|
||||||
|
# and webadmin config reload never fails with EACCES.
|
||||||
|
chgrp stalwart "$DST_KEY"
|
||||||
|
|
||||||
# 2) TLSA 3 1 1 = SHA-256 of the certificate's SubjectPublicKeyInfo (SPKI),
|
# 2) TLSA 3 1 1 = SHA-256 of the certificate's SubjectPublicKeyInfo (SPKI),
|
||||||
# NOT the whole certificate. Matching type 1 = SHA-256 of the SPKI DER.
|
# NOT the whole certificate. Matching type 1 = SHA-256 of the SPKI DER.
|
||||||
@@ -47,8 +55,24 @@
|
|||||||
|
|
||||||
# 4) Reload services so the changes take effect immediately.
|
# 4) Reload services so the changes take effect immediately.
|
||||||
systemctl reload coredns.service || true
|
systemctl reload coredns.service || true
|
||||||
podman exec mailserver postfix reload || true
|
# Stalwart reads its TLS certs ($certDir) via %{file:...}% placeholders at
|
||||||
podman exec mailserver dovecot reload || true
|
# startup only; there is no signal-based reload (management API reload needs
|
||||||
|
# admin credentials we must not store). Restart it, but only when the cert
|
||||||
|
# actually changed (SPKI hash differs from the last applied run), so the
|
||||||
|
# daily timer and inotify events for unchanged certs do not drop connections.
|
||||||
|
# NOTE: stalwart.service `requires` this unit, so the restart must be issued
|
||||||
|
# with --no-block (async): a synchronous restart waits for stalwart to come
|
||||||
|
# back up, which in turn waits for this unit to finish — a deadlock. The state
|
||||||
|
# file is updated BEFORE the restart so the tlsa-update run that stalwart's
|
||||||
|
# required-activation re-triggers sees a matching hash and exits immediately.
|
||||||
|
if [ "$(cat "${stateFile}" 2>/dev/null || true)" != "$HEX" ]; then
|
||||||
|
printf '%s\n' "$HEX" > "${stateFile}"
|
||||||
|
# Restarting stalwart re-runs its Requires=tlsa-update dependency; guard
|
||||||
|
# with is-active so a boot-time run never races stalwart's initial start.
|
||||||
|
if systemctl is-active --quiet stalwart.service 2>/dev/null; then
|
||||||
|
systemctl --no-block restart stalwart.service || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
echo "tlsa-update: TLSA set to $HEX"
|
echo "tlsa-update: TLSA set to $HEX"
|
||||||
'';
|
'';
|
||||||
@@ -57,6 +81,7 @@ in {
|
|||||||
# Ensure the distribution dir exists (Caddy does not write here).
|
# Ensure the distribution dir exists (Caddy does not write here).
|
||||||
tmpfiles.rules = [
|
tmpfiles.rules = [
|
||||||
"d ${distCertDir} 0755 root root - -"
|
"d ${distCertDir} 0755 root root - -"
|
||||||
|
"d /var/lib/tlsa-update 0755 root root - -"
|
||||||
];
|
];
|
||||||
|
|
||||||
services.tlsa-update = {
|
services.tlsa-update = {
|
||||||
@@ -64,7 +89,7 @@ in {
|
|||||||
after = ["caddy.service" "coredns.service"];
|
after = ["caddy.service" "coredns.service"];
|
||||||
partOf = ["coredns.service"];
|
partOf = ["coredns.service"];
|
||||||
wantedBy = ["multi-user.target"];
|
wantedBy = ["multi-user.target"];
|
||||||
path = with pkgs; [openssl coreutils gnused podman systemd];
|
path = with pkgs; [openssl coreutils gnused systemd];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
ExecStart = "${syncScript}";
|
ExecStart = "${syncScript}";
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
--- a/src/lib/catalog.ts
|
|
||||||
+++ b/src/lib/catalog.ts
|
|
||||||
@@ -1,3 +1,8 @@
|
|
||||||
import { prisma } from "@/lib/db";
|
|
||||||
+
|
|
||||||
+// During `next build`, Next evaluates generateStaticParams/generateMetadata for
|
|
||||||
+// each route, which calls these DB readers. No database exists in the sandboxed
|
|
||||||
+// Nix build, so short-circuit them here; the live site fetches real rows/request.
|
|
||||||
+const BUILD = process.env.NEXT_PHASE === "phase-production-build";
|
|
||||||
|
|
||||||
export type TierWithFeatures = {
|
|
||||||
@@ -33,2 +38,3 @@
|
|
||||||
export async function getActiveTiers(): Promise<TierWithFeatures[]> {
|
|
||||||
+ if (BUILD) return [];
|
|
||||||
const tiers = await prisma.tier.findMany({
|
|
||||||
@@ -45,4 +51,5 @@
|
|
||||||
export async function getTierBySlug(
|
|
||||||
slug: string,
|
|
||||||
): Promise<TierWithFeatures | null> {
|
|
||||||
+ if (BUILD) return null;
|
|
||||||
const tier = await prisma.tier.findUnique({
|
|
||||||
@@ -58,2 +65,3 @@
|
|
||||||
export async function getDocPages() {
|
|
||||||
+ if (BUILD) return [];
|
|
||||||
return prisma.docPage.findMany({
|
|
||||||
@@ -65,2 +73,3 @@
|
|
||||||
export async function getDocBySlug(slug: string) {
|
|
||||||
+ if (BUILD) return null;
|
|
||||||
return prisma.docPage.findUnique({ where: { slug } });
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
--- a/src/app/api/checkout/route.ts
|
||||||
|
+++ b/src/app/api/checkout/route.ts
|
||||||
|
@@ -7,6 +7,8 @@
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
+const BYPASS_PAYMENTS = process.env.BYPASS_PAYMENTS === "true";
|
||||||
|
+
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
// Each checkout hits the payment provider, so cap order creation per user
|
||||||
|
// and per IP to prevent order spam and provider-API abuse.
|
||||||
|
@@ -86,6 +88,20 @@
|
||||||
|
const paid = await prisma.order.findUnique({
|
||||||
|
where: { id: order.id },
|
||||||
|
include: { license: true },
|
||||||
|
+ });
|
||||||
|
+ return NextResponse.json({
|
||||||
|
+ ok: true,
|
||||||
|
+ orderId: order.id,
|
||||||
|
+ licenseKey: paid?.license?.key ?? null,
|
||||||
|
+ tier: tier.slug,
|
||||||
|
+ });
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if (BYPASS_PAYMENTS) {
|
||||||
|
+ await markOrderPaid({ orderId: order.id, providerRef: "bypass-test" });
|
||||||
|
+ const paid = await prisma.order.findUnique({
|
||||||
|
+ where: { id: order.id },
|
||||||
|
+ include: { license: true },
|
||||||
|
});
|
||||||
|
return NextResponse.json({
|
||||||
|
ok: true,
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
--- a/src/app/layout.tsx
|
|
||||||
+++ b/src/app/layout.tsx
|
|
||||||
@@ -1,19 +1,14 @@
|
|
||||||
import type { Metadata } from "next";
|
|
||||||
-import { Pixelify_Sans } from "next/font/google";
|
|
||||||
import "./globals.css";
|
|
||||||
import { getCurrentUser } from "@/lib/auth";
|
|
||||||
import { getAppUrl } from "@/lib/env";
|
|
||||||
import { Providers } from "@/components/providers";
|
|
||||||
import { AuthProvider } from "@/contexts/auth-context";
|
|
||||||
import { MatrixEffects } from "@/components/matrix-effects";
|
|
||||||
import { SiteHeader } from "@/components/site-header";
|
|
||||||
import { SiteFooter } from "@/components/site-footer";
|
|
||||||
-
|
|
||||||
-const pixelify = Pixelify_Sans({
|
|
||||||
- subsets: ["latin"],
|
|
||||||
- weight: ["400", "500", "600", "700"],
|
|
||||||
- variable: "--font-pixelify",
|
|
||||||
- display: "swap",
|
|
||||||
-});
|
|
||||||
-
|
|
||||||
+// Built fully dynamic so the sandboxed Nix build needs no database and no
|
|
||||||
+// external font download; also correct for a DB-driven storefront.
|
|
||||||
+export const dynamic = "force-dynamic";
|
|
||||||
+
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
@@ -50,2 +45,2 @@
|
|
||||||
- <html lang="en" className={`${pixelify.variable} h-full`} suppressHydrationWarning>
|
|
||||||
+ <html lang="en" className="h-full" suppressHydrationWarning>
|
|
||||||
<body className="min-h-full flex flex-col bg-background text-foreground">
|
|
||||||
@@ -4,19 +4,26 @@
|
|||||||
unstablePkgs,
|
unstablePkgs,
|
||||||
...
|
...
|
||||||
}: let
|
}: let
|
||||||
rev = "6225e0fca02c02544341c92ecdc9634a9a15f45c";
|
rev = "6e351ddc732421eb775eb9843ccd2389d6525edd";
|
||||||
src = pkgs.fetchgit {
|
apiRev = "390878d126185b21a14479f85c70134a155c61c6";
|
||||||
url = "https://git.severijnse.eu/jory/virtualcam-website.git";
|
|
||||||
|
# Private repositories are fetched over SSH (port 2222). nix-daemon runs as
|
||||||
|
# root and uses /root/.ssh (identity materialized by the git-ssh-key unit),
|
||||||
|
# so the source ends up in the store without any Nix-native credentials.
|
||||||
|
src = builtins.fetchGit {
|
||||||
|
url = "ssh://git@git.severijnse.eu:2222/jory/virtualcam-website.git";
|
||||||
rev = rev;
|
rev = rev;
|
||||||
sha256 = "17ihw2bhsp89nczljz6xzwlvxyzgsdn62ywmchp5blzd6jkxd3w0";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
# Patch the app to be fully dynamic and drop the Google-font download so the
|
|
||||||
# sandboxed Nix build needs neither a database nor network access.
|
|
||||||
srcPatched = pkgs.applyPatches {
|
srcPatched = pkgs.applyPatches {
|
||||||
name = "virtualcam-website-patched";
|
name = "virtualcam-website-patched";
|
||||||
src = src;
|
src = src;
|
||||||
patches = [./virtualcam-layout.patch ./virtualcam-build.patch];
|
patches = [./virtualcam-checkout.patch];
|
||||||
|
};
|
||||||
|
|
||||||
|
apiSrc = builtins.fetchGit {
|
||||||
|
url = "ssh://git@git.severijnse.eu:2222/jory/virtualcam-api.git";
|
||||||
|
rev = apiRev;
|
||||||
};
|
};
|
||||||
|
|
||||||
# Build the Next.js app entirely in Nix (offline npm deps from the lockfile).
|
# Build the Next.js app entirely in Nix (offline npm deps from the lockfile).
|
||||||
@@ -24,7 +31,7 @@
|
|||||||
pname = "virtualcam-website";
|
pname = "virtualcam-website";
|
||||||
version = "0.1.0";
|
version = "0.1.0";
|
||||||
src = srcPatched;
|
src = srcPatched;
|
||||||
npmDepsHash = "sha256-52ugs4ydwxGXLIhF/6P8uO400x3BRYk4NUt2Swob3cY=";
|
npmDepsHash = "sha256-GodWQKtOtsLOjjiwzxun+wTPhtvjR2uCV91n+wiZHw4=";
|
||||||
nodejs = unstablePkgs.nodejs;
|
nodejs = unstablePkgs.nodejs;
|
||||||
|
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
@@ -43,6 +50,7 @@
|
|||||||
'';
|
'';
|
||||||
|
|
||||||
APP_URL = "https://virtualcam.severijnse.eu";
|
APP_URL = "https://virtualcam.severijnse.eu";
|
||||||
|
GOOGLE_CLIENT_ID = "754775011707-c699m092tv3icmovhk5qa106v3q6eh7c.apps.googleusercontent.com";
|
||||||
# Only used to satisfy prisma generate / next build metadata resolution.
|
# Only used to satisfy prisma generate / next build metadata resolution.
|
||||||
DATABASE_URL = "postgresql://virtualcam@localhost/virtualcam?host=/run/postgresql&schema=public";
|
DATABASE_URL = "postgresql://virtualcam@localhost/virtualcam?host=/run/postgresql&schema=public";
|
||||||
# Use the nixpkgs-bundled Prisma engine so the sandboxed offline build does
|
# Use the nixpkgs-bundled Prisma engine so the sandboxed offline build does
|
||||||
@@ -52,7 +60,61 @@
|
|||||||
CI = "true";
|
CI = "true";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# License validation / admin API (Go/Fiber), serving on host port 3004.
|
||||||
|
# go.mod demands go 1.26.5 but the pinned nixpkgs only has 1.26.4; the code
|
||||||
|
# uses nothing newer, so the directive is relaxed to match the toolchain.
|
||||||
|
api = unstablePkgs.buildGoModule {
|
||||||
|
pname = "virtualcam-api";
|
||||||
|
version = "0.1.0";
|
||||||
|
src = apiSrc;
|
||||||
|
vendorHash = "sha256-uvHClXHw9ycoIf6qBZmV2O3CSyIxCgnSPCSALM07qg8=";
|
||||||
|
go = unstablePkgs.go_1_26;
|
||||||
|
postPatch = ''
|
||||||
|
sed -i 's/^go 1\.26\.5$/go 1.26.4/' go.mod
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
dbUrl = "postgresql://virtualcam@localhost/virtualcam?host=/run/postgresql&schema=public";
|
dbUrl = "postgresql://virtualcam@localhost/virtualcam?host=/run/postgresql&schema=public";
|
||||||
|
# lib/pq parses the DSN differently from node-postgres: a hostname in the URL
|
||||||
|
# authority wins over a `host=` query param (so it would go over TCP and fail
|
||||||
|
# password auth), and lib/pq rejects unknown URL params like `schema`. Use a
|
||||||
|
# keyword DSN: unix-socket + peer auth + no SSL, matching the OS user.
|
||||||
|
apiDbUrl = "host=/run/postgresql user=virtualcam dbname=virtualcam sslmode=disable";
|
||||||
|
|
||||||
|
# Root-only runtime environment file holding the secrets both services need.
|
||||||
|
secretsFile = ../../secrets/secrets.yaml;
|
||||||
|
envFile = "/var/lib/virtualcam/environment";
|
||||||
|
|
||||||
|
# Materialize the license signing key and admin token from sops into a
|
||||||
|
# root-only file (0600). systemd reads environmentFiles before dropping
|
||||||
|
# privileges, so the service users never need to read it themselves.
|
||||||
|
writeSecrets = pkgs.writeShellScript "virtualcam-write-secrets" ''
|
||||||
|
set -euo pipefail
|
||||||
|
install -d -o virtualcam -g virtualcam -m 0750 /var/lib/virtualcam
|
||||||
|
: > "${envFile}"
|
||||||
|
chmod 0600 "${envFile}"
|
||||||
|
${pkgs.sops}/bin/sops --decrypt --input-type yaml --output-type yaml ${secretsFile} |
|
||||||
|
${pkgs.gnused}/bin/sed -nE \
|
||||||
|
's/^virtualcam_license_signing_key: (.*)/LICENSE_SIGNING_KEY=\1/p; s/^virtualcam_admin_token: (.*)/ADMIN_TOKEN=\1/p; s/^virtualcam_smtp_pass: (.*)/SMTP_PASS=\1/p; s/^virtualcam_google_OAuth_secret: (.*)/GOOGLE_CLIENT_SECRET=\1/p; s/^shkeeper_api_key: (.*)/SHKEEPER_API_KEY=\1/p' \
|
||||||
|
>> "${envFile}"
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Materialize the nix-daemon's SSH identity so private-repo fetches keep
|
||||||
|
# working after the one-off bootstrap copy in /root/.ssh.
|
||||||
|
writeGitSshKey = pkgs.writeShellScript "git-ssh-key" ''
|
||||||
|
set -euo pipefail
|
||||||
|
install -d -m 0700 /root/.ssh
|
||||||
|
${pkgs.sops}/bin/sops --decrypt --extract '["git_ssh_key_b64"]' \
|
||||||
|
--input-type yaml --output-type yaml ${secretsFile} |
|
||||||
|
${pkgs.coreutils}/bin/base64 -d > /root/.ssh/id_ed25519
|
||||||
|
chmod 0600 /root/.ssh/id_ed25519
|
||||||
|
cat > /root/.ssh/known_hosts <<'EOF'
|
||||||
|
git.severijnse.eu ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAzIVo9Jdp8kwnWmTn26Fj68baJjwDphYw/0HTH5BzYY
|
||||||
|
[git.severijnse.eu]:2222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDGcPKlvdxApYRCZuQxrljEdETrxAu0JsEJ+28zFaMfA3P69J9vEkqqAZZ0Uq3rtE8ZqPBxeDh7JYn2XChb+QVGjOHVWMCWqcdD43ws26eY91aHGYnO2kW+sKSxIh803tdifIp2q8VRnaHVXcLHLx2rmwmvqIfMLnEEShxAt4Q846DianBoA7S8heoBvMLi8AAO09sVEgMPWm8ULkoRq0poHslsKkdYKlWzecP85tPo/tlCEjQq5cC5+xidH9TS4ILxVIXGUfErZzbWZq5LtaRmqiU5U1+zzdb83oJvHFtTZ8wHwZX+VE4jZzKw7wdFPk+dJZp/3ZAVva6fLSjlpyyDFNWMcf2/18Gs6YioD1n2fr1dXruZiXNMnI19Dzyp+8yWIPbAeWyVx3BoCrcjIApEDMLMo7ut0l6RfNy80hYpY91VBrpsjSRLB4uCVDsqhTLPTrFUlu+9YY9lSHEUVLRylOLK54TbY089hbHOB/UdpyCiXpI0ds3ww1UmnDQSFjwNknc+wTXRvzEz+VsX2nAzfO+5NWq2jHNn4wH6GuAinAxSVg1A7Ub9VSmPelRiTzK6ZnplPm762MxcPFmo+y/5cszumUwypcyTS8MJIC57S/kabfsnjdt97K+9eT9X2LDZ2uIQgDS8qEgvbnvfXVmMoqakgBLYG7Embqc8BDzNsQ==
|
||||||
|
EOF
|
||||||
|
chmod 0600 /root/.ssh/known_hosts
|
||||||
|
test -f /root/.ssh/id_ed25519.pub || ${pkgs.openssh}/bin/ssh-keygen -y -f /root/.ssh/id_ed25519 > /root/.ssh/id_ed25519.pub
|
||||||
|
'';
|
||||||
|
|
||||||
# The repo's seed uses tsx (a devDependency buildNpmPackage drops) plus the
|
# The repo's seed uses tsx (a devDependency buildNpmPackage drops) plus the
|
||||||
# "@/..." path alias. nixpkgs ships tsx, which honours tsconfig paths, so we
|
# "@/..." path alias. nixpkgs ships tsx, which honours tsconfig paths, so we
|
||||||
@@ -68,7 +130,7 @@
|
|||||||
|
|
||||||
# One shared PostgreSQL server (existing system postgres). Each service gets
|
# One shared PostgreSQL server (existing system postgres). Each service gets
|
||||||
# its own database + role. virtualcam authenticates over the Unix socket via
|
# its own database + role. virtualcam authenticates over the Unix socket via
|
||||||
# peer auth: the systemd service runs as OS user `virtualcam`, which matches
|
# peer auth: the systemd services run as OS user `virtualcam`, which matches
|
||||||
# the database role `virtualcam`, so no password is stored anywhere.
|
# the database role `virtualcam`, so no password is stored anywhere.
|
||||||
migrate = pkgs.writeShellScript "virtualcam-migrate" ''
|
migrate = pkgs.writeShellScript "virtualcam-migrate" ''
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -83,7 +145,7 @@ in {
|
|||||||
users.virtualcam = {
|
users.virtualcam = {
|
||||||
isSystemUser = true;
|
isSystemUser = true;
|
||||||
group = "virtualcam";
|
group = "virtualcam";
|
||||||
description = "virtualcamera website service user";
|
description = "virtualcamera services user";
|
||||||
};
|
};
|
||||||
groups.virtualcam = {};
|
groups.virtualcam = {};
|
||||||
};
|
};
|
||||||
@@ -100,6 +162,30 @@ in {
|
|||||||
|
|
||||||
systemd = {
|
systemd = {
|
||||||
services = {
|
services = {
|
||||||
|
git-ssh-key = {
|
||||||
|
description = "Materialize nix-daemon git SSH key from sops";
|
||||||
|
wantedBy = ["multi-user.target"];
|
||||||
|
environment.SOPS_AGE_KEY_FILE = "/etc/age/keys.txt";
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
Environment = ["HOME=/root"];
|
||||||
|
ExecStart = "${writeGitSshKey}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
virtualcam-secrets = {
|
||||||
|
description = "Materialize virtualcam secrets from sops";
|
||||||
|
wantedBy = ["multi-user.target"];
|
||||||
|
# The age key lives in /etc/age/keys.txt; the service must know where it
|
||||||
|
# is and needs a HOME for age to report its user config directory.
|
||||||
|
environment.SOPS_AGE_KEY_FILE = "/etc/age/keys.txt";
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
Environment = ["HOME=/root"];
|
||||||
|
ExecStart = "${writeSecrets}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
virtualcam-migrate = {
|
virtualcam-migrate = {
|
||||||
description = "Virtualcam Prisma migrations";
|
description = "Virtualcam Prisma migrations";
|
||||||
after = ["postgresql.service"];
|
after = ["postgresql.service"];
|
||||||
@@ -135,14 +221,15 @@ in {
|
|||||||
|
|
||||||
virtualcam = {
|
virtualcam = {
|
||||||
description = "Virtualcamera website (Next.js)";
|
description = "Virtualcamera website (Next.js)";
|
||||||
after = ["postgresql.service" "virtualcam-migrate.service" "virtualcam-seed.service"];
|
after = ["postgresql.service" "podman-shkeeper.service" "virtualcam-migrate.service" "virtualcam-seed.service" "virtualcam-secrets.service"];
|
||||||
requires = ["postgresql.service" "virtualcam-migrate.service" "virtualcam-seed.service"];
|
requires = ["postgresql.service" "podman-shkeeper.service" "virtualcam-migrate.service" "virtualcam-seed.service" "virtualcam-secrets.service"];
|
||||||
wantedBy = ["multi-user.target"];
|
wantedBy = ["multi-user.target"];
|
||||||
path = [unstablePkgs.nodejs];
|
path = [unstablePkgs.nodejs];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
User = "virtualcam";
|
User = "virtualcam";
|
||||||
Group = "virtualcam";
|
Group = "virtualcam";
|
||||||
WorkingDirectory = "${app}";
|
WorkingDirectory = "${app}";
|
||||||
|
EnvironmentFile = [envFile];
|
||||||
ExecStart = "${app}/node_modules/.bin/next start -p 3001 -H 127.0.0.1";
|
ExecStart = "${app}/node_modules/.bin/next start -p 3001 -H 127.0.0.1";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
RestartSec = 5;
|
RestartSec = 5;
|
||||||
@@ -151,21 +238,50 @@ in {
|
|||||||
Environment = [
|
Environment = [
|
||||||
"DATABASE_URL=${dbUrl}"
|
"DATABASE_URL=${dbUrl}"
|
||||||
"APP_URL=https://virtualcam.severijnse.eu"
|
"APP_URL=https://virtualcam.severijnse.eu"
|
||||||
|
"GOOGLE_CLIENT_ID=754775011707-c699m092tv3icmovhk5qa106v3q6eh7c.apps.googleusercontent.com"
|
||||||
"PAYMENTS_MODE=shkeeper"
|
"PAYMENTS_MODE=shkeeper"
|
||||||
|
"BYPASS_PAYMENTS=false"
|
||||||
|
"SHKEEPER_URL=https://pay.severijnse.eu"
|
||||||
|
"SHKEEPER_CRYPTO=BTC"
|
||||||
|
"SHKEEPER_FIAT=USD"
|
||||||
"ADMIN_EMAILS=jory@severijnse.eu"
|
"ADMIN_EMAILS=jory@severijnse.eu"
|
||||||
"SMTP_HOST=localhost"
|
"SMTP_HOST=mail.severijnse.eu"
|
||||||
"SMTP_PORT=587"
|
"SMTP_PORT=587"
|
||||||
"SMTP_USER=jory@severijnse.eu"
|
"SMTP_USER=no-reply@severijnse.eu"
|
||||||
"SMTP_FROM=noreply@severijnse.eu"
|
"SMTP_FROM=no-reply@severijnse.eu"
|
||||||
"NODE_ENV=production"
|
"NODE_ENV=production"
|
||||||
"NEXT_TELEMETRY_DISABLED=1"
|
"NEXT_TELEMETRY_DISABLED=1"
|
||||||
"HOME=/var/lib/virtualcam"
|
"HOME=/var/lib/virtualcam"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
virtualcam-api = {
|
||||||
|
description = "Virtualcamera license API (Go/Fiber)";
|
||||||
|
after = ["postgresql.service" "podman-shkeeper.service" "virtualcam-migrate.service" "virtualcam-secrets.service"];
|
||||||
|
requires = ["postgresql.service" "podman-shkeeper.service" "virtualcam-migrate.service" "virtualcam-secrets.service"];
|
||||||
|
wantedBy = ["multi-user.target"];
|
||||||
|
serviceConfig = {
|
||||||
|
User = "virtualcam";
|
||||||
|
Group = "virtualcam";
|
||||||
|
WorkingDirectory = "${api}";
|
||||||
|
EnvironmentFile = [envFile];
|
||||||
|
ExecStart = "${api}/bin/virtualcam-api";
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = 5;
|
||||||
|
StateDirectory = "virtualcam";
|
||||||
|
StateDirectoryMode = "0750";
|
||||||
|
Environment = [
|
||||||
|
"PORT=3004"
|
||||||
|
"DATABASE_URL=${apiDbUrl}"
|
||||||
|
"CORS_ORIGINS=https://virtualcam.severijnse.eu"
|
||||||
|
"HOME=/var/lib/virtualcam"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Serve behind Caddy on 127.0.0.1:3000 (virtualHost wired in caddy.nix).
|
# Served behind Caddy (virtualHosts wired in caddy.nix).
|
||||||
networking.firewall.allowedTCPPorts = [];
|
networking.firewall.allowedTCPPorts = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,40 @@
|
|||||||
{...}: {
|
{pkgs, ...}: let
|
||||||
|
# sops-encrypted secrets (single file holds all service secrets), same as backup.nix.
|
||||||
|
secretsFile = ../../secrets/secrets.yaml;
|
||||||
|
# Root-only env file wg-easy reads the admin password from (0600 root).
|
||||||
|
wgEnvFile = "/var/lib/wg-easy/environment";
|
||||||
|
|
||||||
|
# Materialize the wg-easy admin password from sops into a root-only env file,
|
||||||
|
# so the secret never lands in the Nix store.
|
||||||
|
writeSecrets = pkgs.writeShellScript "wg-easy-write-secrets" ''
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p "$(dirname ${wgEnvFile})"
|
||||||
|
PASSWORD="$(${pkgs.sops}/bin/sops \
|
||||||
|
--decrypt --extract '["wg_admin_password"]' \
|
||||||
|
--input-type yaml --output-type yaml ${secretsFile} | tr -d '\n')"
|
||||||
|
printf 'INIT_PASSWORD=%s\n' "$PASSWORD" > "${wgEnvFile}"
|
||||||
|
chmod 0600 "${wgEnvFile}"
|
||||||
|
'';
|
||||||
|
in {
|
||||||
|
boot.kernelModules = ["ip6table_nat"];
|
||||||
|
|
||||||
virtualisation.oci-containers.containers.wg-easy = {
|
virtualisation.oci-containers.containers.wg-easy = {
|
||||||
image = "ghcr.io/wg-easy/wg-easy:latest";
|
image = "ghcr.io/wg-easy/wg-easy:15";
|
||||||
autoStart = true;
|
autoStart = true;
|
||||||
volumes = [
|
volumes = [
|
||||||
"/home/admin/config:/etc/wireguard:Z"
|
"/home/admin/config:/etc/wireguard:Z"
|
||||||
];
|
];
|
||||||
|
environmentFiles = [wgEnvFile];
|
||||||
environment = {
|
environment = {
|
||||||
WG_HOST = "severijnse.eu";
|
INSECURE = "true";
|
||||||
PASSWORD_HASH = "$2a$14$f6l9jto2Uwn9hNudNo7cHeq08M8UDYzrUiOofWSH522QDRhgTlddC";
|
INIT_ENABLED = "true";
|
||||||
WG_DEFAULT_ADDRESS = "10.8.0.x";
|
INIT_USERNAME = "admin";
|
||||||
WG_DEFAULT_DNS = "1.1.1.1";
|
INIT_HOST = "severijnse.eu";
|
||||||
WG_PORT = "51820";
|
INIT_PORT = "51820";
|
||||||
|
INIT_DNS = "1.1.1.1,2606:4700:4700::1111";
|
||||||
|
INIT_IPV4_CIDR = "10.8.0.0/24";
|
||||||
|
INIT_IPV6_CIDR = "fd10:8::/64";
|
||||||
|
INIT_ALLOWED_IPS = "0.0.0.0/0, ::/0";
|
||||||
};
|
};
|
||||||
extraOptions = [
|
extraOptions = [
|
||||||
"--cap-add=NET_ADMIN"
|
"--cap-add=NET_ADMIN"
|
||||||
@@ -19,4 +43,38 @@
|
|||||||
"--network=host"
|
"--network=host"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Materialize the wg-easy admin password from sops before the container starts.
|
||||||
|
systemd.services.wg-easy-secrets = {
|
||||||
|
description = "Materialize wg-easy admin password from sops";
|
||||||
|
wantedBy = ["multi-user.target"];
|
||||||
|
# The age key lives in /etc/age/keys.txt; the service must know where it is
|
||||||
|
# and needs a HOME for age to report its user config directory.
|
||||||
|
environment.SOPS_AGE_KEY_FILE = "/etc/age/keys.txt";
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
Environment = ["HOME=/root"];
|
||||||
|
ExecStart = "${writeSecrets}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services."podman-wg-easy" = {
|
||||||
|
requires = ["wg-easy-secrets.service"];
|
||||||
|
after = ["wg-easy-secrets.service"];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.wg-nat66 = {
|
||||||
|
description = "NAT66 for WireGuard IPv6";
|
||||||
|
after = ["network.target" "podman-wg-easy.service"];
|
||||||
|
wants = ["podman-wg-easy.service"];
|
||||||
|
wantedBy = ["multi-user.target"];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
${pkgs.iptables}/bin/ip6tables -t nat -C POSTROUTING -s fd10:8::/64 -o enp1s0 -j MASQUERADE 2>/dev/null || \
|
||||||
|
${pkgs.iptables}/bin/ip6tables -t nat -A POSTROUTING -s fd10:8::/64 -o enp1s0 -j MASQUERADE
|
||||||
|
'';
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
{pkgs, ...}: {
|
|
||||||
environment.systemPackages = [
|
|
||||||
(pkgs.writeShellScriptBin "opencode" ''
|
|
||||||
exec /home/admin/.local/bin/opencode "$@"
|
|
||||||
'')
|
|
||||||
];
|
|
||||||
|
|
||||||
programs.fish.shellInit = ''
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
|
shkeeper_api_key: ENC[AES256_GCM,data:kUymtefYJOllK5cxKHhD3HnXO/uo1w==,iv:OH9ZaQJZsFfo00BIl5MlRw1XBzHUmFi1FW54/J6O/8Q=,tag:HQJyuEoz5fsDi7iMq3PEzw==,type:str]
|
||||||
restic_password: ENC[AES256_GCM,data:OHQlxUpNnTqMQm6A/o3ID/3F91NpVKOrsFYdLqrtI4vp+6TWHR8=,iv:bBd+gfi78lgTaTv0QUWYGQWPsurrzx90VvOzA2d2euA=,tag:qsHSdAHx1auwdZCgn2O5Qw==,type:str]
|
restic_password: ENC[AES256_GCM,data:OHQlxUpNnTqMQm6A/o3ID/3F91NpVKOrsFYdLqrtI4vp+6TWHR8=,iv:bBd+gfi78lgTaTv0QUWYGQWPsurrzx90VvOzA2d2euA=,tag:qsHSdAHx1auwdZCgn2O5Qw==,type:str]
|
||||||
b2_key_id: ENC[AES256_GCM,data:4B9rvg06baH6aNiT,iv:Qk1ToF3lMYLTrZdzpfaoGVzdiKYs492w9fzn4/TbNfQ=,tag:djxfqwgNZmRGqvteTX9G3w==,type:str]
|
b2_key_id: ENC[AES256_GCM,data:4B9rvg06baH6aNiT,iv:Qk1ToF3lMYLTrZdzpfaoGVzdiKYs492w9fzn4/TbNfQ=,tag:djxfqwgNZmRGqvteTX9G3w==,type:str]
|
||||||
b2_application_key: ENC[AES256_GCM,data:xGAbBqx+6ErK7sy3FR0yza6mJU5oc5uQQGcwKtcPp1Ef4XEVd8do/wN7,iv:CojcoykDRBfvN8yqMMrPJq9mtAwxyswqXXVBKPupxDs=,tag:BkfFHAGJvtCDUpFoxXVP3Q==,type:str]
|
b2_application_key: ENC[AES256_GCM,data:xGAbBqx+6ErK7sy3FR0yza6mJU5oc5uQQGcwKtcPp1Ef4XEVd8do/wN7,iv:CojcoykDRBfvN8yqMMrPJq9mtAwxyswqXXVBKPupxDs=,tag:BkfFHAGJvtCDUpFoxXVP3Q==,type:str]
|
||||||
stalwart_admin_hash: ENC[AES256_GCM,data:GuSL/4dVdAsPDOqzvBv/rQ/TGrKU1Enc5MEQ17R8gHdsVH7ujIHxQYMu+aSAtKW0Le/hqNT+2TwOEXPsRt2ZQO/2Ks+hg7cN0mPGg8PRIN7v1wwrQpxVBNfyuX/HzwzLW/Plxc8g4Cmf8g==,iv:HwhafxB9ek9WnA76EJ04iaLZHJ72b4PtbJYOI1eFJcU=,tag:w1M+7KWOVkPZtyyE2Uv/Zg==,type:str]
|
stalwart_admin_hash: ENC[AES256_GCM,data:WqRfogOC+LrlSmg8/A4kLaUVJRdGgfrCnbuAzwyf4SMed37vsialN5QPR1IxiY02ueqz12dZ1VObittLMGxK+tUIIAwv3+F3uyKboDSyP9+wGAhBkcwTFTMjc6RMzaJ5VHW4OGaSvijiMQ==,iv:jvzoEI8evtFxuZ7W4P3nkQ0KwccVLj0j/s0s/MMXmIo=,tag:KopbV++Y/aABh1c8zfo1dw==,type:str]
|
||||||
|
wg_admin_password: ENC[AES256_GCM,data:X/+YJoNoFFU7P/2HCpTI,iv:FXDS3xuFB9jxjpQhsX2EKn0YFVzL23c2sZO6RqpKTiw=,tag:ccyQVzqNc1HEY0rxJSLlKw==,type:str]
|
||||||
|
virtualcam_license_signing_key: ENC[AES256_GCM,data:zk8YDER9d9mBOdXgEKKC6dubSMfz6UMMb758ZC9SvjkYIv+R2EVd+CJwL93MCs0l62lUDxoBDCgTLbKM55kcaThkkqfgZCYPrndXevYZ7kquNQEMkoYg/pyGAWhAQ9trvT5QXfBES9UDdYi6J9taCVKCofutiMEdoJM8olgUwKsP4i+fbt3idYkdWNGelastPnvTuUK/M3/dOKR5eheiy5oIntG+sECOz/2eKHJ8cU3ja18uAEwcpd9VojuOa0fvfui8AujQcMZNSMv8VsLGnoR854iX3g1MvHkTBSzv3qaDzRC54MueXYR4WOzm0o+yv6Otmw1n9trWtk+WLUimmiKxDdW6nSFaddeisWzy47+QYzrSsd7iovNYEIHnW4kpnT5W2g5lwaJwA0J0pBOqvV9gziv8oBNeEbLurBxi78KwHBqG,iv:svx+hC4tS26xqI05+yo6N2d3uHzO7ULM0e76dMbShjs=,tag:EJ9fjHEi1wUhLkxy8sexww==,type:str]
|
||||||
|
virtualcam_admin_token: ENC[AES256_GCM,data:JC5zSFBCoEapxv/Kcs4VXk3HAkiqwXVrCv1rlqFsXVx3LGspnQdzjOIahlM=,iv:GRVfMNGkwC3xXR00ww29rFbhTrpuViad+H05yDhR9BA=,tag:OBnxBD/8KU4mn17FFTcdng==,type:str]
|
||||||
|
git_ssh_key_b64: ENC[AES256_GCM,data:LaUcR005w9iCd/DifDhzLM7tU4AuTiFM802gMU1HMtvFN2iSffucaFJWxpKv2zFxDxz0QpA3XwM1M5yJC4K3YFMxPkPXcWLYN4yHcc1yXuoB3SseQXjcJnPMqZvJfVYhHep+6CVVlVDQs1Y+miBvbAhrvCEblwUu5LhD7yvoJLwTnEpT15WGDvqezzYKzNSyQGNRa8KPjsD/7gJN3RyUPzKo6GsrIp8d8eOTuCZwEcJVDKE0ACXs4V6cd+akqIWZkNPyilnMTuK5cqg2YuqSpwu9NncKED6SjmPiozPKKgTVaWhNCRJUTBfIw85fdXizvq7PNbNRxxBsscAWd0ixXiLqg1RTipfv85wL4Nru3r3qcQGAWmOpTHKW5fA9kR4ygdNxEVkOgQ70m9U0wev+vdIJUoRYj09Ikobej370NKppOP6+oeEDE7NpkFH7tzRk1oHtwoc1dwP9Kq1T8XdxttCKFyb1pAJ6gMdMThCfJlC9/Uu/WYbYVu5bCj6BAmrdrmhbPrsEOylBeC9j9RhyRrFEqLd8AZ01Zg8oKcj9WtCWEMf9Y4biv2xE43JbBGRvLAx3sH9MRplu2hN2W/bRxa/EPj9GHgbUjScDBkm/QVbMeV+QOgi09sCHDTeSX3BHdZ8ZLNjuNUwYnWZq4lPix0iYS5MufiixNdQmncPeHq0UuBtVifEl2Jmjq1COLyeAQe47AKQ5zDjT91xtBRZVZEVk0kGBYHtBEJ9ezPwckSw=,iv:cbIpwwhmyJVvvTMB95YBzt+RQLOji6yJh+SpcJc3Q1Y=,tag:t8WxU1IPDfkB8muTdBLUhg==,type:str]
|
||||||
|
virtualcam_smtp_pass: ENC[AES256_GCM,data:D5kDTQP9/YV5BjAWeTn0G2zKe+s=,iv:7dZJprrs0S2ECtbDQuvhUWsU0Xzpld5/XZqdbyefg0o=,tag:uWe811KAisBJ5M1EhJjrcQ==,type:str]
|
||||||
|
virtualcam_google_OAuth_secret: ENC[AES256_GCM,data:kPJIP4iH2vFo+VRojvhjzrQhDWidFq7kjC7ZaFcwJ7dNHlo=,iv:qKWClwSIRPU2Xz50bGYqU+eMR/iFxqnaKkb18eL3aMU=,tag:78jGQgL1myDBMiZP8GZ80g==,type:str]
|
||||||
sops:
|
sops:
|
||||||
kms: []
|
kms: []
|
||||||
gcp_kms: []
|
gcp_kms: []
|
||||||
@@ -35,8 +42,8 @@ sops:
|
|||||||
MHJrVVpDYWdJNmxtUkozSzR4Nmt3R28KrhYi830HUFAPfg8WvPad7BAuNe1mYOWt
|
MHJrVVpDYWdJNmxtUkozSzR4Nmt3R28KrhYi830HUFAPfg8WvPad7BAuNe1mYOWt
|
||||||
WEFIquuX/H/N+y/7uQcBDbvnBzyropE1hW8aNrxSKMeawvQZWNXkZA==
|
WEFIquuX/H/N+y/7uQcBDbvnBzyropE1hW8aNrxSKMeawvQZWNXkZA==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
lastmodified: "2026-08-05T20:44:15Z"
|
lastmodified: "2026-08-19T10:30:19Z"
|
||||||
mac: ENC[AES256_GCM,data:WIs44leXvMTFx2WBaUz2in2Cj0+nfjJ+wGD9Qxw6sLjfJkWZKKpEkyoajR6dEVenPKBVYCmNQ5AZKV6XA1ch3CpBObeig4aNpc4T9YFuT9avj3P8mFp3iA6ecpy/uwiFY8F6aP5D6/tgwDm6JNSu7K48CHLOTbyx3EMJCc6J1Wk=,iv:yDkBVTgMdaS6pfGyaf7LbkBwKbrmLjcntHhFlSYOvBI=,tag:G5qtf26XoWR458V5EgClNg==,type:str]
|
mac: ENC[AES256_GCM,data:JOWFguh2iE4+OyU00l+2gn7GWv2yej37OLLhVVO6Yb7Kv4vgk4NhZKJLqopteQK1o6dPeWbeIydHG1Qmef0Sm0NmCyi7i6BCTplVizf/puaGjpkU5YAs90+HiEa9cKlNQ5brfNH3YecJeN9dK+d7o8bMO/xPv3raqFMBBoVtgu4=,iv:A7kCOzyWwiVaXKVMKXQ4j86zz2f65wAjOtaVEuLa81I=,tag:bcbPsmRRFRHX7qi4WdYb0g==,type:str]
|
||||||
pgp: []
|
pgp: []
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
version: 3.8.1
|
version: 3.8.1
|
||||||
|
|||||||
@@ -19,6 +19,16 @@
|
|||||||
in {
|
in {
|
||||||
package = pkgs.lix;
|
package = pkgs.lix;
|
||||||
|
|
||||||
|
gc = {
|
||||||
|
automatic = true;
|
||||||
|
dates = "weekly";
|
||||||
|
options = "--delete-older-than 14d";
|
||||||
|
};
|
||||||
|
|
||||||
|
optimise = {
|
||||||
|
automatic = true;
|
||||||
|
};
|
||||||
|
|
||||||
# pin the registry to avoid downloading and evaling a new nixpkgs version every time
|
# pin the registry to avoid downloading and evaling a new nixpkgs version every time
|
||||||
registry = lib.mapAttrs (_: v: {flake = v;}) flakeInputs;
|
registry = lib.mapAttrs (_: v: {flake = v;}) flakeInputs;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user