Compare commits

...

3 commits

Author SHA1 Message Date
Patrick Großmann 13e1a8fb32
feat: mails!!!! 2024-01-28 02:10:15 +01:00
Patrick Großmann 04c127e144
feat: forgejo backups
feat: maddy
2024-01-27 23:21:42 +01:00
Patrick Großmann b4db6868e8
feat: tried musnix; didn't work 2024-01-27 00:36:23 +01:00
25 changed files with 412 additions and 45 deletions

View file

@ -961,6 +961,26 @@
"type": "github"
}
},
"musnix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1702456985,
"narHash": "sha256-mnjCk8mINY4t5uIwP2eR19FOQXHTCO68+KsgKFTv1NI=",
"owner": "musnix",
"repo": "musnix",
"rev": "cf93a72da8ad677864045e9a5dd32170378d9a62",
"type": "github"
},
"original": {
"owner": "musnix",
"repo": "musnix",
"type": "github"
}
},
"nix-darwin": {
"inputs": {
"nixpkgs": [
@ -1496,6 +1516,7 @@
"impermanence": "impermanence",
"lanzaboote": "lanzaboote",
"microvm": "microvm",
"musnix": "musnix",
"nix-index-database": "nix-index-database",
"nixos-extra-modules": "nixos-extra-modules",
"nixos-generators": "nixos-generators",

View file

@ -60,6 +60,10 @@
inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-utils.follows = "flake-utils";
};
musnix = {
url = "github:musnix/musnix";
inputs.nixpkgs.follows = "nixpkgs";
};
#templates.url = "git+https://git.lel.lol/patrick/nix-templates.git";

View file

@ -19,8 +19,6 @@
../../modules/hardware/physical.nix
../../modules/hardware/zfs.nix
../../modules/services/acme.nix
./net.nix
./fs.nix
]

View file

@ -39,4 +39,36 @@
interface = "lan01";
mode = "bridge";
};
age.secrets.cloudflare_token_acme = {
rekeyFile = ./secrets/cloudflare_api_token.age;
mode = "440";
group = "acme";
};
security.acme = {
acceptTerms = true;
defaults = {
email = config.secrets.secrets.global.devEmail;
dnsProvider = "cloudflare";
dnsPropagationCheck = true;
reloadServices = ["nginx"];
credentialFiles = {
"CF_DNS_API_TOKEN_FILE" = config.age.secrets.cloudflare_token_acme.path;
"CF_ZONE_API_TOKEN_FILE" = config.age.secrets.cloudflare_token_acme.path;
};
};
};
security.acme.certs.web = {
domain = config.secrets.secrets.global.domains.web;
extraDomainNames = ["*.${config.secrets.secrets.global.domains.web}"];
};
users.groups.acme.members = ["nginx"];
environment.persistence."/state".directories = [
{
directory = "/var/lib/acme";
user = "acme";
group = "acme";
mode = "0755";
}
];
}

View file

@ -0,0 +1,13 @@
age-encryption.org/v1
-> X25519 j1vdwUZ+o5coMFAaOCyiS42rLq7FPX6xwuWmoHcN61U
m1QEYj4NW5IdNsFh26Uhwe2Sg1ggkvTYB92S4B2lC8M
-> piv-p256 XTQkUA AhjsxoVBz3h/1Sj+cwnT7gpcE6SDMhNOBMU9nP+gfC5G
a7E3dolF4QaxTVpJBOKA314INK32eTdDykDyRT+/8XQ
-> piv-p256 ZFgiIw Ah49xwjTzvroi4R90URbbE0yY15w+OvUsWZ2cQdYHs/w
4i6XZ8lwOeWinlU1IiCgUBTSWMzxuPyvYKRbz6GqNUk
-> piv-p256 ZFgiIw A49Cv751h0WJYL6qPceFVwjbGVpF668SGKVjHq/lQ4Rs
AAGD0jOCHIOAIBk872SJwe2mCx69xn/1ZjiswebgU0w
-> K("0$@8-grease z`/W }"_xiVH <~Bj._
--- /NUrs98fD72LqCIYVOzrUhFNhxGivAEOZ9pob65I2fI
:(#­¥aô[8@BÊ4èÝ|ÍC7!³>Ù?¬Œ›`5á‰o‡Þr õB´ˆ°y`óAIJ;)÷Å&“)@ÝÎB²¹Ï¡ñ´¾Q

View file

@ -7,6 +7,7 @@
[
../../modules/config
../../modules/optional/initrd-ssh.nix
../../modules/services/maddy.nix
../../modules/hardware/zfs.nix

View file

@ -28,7 +28,6 @@
fileSystems."/state".neededForBoot = true;
fileSystems."/persist".neededForBoot = true;
boot.initrd.luks.devices.enc-rpool.allowDiscards = true;
boot.loader.grub.devices = [
"/dev/disk/by-id/${config.secrets.secrets.local.disko.drive}"
];

View file

@ -1,4 +1,8 @@
{config, ...}: {
{
config,
lib,
...
}: {
networking.hostId = config.secrets.secrets.local.networking.hostId;
networking.domain = config.secrets.secrets.global.domains.mail;
@ -13,7 +17,7 @@
in {
address = [
icfg.hostCidrv4
icfg.hostCidrv6
(lib.net.cidr.hostCidr 1 icfg.hostCidrv6)
];
gateway = ["fe80::1"];
routes = [
@ -30,4 +34,35 @@
linkConfig.RequiredForOnline = "routable";
};
};
age.secrets.cloudflare_token_acme = {
rekeyFile = ./secrets/cloudflare_api_token.age;
mode = "440";
group = "acme";
};
security.acme = {
acceptTerms = true;
defaults = {
email = config.secrets.secrets.global.devEmail;
dnsProvider = "cloudflare";
dnsPropagationCheck = true;
reloadServices = ["nginx"];
credentialFiles = {
"CF_DNS_API_TOKEN_FILE" = config.age.secrets.cloudflare_token_acme.path;
"CF_ZONE_API_TOKEN_FILE" = config.age.secrets.cloudflare_token_acme.path;
};
};
};
security.acme.certs.mail = {
domain = config.secrets.secrets.global.domains.mail;
extraDomainNames = ["*.${config.secrets.secrets.global.domains.mail}"];
};
users.groups.acme.members = ["maddy"];
environment.persistence."/state".directories = [
{
directory = "/var/lib/acme";
user = "acme";
group = "acme";
mode = "0755";
}
];
}

View file

@ -0,0 +1,15 @@
age-encryption.org/v1
-> X25519 uhnRibm92XSz2UcJWT43CrsZfOrSzUyqVFU8nWiYEXs
QNxh6YGDCgSSoCWLthZlou7F7i9OJpunB+/6J4ogk2k
-> piv-p256 XTQkUA AzTDTMXLU5jTp54ysvnVIDo5lIb5ED1zkP8659tTH2JJ
VLO6rtfY5poFGVH/eeD+T/xrlNdPGnlLQ6mK1HytT8A
-> piv-p256 ZFgiIw AnwL/t0GNZI3/y7KlatHLebToW1pJLfOasODGQ7ogriz
Wl7xm6+a1qmqLeTZszpO0XG96BcDRO5l8wvpc0atW0Y
-> piv-p256 5vmPtQ AzC3t9sPdKF/IPkJSqhldnx3Mnkc84DCD13l8tYqZIWd
GaNzRxPoSOy/kEuLzbXpiRDo5F2hZT8KriXpgqZkQ5Y
-> piv-p256 ZFgiIw ApFdJVoW4zoWq38fE27TR/OFEDs4Wub1g3q6RiF+fDTR
IypnQqeluntk31gez5I6eYtlKiY/8sy+dXNkpWhdwPs
-> wX-grease
neAQttCOcpQWsfSpI38jdOjODJYK8uOhqjWsZOLWlHZaRUQtoyXI
--- r44AgWizs6H92oY6hKMs67ARXqr8Je0Z0cIJr9xidBg
°ß¦Ñ¨â<>Ÿî̪øÙ¤Ph\œdv úí¥]’ÀÓšÆÜŠÚ˜ùÄE<C384>ʃ´¯æewIé‡t.¬²WÃÂ6ZFi

Binary file not shown.

View file

@ -33,6 +33,7 @@
inputs.lanzaboote.nixosModules.lanzaboote
inputs.nixvim.nixosModules.nixvim
inputs.nixos-extra-modules.nixosModules.default
inputs.musnix.nixosModules.musnix
];
age.identityPaths = ["/state/etc/ssh/ssh_host_ed25519_key"];
boot.mode = lib.mkDefault "efi";

View file

@ -61,6 +61,8 @@
fd
kitty.terminfo
nvd
pciutils
usbutils
# fix pcscd
pcscliteWithPolkit.out
];

View file

@ -25,6 +25,7 @@
vaultwarden = uidGid 215;
redis-paperless = uidGid 216;
microvm = uidGid 217;
maddy = uidGid 218;
paperless = uidGid 315;
systemd-oom = uidGid 300;
systemd-coredump = uidGid 301;

View file

@ -5,6 +5,14 @@
...
}:
lib.optionalAttrs (!minimal) {
# Sadly does not seem to do anything yet
#musnix = {
# enable = true;
# kernel= {
# realtime = true;
# packages = pkgs.linuxPackages_6_6_rt;
# };
#};
environment.systemPackages = with pkgs; [pulseaudio pulsemixer];
hardware.pulseaudio.enable = lib.mkForce false;

View file

@ -1,10 +1,11 @@
{
pkgs,
config,
lib,
...
}: {
boot.supportedFilesystems = ["zfs"];
boot.kernelPackages = config.boot.zfs.package.latestCompatibleLinuxPackages;
boot.kernelPackages = lib.mkDefault config.boot.zfs.package.latestCompatibleLinuxPackages;
# The root pool should never be imported forcefully.
# Failure to import is important to notice!

View file

@ -1,37 +0,0 @@
{
config,
lib,
...
}: {
age.secrets.cloudflare_token_acme = {
rekeyFile = ../../secrets/cloudflare/api_token.age;
mode = "440";
group = "acme";
};
security.acme = {
acceptTerms = true;
defaults = {
email = config.secrets.secrets.global.devEmail;
dnsProvider = "cloudflare";
dnsPropagationCheck = true;
reloadServices = ["nginx"];
credentialFiles = {
"CF_DNS_API_TOKEN_FILE" = config.age.secrets.cloudflare_token_acme.path;
"CF_ZONE_API_TOKEN_FILE" = config.age.secrets.cloudflare_token_acme.path;
};
};
};
security.acme.certs = lib.flip lib.mapAttrs config.secrets.secrets.global.domains (_: value: {
domain = value;
extraDomainNames = ["*.${value}"];
});
users.groups.acme.members = ["nginx"];
environment.persistence."/state".directories = [
{
directory = "/var/lib/acme";
user = "acme";
group = "acme";
mode = "0755";
}
];
}

View file

@ -1,6 +1,6 @@
{config, ...}: {
age.secrets.cloudflare_token_dns = {
rekeyFile = ../../secrets/cloudflare/api_token.age;
rekeyFile = "${config.node.secretsDir}/cloudflare_api_token.age";
mode = "440";
};
# So we only update the A record

View file

@ -5,6 +5,38 @@
}: let
giteaDomain = "git.${config.secrets.secrets.global.domains.web}";
in {
age.secrets.resticpasswd = {
generator.script = "alnum";
};
age.secrets.forgejoHetznerSsh = {
generator.script = "ssh-ed25519";
};
services.restic.backups = {
main = {
user = "root";
timerConfig = {
OnCalendar = "06:00";
Persistent = true;
RandomizedDelaySec = "3h";
};
initialize = true;
passwordFile = config.age.secrets.resticpasswd.path;
hetznerStorageBox = {
enable = true;
inherit (config.secrets.secrets.global.hetzner) mainUser;
inherit (config.secrets.secrets.global.hetzner.users.forgejo) subUid path;
sshAgeSecret = "forgejoHetznerSsh";
};
paths = [config.services.gitea.stateDir];
pruneOpts = [
"--keep-daily 10"
"--keep-weekly 7"
"--keep-monthly 12"
"--keep-yearly 75"
];
};
};
# Recommended by forgejo: https://forgejo.org/docs/latest/admin/recommendations/#git-over-ssh
services.openssh.settings.AcceptEnv = "GIT_PROTOCOL";
networking.firewall.allowedTCPPorts = [3000 9922];

241
modules/services/maddy.nix Normal file
View file

@ -0,0 +1,241 @@
# TODO
# autoconfig
# catch all
# service sending
# trash domain
{
config,
pkgs,
...
}: let
domain = config.secrets.secrets.global.domains.mail;
in {
age.secrets.patrickPasswd = {
generator.script = "alnum";
owner = "maddy";
group = "maddy";
};
# Opening ports for additional TLS listeners. This is not yet
# implemented in the module.
networking.firewall.allowedTCPPorts = [993 465];
services.maddy = {
enable = true;
hostname = "mx1" + domain;
primaryDomain = domain;
tls = {
certificates = [
{
keyPath = "${config.security.acme.certs.mail.directory}/key.pem";
certPath = "${config.security.acme.certs.mail.directory}/fullchain.pem";
}
];
loader = "file";
};
ensureCredentials = {
"patrick@${domain}".passwordFile = config.age.secrets.patrickPasswd.path;
};
ensureAccounts = [
"patrick@${domain}"
];
openFirewall = true;
config = ''
## Maddy Mail Server - default configuration file (2022-06-18)
# Suitable for small-scale deployments. Uses its own format for local users DB,
# should be managed via maddy subcommands.
#
# See tutorials at https://maddy.email for guidance on typical
# configuration changes.
# ----------------------------------------------------------------------------
# Local storage & authentication
# pass_table provides local hashed passwords storage for authentication of
# users. It can be configured to use any "table" module, in default
# configuration a table in SQLite DB is used.
# Table can be replaced to use e.g. a file for passwords. Or pass_table module
# can be replaced altogether to use some external source of credentials (e.g.
# PAM, /etc/shadow file).
#
# If table module supports it (sql_table does) - credentials can be managed
# using 'maddy creds' command.
auth.pass_table local_authdb {
table sql_table {
driver sqlite3
dsn credentials.db
table_name passwords
}
}
# imapsql module stores all indexes and metadata necessary for IMAP using a
# relational database. It is used by IMAP endpoint for mailbox access and
# also by SMTP & Submission endpoints for delivery of local messages.
#
# IMAP accounts, mailboxes and all message metadata can be inspected using
# imap-* subcommands of maddy.
storage.imapsql local_mailboxes {
driver sqlite3
dsn imapsql.db
}
# ----------------------------------------------------------------------------
# SMTP endpoints + message routing
table.chain local_rewrites {
optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3"
optional_step static {
entry postmaster postmaster@$(primary_domain)
}
optional_step file /etc/maddy/aliases
}
msgpipeline local_routing {
# Insert handling for special-purpose local domains here.
# e.g.
# destination lists.example.org {
# deliver_to lmtp tcp://127.0.0.1:8024
# }
destination postmaster $(local_domains) {
modify {
replace_rcpt &local_rewrites
}
deliver_to &local_mailboxes
}
default_destination {
reject 550 5.1.1 "User doesn't exist"
}
}
smtp tcp://0.0.0.0:25 {
limits {
# Up to 20 msgs/sec across max. 10 SMTP connections.
all rate 20 1s
all concurrency 10
}
dmarc yes
max_message_size 256M
check {
require_mx_record
dkim
spf
}
source $(local_domains) {
reject 501 5.1.8 "Use Submission for outgoing SMTP"
}
default_source {
destination postmaster $(local_domains) {
deliver_to &local_routing
}
default_destination {
reject 550 5.1.1 "User doesn't exist"
}
}
}
submission tls://0.0.0.0:465 tcp://0.0.0.0:587 {
limits {
# Up to 50 msgs/sec across any amount of SMTP connections.
all rate 50 1s
}
auth &local_authdb
source $(local_domains) {
check {
authorize_sender {
prepare_email &local_rewrites
user_to_email identity
}
}
destination postmaster $(local_domains) {
deliver_to &local_routing
}
default_destination {
modify {
dkim $(primary_domain) $(local_domains) default
}
deliver_to &remote_queue
}
}
default_source {
reject 501 5.1.8 "Non-local sender domain"
}
}
target.remote outbound_delivery {
limits {
# Up to 20 msgs/sec across max. 10 SMTP connections
# for each recipient domain.
destination rate 20 1s
destination concurrency 10
}
mx_auth {
dane
mtasts {
cache fs
fs_dir mtasts_cache/
}
local_policy {
min_tls_level encrypted
min_mx_level none
}
}
}
target.queue remote_queue {
target &outbound_delivery
autogenerated_msg_domain $(primary_domain)
bounce {
destination postmaster $(local_domains) {
deliver_to &local_routing
}
default_destination {
reject 550 5.0.0 "Refusing to send DSNs to non-local addresses"
}
}
}
# ----------------------------------------------------------------------------
# IMAP endpoints
imap tls://0.0.0.0:993 tcp://0.0.0.0:143 {
auth &local_authdb
storage &local_mailboxes
}
'';
};
services.nginx = {
enable = true;
virtualHosts."mta-sts.pgrossmann.org".extraConfig = ''
encode gzip
file_server
root * ${
pkgs.runCommand "testdir" {} ''
mkdir -p "$out/.well-known"
echo "
version: STSv1
mode: enforce
max_age: 604800
mx: mx1.pgrossmann.org
" > "$out/.well-known/mta-sts.txt"
''
}
'';
};
environment.persistence."/persist".directories = [
{
directory = "/var/lib/maddy";
user = "maddy";
group = "maddy";
mode = "0755";
}
];
}

Binary file not shown.

View file

@ -6,6 +6,7 @@
./programs/gpg.nix
./programs/nvim
./programs/htop.nix
];
programs.bat.enable = true;

View file

@ -61,7 +61,6 @@ lib.optionalAttrs (!minimal) {
../common/impermanence.nix
../common/programs/direnv.nix
../common/programs/htop.nix
../common/programs/git.nix
../common/programs/bottles.nix
../common/programs/gdb.nix