feat: stalwart mail-server

This commit is contained in:
danny 2025-09-17 15:24:07 +08:00
parent 85feeb7b3f
commit a565033341
16 changed files with 1192 additions and 442 deletions

View file

@ -8,6 +8,7 @@ creation_rules:
key_groups:
- age:
- *dn_server
- *dn_pre7780
- path_regex: system/dev/dn-pre7780/secret.yaml
key_groups:
- age:

113
flake.lock generated
View file

@ -491,6 +491,24 @@
}
},
"flake-parts_4": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib_2"
},
"locked": {
"lastModified": 1733312601,
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_5": {
"inputs": {
"nixpkgs-lib": [
"nvf",
@ -511,7 +529,7 @@
"type": "github"
}
},
"flake-parts_5": {
"flake-parts_6": {
"inputs": {
"nixpkgs-lib": [
"stylix",
@ -532,6 +550,21 @@
"type": "github"
}
},
"flake-root": {
"locked": {
"lastModified": 1723604017,
"narHash": "sha256-rBtQ8gg+Dn4Sx/s+pvjdq3CB2wQNzx9XGFq/JVGCB6k=",
"owner": "srid",
"repo": "flake-root",
"rev": "b759a56851e10cb13f6b8e5698af7b59c44be26e",
"type": "github"
},
"original": {
"owner": "srid",
"repo": "flake-root",
"type": "github"
}
},
"flake-schemas": {
"locked": {
"lastModified": 1721999734,
@ -1405,6 +1438,22 @@
"type": "github"
}
},
"marks-nvim": {
"flake": false,
"locked": {
"lastModified": 1747179163,
"narHash": "sha256-ho2b2Ulh+GTqY0QvW7zjFOSlF5g/kaxWyOjKWhTFq7c=",
"owner": "chentoast",
"repo": "marks.nvim",
"rev": "f353e8c08c50f39e99a9ed474172df7eddd89b72",
"type": "github"
},
"original": {
"owner": "chentoast",
"repo": "marks.nvim",
"type": "github"
}
},
"microvm": {
"inputs": {
"flake-utils": "flake-utils_4",
@ -1563,6 +1612,29 @@
"type": "github"
}
},
"nixd": {
"inputs": {
"flake-parts": "flake-parts_4",
"flake-root": "flake-root",
"nixpkgs": [
"nixpkgs"
],
"treefmt-nix": "treefmt-nix_2"
},
"locked": {
"lastModified": 1756563652,
"narHash": "sha256-0MvTa6l071JAbePgP3qTkNXr1CbeGDmqyDyvVHxetqg=",
"owner": "nix-community",
"repo": "nixd",
"rev": "15a3376f65de9e7984429b975777f3569430b8a6",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixd",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1755027561,
@ -1594,6 +1666,18 @@
"type": "github"
}
},
"nixpkgs-lib_2": {
"locked": {
"lastModified": 1733096140,
"narHash": "sha256-1qRH7uAUsyQI7R1Uwl4T+XvdNv778H0Nb5njNrqvylY=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1730741070,
@ -1795,7 +1879,7 @@
"nvf": {
"inputs": {
"flake-compat": "flake-compat_8",
"flake-parts": "flake-parts_4",
"flake-parts": "flake-parts_5",
"mnw": "mnw",
"nixpkgs": [
"nixpkgs"
@ -1903,11 +1987,13 @@
"hyprlock": "hyprlock",
"hyprtasking": "hyprtasking",
"lanzaboote": "lanzaboote",
"marks-nvim": "marks-nvim",
"microvm": "microvm",
"neovim-nightly-overlay": "neovim-nightly-overlay",
"nix-index-database": "nix-index-database",
"nix-minecraft": "nix-minecraft",
"nix-tmodloader": "nix-tmodloader",
"nixd": "nixd",
"nixpkgs": "nixpkgs_7",
"nvf": "nvf",
"sops-nix": "sops-nix",
@ -2042,7 +2128,7 @@
"base16-helix": "base16-helix",
"base16-vim": "base16-vim",
"firefox-gnome-theme": "firefox-gnome-theme",
"flake-parts": "flake-parts_5",
"flake-parts": "flake-parts_6",
"gnome-shell": "gnome-shell",
"nixpkgs": [
"nixpkgs"
@ -2371,6 +2457,27 @@
"type": "github"
}
},
"treefmt-nix_2": {
"inputs": {
"nixpkgs": [
"nixd",
"nixpkgs"
]
},
"locked": {
"lastModified": 1734704479,
"narHash": "sha256-MMi74+WckoyEWBRcg/oaGRvXC9BVVxDZNRMpL+72wBI=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "65712f5af67234dad91a5a4baee986a8b62dbf8f",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
},
"xdph": {
"inputs": {
"hyprland-protocols": [

View file

@ -117,16 +117,28 @@
url = "github:NotAShelf/nvf";
inputs.nixpkgs.follows = "nixpkgs";
};
nixd = {
url = "github:nix-community/nixd";
inputs.nixpkgs.follows = "nixpkgs";
};
outputs = {
marks-nvim = {
url = "github:chentoast/marks.nvim";
flake = false;
};
};
outputs =
{
self,
nixpkgs,
nix-index-database,
lanzaboote,
home-manager,
...
} @ inputs: let
}@inputs:
let
system = "x86_64-linux";
nix-version = "25.05";
@ -136,7 +148,7 @@
inherit (pkgs) lib;
helper = import ./helper {inherit pkgs lib;};
helper = import ./helper { inherit pkgs lib; };
# Declare COMMON modules here
common-settings = {
@ -195,9 +207,9 @@
systemd.network.networks."10-lan-bridge" = {
matchConfig.Name = "br0";
networkConfig = {
Address = ["192.168.0.5/24"];
Address = [ "192.168.0.5/24" ];
Gateway = "192.168.0.1";
DNS = ["192.168.0.1"];
DNS = [ "192.168.0.1" ];
};
linkConfig.RequiredForOnline = "routable";
@ -217,7 +229,7 @@
};
}
];
overlays = [];
overlays = [ ];
};
# Laptop
@ -251,19 +263,17 @@
];
};
};
in {
in
{
nixosConfigurations =
(builtins.mapAttrs (
dev: conf: let
domain =
if conf.domain != null
then conf.domain
else "local";
dev: conf:
let
domain = if conf.domain != null then conf.domain else "local";
inherit (conf) username hostname;
in
nixpkgs.lib.nixosSystem {
modules =
[
modules = [
{
system.stateVersion = nix-version;
home-manager = {
@ -308,24 +318,24 @@
]
++ common-settings.modules
++ conf.extra-modules;
specialArgs =
{
specialArgs = {
inherit username;
}
// common-settings.args;
}
)
devices)
) devices)
//
# VM For k8s
(
let
vmList = let
vmList =
let
kubeMasterIP = "192.168.0.6";
kubeMasterHostname = "api.kube";
kubeMasterAPIServerPort = 6443;
kubeApi = "https://${kubeMasterHostname}:${toString kubeMasterAPIServerPort}";
in {
in
{
# master
vm-1 = {
ip = "192.168.0.6";
@ -356,7 +366,7 @@
};
systemd.services.link-kube-config = {
wantedBy = ["multi-user.target"];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.writeShellScript "link-kube-config.sh" ''
@ -384,7 +394,7 @@
];
services.kubernetes = {
roles = ["node"];
roles = [ "node" ];
masterAddress = kubeMasterHostname;
easyCerts = true;
@ -448,15 +458,15 @@
systemd.network.networks."20-lan" = {
matchConfig.Type = "ether";
networkConfig = {
Address = ["${value.ip}/24"];
Address = [ "${value.ip}/24" ];
Gateway = "192.168.0.1";
DNS = ["192.168.0.1"];
DNS = [ "192.168.0.1" ];
DHCP = "no";
};
};
systemd.services.br-netfilter = {
wantedBy = ["multi-user.target"];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "/run/current-system/sw/bin/modprobe br_netfilter";
};
@ -479,8 +489,7 @@
];
}
)
)
vmList
) vmList
)
// {
vps = nixpkgs.lib.nixosSystem {

View file

@ -2,8 +2,11 @@
pkgs,
lib,
osConfig,
inputs,
system,
...
}: let
}:
let
inherit (lib.generators) mkLuaInline;
suda-nvim = pkgs.vimUtils.buildVimPlugin {
@ -15,7 +18,13 @@
hash = "sha256-46sy3rAdOCULVt1RkIoGdweoV3MqQaB33Et9MrxI6Lk=";
};
};
in {
marks-nvim = pkgs.vimUtils.buildVimPlugin {
name = "marks-nvim";
src = inputs.marks-nvim;
};
in
{
programs.nvf = {
enable = true;
settings = {
@ -69,6 +78,12 @@ in {
suda = {
package = suda-nvim;
};
marks = {
package = marks-nvim;
setup = ''
require("marks").setup {}
'';
};
};
keymaps = [
@ -76,16 +91,16 @@ in {
# Explorer
{
key = "<leader>e";
mode = ["n"];
mode = [ "n" ];
action = ":Neotree toggle<CR>";
silent = true;
desc = "Toggle file explorer";
}
# Fzf lua
# === Fzf lua === #
{
key = "<Leader><Space>";
silent = true;
mode = ["n"];
mode = [ "n" ];
action = ":FzfLua files<CR>";
nowait = true;
unique = true;
@ -93,7 +108,7 @@ in {
}
{
key = "<Leader>/";
mode = ["n"];
mode = [ "n" ];
action = ":FzfLua live_grep<CR>";
nowait = true;
unique = true;
@ -103,7 +118,7 @@ in {
{
key = "<Leader>ss";
silent = true;
mode = ["n"];
mode = [ "n" ];
action = ":FzfLua lsp_document_symbols<CR>";
nowait = true;
unique = true;
@ -113,23 +128,37 @@ in {
{
key = "<Leader>sS";
silent = true;
mode = ["n"];
mode = [ "n" ];
action = ":FzfLua lsp_workspace_symbols<CR>";
unique = true;
nowait = true;
desc = "Find symbols (workspace)";
}
# Registers
{
key = ''""'';
mode = [ "n" ];
action = ":FzfLua registers<CR>";
desc = "Registers";
}
# Marks
{
key = "''";
mode = [ "n" ];
action = ":FzfLua marks<CR>";
desc = "Marks";
}
# === Buffer === #
{
key = "<Leader>bo";
mode = ["n"];
mode = [ "n" ];
action = ":BufferLineCloseOther<CR>";
desc = "Close other buffer";
}
{
key = "<Leader>bS";
mode = ["n"];
mode = [ "n" ];
action = ":SudaWrite<CR>";
desc = "Save file as root";
}
@ -148,48 +177,48 @@ in {
}
{
key = "<S-Tab>";
mode = ["i"];
mode = [ "i" ];
action = "<C-d>";
desc = "Shift left";
}
{
key = "gd";
mode = ["n"];
mode = [ "n" ];
action = ":FzfLua lsp_definitions<CR>";
nowait = true;
desc = "Go to definition";
}
{
key = "gD";
mode = ["n"];
mode = [ "n" ];
action = ":FzfLua lsp_declarations<CR>";
nowait = true;
desc = "Go to declaration";
}
{
key = "gi";
mode = ["n"];
mode = [ "n" ];
action = ":FzfLua lsp_implementations<CR>";
nowait = true;
desc = "Go to implementation";
}
{
key = "gr";
mode = ["n"];
mode = [ "n" ];
action = ":FzfLua lsp_references<CR>";
nowait = true;
desc = "List references";
}
{
key = "<Leader>n";
mode = ["n"];
mode = [ "n" ];
action = ":NoiceAll<CR>";
nowait = true;
desc = "Notifications";
}
{
key = "<ESC><ESC>";
mode = ["n"];
mode = [ "n" ];
action = ":noh<CR>";
desc = "Clear highlight";
}
@ -197,14 +226,14 @@ in {
# === Tab === #
{
key = ">";
mode = ["v"];
mode = [ "v" ];
action = ">gv";
silent = true;
desc = "Shift right";
}
{
key = "<";
mode = ["v"];
mode = [ "v" ];
action = "<gv";
silent = true;
desc = "Shift left";
@ -213,22 +242,22 @@ in {
# === Terminal === #
{
key = "<C-/>";
mode = ["t"];
mode = [ "t" ];
action = "<C-\\><C-n>:ToggleTerm<CR>";
}
{
key = "<C-_>";
mode = ["t"];
mode = [ "t" ];
action = "<C-\\><C-n>:ToggleTerm<CR>";
}
{
key = "<C-_>";
mode = ["n"];
mode = [ "n" ];
action = ":ToggleTerm<CR>";
}
{
key = "<ESC><ESC>";
mode = ["t"];
mode = [ "t" ];
action = "<C-\\><C-n>";
}
{
@ -270,7 +299,7 @@ in {
# New Term
{
key = "<Leader>tn";
mode = ["n"];
mode = [ "n" ];
action = ":TermNew<CR>";
nowait = true;
desc = "Spawn new terminal";
@ -278,7 +307,7 @@ in {
# Select Term
{
key = "<Leader>tt";
mode = ["n"];
mode = [ "n" ];
action = ":TermSelect<CR>";
nowait = true;
desc = "Select terminal";
@ -286,7 +315,7 @@ in {
# Send current selection to Term
{
key = "<Leader>ts";
mode = ["v"];
mode = [ "v" ];
action = ":ToggleTermSendVisualSelection<CR>";
nowait = true;
desc = "Send current selection to terminal";
@ -295,7 +324,7 @@ in {
# === Fold (nvim-ufo) === #
{
key = "zR";
mode = ["n"];
mode = [ "n" ];
action = ''
require("ufo").openAllFolds
'';
@ -303,7 +332,7 @@ in {
}
{
key = "zM";
mode = ["n"];
mode = [ "n" ];
action = ''
require("ufo").closeAllFolds
'';
@ -313,7 +342,7 @@ in {
autocmds = [
{
event = ["TextYankPost"];
event = [ "TextYankPost" ];
callback =
mkLuaInline
# lua
@ -325,7 +354,7 @@ in {
desc = "Highlight yanked";
}
{
event = ["BufWritePost"];
event = [ "BufWritePost" ];
callback =
mkLuaInline
# lua
@ -427,17 +456,21 @@ in {
};
nix = {
enable = true;
extraDiagnostics.enable = false;
format.type = "nixfmt";
lsp = {
server = "nixd";
server = "nil";
options = {
nixos.expr =
# nix
''(builtins.getFlake (builtins.toString ./.)).nixosConfigurations.${osConfig.networking.hostName}.options'';
home_manager.expr =
# nix
''(builtins.getFlake (builtins.toString ./.)).nixosConfigurations.${osConfig.networking.hostName}.options.home-manager.users.type.getSubOptions []'';
nix.flake.autoArchive = true;
};
# options = {
# nixos.expr =
# # nix
# ''(builtins.getFlake (builtins.toString ./.)).nixosConfigurations.${osConfig.networking.hostName}.options'';
# home_manager.expr =
# # nix
# ''(builtins.getFlake (builtins.toString ./.)).nixosConfigurations.${osConfig.networking.hostName}.options.home-manager.users.type.getSubOptions []'';
# };
};
};
sql.enable = true;
@ -450,7 +483,12 @@ in {
};
};
python.enable = true;
markdown.enable = true;
markdown = {
enable = true;
extensions = {
render-markdown-nvim.enable = true;
};
};
html.enable = true;
lua.enable = true;
};
@ -667,7 +705,7 @@ in {
snippets.luasnip = {
enable = true;
providers = ["blink-cmp"];
providers = [ "blink-cmp" ];
setupOpts.enable_autosnippets = true;
};
@ -716,7 +754,85 @@ in {
whichKey.enable = true;
};
fzf-lua.enable = true;
fzf-lua = {
enable = true;
setupOpts = {
previewers = {
builtin = {
extensions = {
"jpg" = {
"kitty" = "";
};
};
snacks_image = {
enabled = false;
render_inline = false;
};
};
};
winopts = {
preview = {
hidden = "hidden";
};
border = "rounded";
};
fzf_opts = {
"--no-header" = "";
"--no-scrollbar" = "";
};
files = {
formatter = "path.filename_first";
prompt = ":";
no_header = true;
cwd_header = false;
cwd_prompt = false;
winopts = {
title = " files 📑 ";
title_pos = "center";
title_flags = false;
};
};
buffers = {
formatter = "path.filename_first";
prompt = ":";
no_header = true;
fzf_opts = {
"--delimiter" = " ";
"--with-nth" = "-1..";
};
winopts = {
title = " buffers 📝 ";
title_pos = "center";
};
};
lsp = {
symbols = {
cwd_only = true;
no_header = true;
prompt = ":";
winopts = {
title = " symbols ";
title_pos = "center";
height = 0.6;
preview = {
hidden = "nohidden";
horizontal = "down:40%";
wrap = "wrap";
};
};
};
};
registers = {
prompt = "registers:";
filter = "%a";
winopts = {
title = " registers 🏷 ";
title_pos = "center";
};
};
};
};
dashboard = {
alpha.enable = true;
@ -745,7 +861,12 @@ in {
};
images = {
img-clip.enable = true;
image-nvim = {
enable = true;
setupOpts = {
backend = "kitty";
};
};
};
};
@ -782,7 +903,7 @@ in {
event = "notify";
kind = "info";
any = [
{find = "hidden";}
{ find = "hidden"; }
];
};
}
@ -800,7 +921,7 @@ in {
filter = {
event = "msg_show";
any = [
{find = "written";}
{ find = "written"; }
];
};
}

View file

@ -1,4 +1,5 @@
[
(import ./vesktop.nix)
(import ./powerdns-admin.nix)
(import ./stalwart-mail)
]

View file

@ -0,0 +1,7 @@
final: prev: {
stalwart-mail = prev.stalwart-mail.overrideAttrs (oldAttrs: {
patches = [
./enable_root_ca.patch
];
});
}

View file

@ -0,0 +1,291 @@
diff --git a/crates/common/src/config/server/tls.rs b/crates/common/src/config/server/tls.rs
index 603cf889..58e38c84 100644
--- a/crates/common/src/config/server/tls.rs
+++ b/crates/common/src/config/server/tls.rs
@@ -5,7 +5,7 @@
*/
use std::{
- io::Cursor,
+ io::{Cursor, Read},
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
sync::Arc,
time::Duration,
@@ -35,7 +35,8 @@ use x509_parser::{
use crate::listener::{
acme::{
- AcmeProvider, ChallengeSettings, EabSettings, directory::LETS_ENCRYPT_PRODUCTION_DIRECTORY,
+ AcmeProvider, ChallengeSettings, EabSettings,
+ directory::{CABundle, LETS_ENCRYPT_PRODUCTION_DIRECTORY},
},
tls::AcmeProviders,
};
@@ -66,6 +67,31 @@ impl AcmeProviders {
.property_or_default(("acme", acme_id, "renew-before"), "30d")
.unwrap_or_else(|| Duration::from_secs(30 * 24 * 60 * 60));
+ let ca_bundle: CABundle = config
+ .value(("acme", acme_id, "ca_bundle"))
+ .map(|s| s.to_string())
+ .and_then(|path| {
+ let buf = std::fs::read(&path)
+ .map_err(|err| {
+ config.new_parse_error(
+ format!("acme.{acme_id}.ca_bundle"),
+ err.to_string(),
+ );
+ })
+ .ok()?;
+
+ reqwest::Certificate::from_pem_bundle(&buf)
+ .map_err(|err| {
+ config.new_parse_error(
+ format!("acme.{acme_id}.ca_bundle"),
+ err.to_string(),
+ );
+
+ err
+ })
+ .ok()
+ });
+
if directory.is_empty() {
config.new_parse_error(format!("acme.{acme_id}.directory"), "Missing property");
continue;
@@ -167,6 +193,7 @@ impl AcmeProviders {
contact,
challenge,
eab,
+ ca_bundle,
renew_before,
default,
) {
@@ -304,7 +331,7 @@ fn build_dns_updater(config: &mut Config, acme_id: &str) -> Option<DnsUpdater> {
.map_err(|err| {
config.new_build_error(
("acme", acme_id, "provider"),
- format!("Failed to create Desec DNS updater: {err}"),
+ format!("Failed to create OVH DNS updater: {err}"),
)
})
.ok(),
diff --git a/crates/common/src/listener/acme/directory.rs b/crates/common/src/listener/acme/directory.rs
index 21640cd6..bbefd7aa 100644
--- a/crates/common/src/listener/acme/directory.rs
+++ b/crates/common/src/listener/acme/directory.rs
@@ -20,6 +20,8 @@ use store::write::Archiver;
use trc::AddContext;
use trc::event::conv::AssertSuccess;
+pub type CABundle = Option<Vec<reqwest::Certificate>>;
+
pub const LETS_ENCRYPT_STAGING_DIRECTORY: &str =
"https://acme-staging-v02.api.letsencrypt.org/directory";
pub const LETS_ENCRYPT_PRODUCTION_DIRECTORY: &str =
@@ -31,6 +33,7 @@ pub struct Account {
pub key_pair: EcdsaKeyPair,
pub directory: Directory,
pub kid: String,
+ pub ca_bundle: CABundle,
}
#[derive(Debug, serde::Serialize)]
@@ -89,16 +92,23 @@ impl Account {
let body = sign(
&key_pair,
None,
- directory.nonce().await?,
+ directory.nonce(&provider.ca_bundle).await?,
&directory.new_account,
&payload,
)?;
- let response = https(&directory.new_account, Method::POST, Some(body)).await?;
+ let response = https(
+ &directory.new_account,
+ Method::POST,
+ Some(body),
+ &provider.ca_bundle,
+ )
+ .await?;
let kid = get_header(&response, "Location")?;
Ok(Account {
key_pair,
kid,
directory,
+ ca_bundle: provider.ca_bundle.clone(),
})
}
@@ -106,15 +116,16 @@ impl Account {
&self,
url: impl AsRef<str>,
payload: &str,
+ ca_bundle: &CABundle,
) -> trc::Result<(Option<String>, String)> {
let body = sign(
&self.key_pair,
Some(&self.kid),
- self.directory.nonce().await?,
+ self.directory.nonce(ca_bundle).await?,
url.as_ref(),
payload,
)?;
- let response = https(url.as_ref(), Method::POST, Some(body)).await?;
+ let response = https(url.as_ref(), Method::POST, Some(body), ca_bundle).await?;
let location = get_header(&response, "Location").ok();
let body = response
.text()
@@ -130,7 +141,9 @@ impl Account {
serde_json::to_string(&domains)
.map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).from_json_error(err))?
);
- let response = self.request(&self.directory.new_order, &payload).await?;
+ let response = self
+ .request(&self.directory.new_order, &payload, &self.ca_bundle)
+ .await?;
let url = response.0.ok_or(
trc::EventType::Acme(trc::AcmeEvent::Error)
.caused_by(trc::location!())
@@ -143,30 +156,30 @@ impl Account {
}
pub async fn auth(&self, url: impl AsRef<str>) -> trc::Result<Auth> {
- let response = self.request(url, "").await?;
+ let response = self.request(url, "", &self.ca_bundle).await?;
serde_json::from_str(&response.1)
.map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).from_json_error(err))
}
pub async fn challenge(&self, url: impl AsRef<str>) -> trc::Result<()> {
- self.request(&url, "{}").await.map(|_| ())
+ self.request(&url, "{}", &self.ca_bundle).await.map(|_| ())
}
pub async fn order(&self, url: impl AsRef<str>) -> trc::Result<Order> {
- let response = self.request(&url, "").await?;
+ let response = self.request(&url, "", &self.ca_bundle).await?;
serde_json::from_str(&response.1)
.map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).from_json_error(err))
}
pub async fn finalize(&self, url: impl AsRef<str>, csr: Vec<u8>) -> trc::Result<Order> {
let payload = format!("{{\"csr\":\"{}\"}}", URL_SAFE_NO_PAD.encode(csr));
- let response = self.request(&url, &payload).await?;
+ let response = self.request(&url, &payload, &self.ca_bundle).await?;
serde_json::from_str(&response.1)
.map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).from_json_error(err))
}
pub async fn certificate(&self, url: impl AsRef<str>) -> trc::Result<String> {
- Ok(self.request(&url, "").await?.1)
+ Ok(self.request(&url, "", &self.ca_bundle).await?.1)
}
pub fn http_proof(&self, challenge: &Challenge) -> trc::Result<Vec<u8>> {
@@ -218,9 +231,9 @@ pub struct Directory {
}
impl Directory {
- pub async fn discover(url: impl AsRef<str>) -> trc::Result<Self> {
+ pub async fn discover(url: impl AsRef<str>, ca_bundle: &CABundle) -> trc::Result<Self> {
serde_json::from_str(
- &https(url, Method::GET, None)
+ &https(url, Method::GET, None, ca_bundle)
.await?
.text()
.await
@@ -228,9 +241,9 @@ impl Directory {
)
.map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).from_json_error(err))
}
- pub async fn nonce(&self) -> trc::Result<String> {
+ pub async fn nonce(&self, ca_bundle: &CABundle) -> trc::Result<String> {
get_header(
- &https(&self.new_nonce.as_str(), Method::HEAD, None).await?,
+ &https(&self.new_nonce.as_str(), Method::HEAD, None, ca_bundle).await?,
"replay-nonce",
)
}
@@ -316,6 +329,7 @@ async fn https(
url: impl AsRef<str>,
method: Method,
body: Option<String>,
+ ca_bundle: &CABundle,
) -> trc::Result<Response> {
let url = url.as_ref();
let mut builder = reqwest::Client::builder()
@@ -329,6 +343,15 @@ async fn https(
);
}
+ match ca_bundle {
+ Some(certs) => {
+ for cert in certs {
+ builder = builder.add_root_certificate(cert.clone());
+ }
+ }
+ None => {}
+ };
+
let mut request = builder
.build()
.map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).from_http_error(err))?
diff --git a/crates/common/src/listener/acme/mod.rs b/crates/common/src/listener/acme/mod.rs
index a6d9b978..2674dd3a 100644
--- a/crates/common/src/listener/acme/mod.rs
+++ b/crates/common/src/listener/acme/mod.rs
@@ -16,7 +16,7 @@ use arc_swap::ArcSwap;
use dns_update::DnsUpdater;
use rustls::sign::CertifiedKey;
-use crate::Server;
+use crate::{Server, listener::acme::directory::CABundle};
use self::directory::{Account, ChallengeType};
@@ -27,6 +27,7 @@ pub struct AcmeProvider {
pub contact: Vec<String>,
pub challenge: ChallengeSettings,
pub eab: Option<EabSettings>,
+ pub ca_bundle: CABundle,
renew_before: chrono::Duration,
account_key: ArcSwap<Vec<u8>>,
default: bool,
@@ -64,6 +65,7 @@ impl AcmeProvider {
contact: Vec<String>,
challenge: ChallengeSettings,
eab: Option<EabSettings>,
+ ca_bundle: CABundle,
renew_before: Duration,
default: bool,
) -> trc::Result<Self> {
@@ -85,6 +87,7 @@ impl AcmeProvider {
account_key: Default::default(),
challenge,
eab,
+ ca_bundle,
default,
})
}
@@ -153,6 +156,7 @@ impl Clone for AcmeProvider {
renew_before: self.renew_before,
account_key: ArcSwap::from_pointee(self.account_key.load().as_ref().clone()),
eab: self.eab.clone(),
+ ca_bundle: self.ca_bundle.clone(),
default: self.default,
}
}
diff --git a/crates/common/src/listener/acme/order.rs b/crates/common/src/listener/acme/order.rs
index 6def86a4..8ba3b185 100644
--- a/crates/common/src/listener/acme/order.rs
+++ b/crates/common/src/listener/acme/order.rs
@@ -85,7 +85,7 @@ impl Server {
}
async fn order(&self, provider: &AcmeProvider) -> trc::Result<Vec<u8>> {
- let directory = Directory::discover(&provider.directory_url).await?;
+ let directory = Directory::discover(&provider.directory_url, &provider.ca_bundle).await?;
let account = Account::create_with_keypair(directory, provider).await?;
let mut params = CertificateParams::new(provider.domains.clone());

View file

@ -59,6 +59,33 @@ in
];
})
(import ../../modules/stalwart.nix {
enableNginx = true;
domain = "pre7780.dn";
adminPassFile = config.sops.secrets."stalwart/adminPassword".path;
dbPassFile = config.sops.secrets."stalwart/db".path;
acmeConf = {
directory = "https://ca.net.dn/acme/acme/directory";
ca_bundle = "${"" + ../../extra/ca.crt}";
challenge = "dns-01";
origin = "pre7780.dn";
contact = "admin@pre7780.dn";
domains = [
"pre7780.dn"
"mx1.pre7780.dn"
];
default = true;
provider = "rfc2136-tsig";
host = "10.0.0.1";
renew-before = "1d";
port = 5359;
cache = "${config.services.stalwart-mail.dataDir}/acme";
key = "stalwart";
tsig-algorithm = "hmac-sha512";
secret = "%{file:${config.sops.secrets."stalwart/tsig".path}}%";
};
})
../../modules/davinci-resolve.nix
../../modules/webcam.nix
../../modules/postgresql.nix

View file

@ -1,13 +1,20 @@
{ ... }:
{ config, ... }:
{
networking.firewall.allowedTCPPorts = [
443
80
];
security.acme = {
acceptTerms = true;
defaults = {
validMinDays = 2;
server = "https://10.0.0.1:${toString 8443}/acme/acme/directory";
server = "https://ca.net.dn/acme/acme/directory";
renewInterval = "daily";
email = "danny@net.dn";
webroot = "/var/lib/acme/acme-challenge";
dnsProvider = "pdns";
dnsPropagationCheck = false;
environmentFile = config.sops.secrets."acme/pdns".path;
};
};

View file

@ -8,6 +8,12 @@ openldap:
adminPassword: ENC[AES256_GCM,data:jEGuzgs5QTWfdyJenC3t3g==,iv:StfFOcvbDapnma6eAlpaGiBWnqiD3I/wfQsMBzufol0=,tag:892q7N4KrsSQoZYGy6CQrA==,type:str]
lam:
env: ENC[AES256_GCM,data:f1LlC/VvilH8o2Ra7MrSHsMEGlGw3LOV2O9JJf9f,iv:u7cXM8n3jJeLBfxXtA0QMyijBqTcC+yJeW/OO9JuZMI=,tag:QL5FkcCPI5Gxudi0NmCZWg==,type:str]
stalwart:
adminPassword: ENC[AES256_GCM,data:6tUL7b2s3gLtF4Ors9CgYQ==,iv:9UQowgXKr9HR/poELP6SZijp3c2HVTHzEfwf1tZI/3w=,tag:KIOiYEwLsZLH31E2Xb478A==,type:str]
tsig: ENC[AES256_GCM,data:wxsM/dbkW2fNf86b6TsLRNAce19h7mBEuSzFT84aIlaVZA/S29g1U4/CAwD4b+h/XfBgpZQCJf/9yT3yo6dbGAIAk5UgjV2cNY9pO1/uF1T6xoKDgfRZxA==,iv:9BvP8vQkTTEaNgYUPfQcfEMcWqDyD045EPBr7NyHmO4=,tag:coBBAe62kpe/L0S6V8NhXg==,type:str]
db: ENC[AES256_GCM,data:ZRZ2ZzUotYMe2GfkMS7o7dz0aGg=,iv:ys6ogueueESp0y6A+hUG9zTnqmCVobuIzyqA4WVtewo=,tag:p74G+8XhMcpgDnIfh1aXTg==,type:str]
acme:
pdns: ENC[AES256_GCM,data:+InGSnaGIFVtDRlVltzWbZfquzodHUQrPeMRBnVNB9mrajlKr5dFK6DD8dXAvN7UjZFBfrgZefOPkmLR2ncLXGOV2Kl7jorVw50Y0f0iKl7mqwHaZKaQdk7cpGDkCrt/LvfbP9x7gVrs6pQpsU+c/P5rbBLRyejchh/WtiyzgowYIJxYohggeG09+l7YI3FR6U5wiymIRISpNBGEhwG0q17qdAhdtc49qP/K,iv:JcSlxAwHwU528S7iSpAnSbUZw7iO+LMjR3qGwRHp+Zk=,tag:twf2WOQb/yZ3GtN/hlikMA==,type:str]
sops:
age:
- recipient: age1uvsvf5ljaezh5wze32p685kfentyle0l2mvysc67yvgct2h4850qqph9lv
@ -19,7 +25,7 @@ sops:
MEdmWkFwNXZoR1ZVRnQ0aWlkYzZwSmsK0EFecUIdqlDKX08oRCoDQQ3QCX1wzb8w
lghDJhWlfuKr+X24GoE4UK04aJVLqVMRRI4BJW+LQXeHS+dWKu3mQA==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2025-06-15T08:18:50Z"
mac: ENC[AES256_GCM,data:sq+/fpOeNO5wn9S1kFqzRy6xCOVkSBcAkral7MTn4UxRebBDa78KF76Nsba0+o5bzwCchoGl/TC6vySIzGq8FUYwd1tQ9nH5DlqYBVVRgRlKLRyhxXf14BTyYgzHzFuRWdFyY8i4j0flZtlDHk4dVQrE4OhHvhLQ2Zvet5HQ20I=,iv:qoPZ+8tAHJxcR53M2PNwukYgdguSRrAVB+FtKYbf+aM=,tag:FYaPzh6o0ZI27Ul5jEhgVg==,type:str]
lastmodified: "2025-09-16T04:39:12Z"
mac: ENC[AES256_GCM,data:yRVAJz73AqlBm6fxeTehfSqlTLyRYIsPjC/5igpnGC8URUiK66SUtHJSE3196AaPV+CWJrxrXfNWoCmZsP85Rr5V9nw31ZF1boaAc0YzRQBxVmBBlAK7+9Z5KADShAetYNwk9qtCrXd6S8mCwmZjNJaN/Rthy3hchxzAB0/79R4=,iv:QeNUZfmnCx4QF/0wjU/JJRu6umNFC/weW2BJx+7OaPo=,tag:KsityLnPYhugFL4c6wrs6Q==,type:str]
unencrypted_suffix: _unencrypted
version: 3.10.2

View file

@ -15,6 +15,36 @@
group = config.services.dovecot2.group;
mode = "0660";
};
"stalwart/adminPassword" =
let
inherit (config.users.users.stalwart-mail) name group;
in
lib.mkIf config.services.stalwart-mail.enable {
inherit group;
owner = name;
};
"stalwart/tsig" =
let
inherit (config.users.users.stalwart-mail) name group;
in
lib.mkIf config.services.stalwart-mail.enable {
inherit group;
owner = name;
};
"stalwart/db" =
let
inherit (config.users.users.stalwart-mail) name group;
in
lib.mkIf config.services.stalwart-mail.enable {
inherit group;
owner = name;
};
"acme/pdns" = {
mode = "0660";
owner = "acme";
group = "acme";
};
};
};
}

View file

@ -64,6 +64,14 @@
locations."/".proxyPass = "http://10.0.0.130:8001/phone.html";
};
"ca.net.dn" = {
enableACME = true;
forceSSL = true;
locations."/" = {
proxyPass = "https://10.0.0.1:8443/";
};
};
};
};
}

View file

@ -33,11 +33,20 @@ sops:
- recipient: age1z6f643a6vqm7cqh6fna5dhmxfkgwxgqy8kg9s0vf9uxhaswtngtspmqsjw
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuKzJObXlPVUJzUkEyZXlV
Q0tEbzBPTy9kUXIwVmJkckUyWklUMzhCcTE0Ckh3bXIwRkpESTJYeTBPMGhQYk9y
L2NQTWFuMWVqYzJHZGhTaHpDRE5CRGMKLS0tIEsybHdPMk9JeEM2cXFwdlpOeXRj
Qm0wbmNGZDZwZlNTOVl0WVh5RXNxK2cK1Fwbgl5kKAFyrIIhBP+X4ZKFS4Xl39QY
11qkglNgro/JBFJ/W7Hj5wtEd8QToiJM1RW0lQaI25sneQ2v6L5pDA==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuYWpiZ0h3VURrcW0rV0Vj
SFJwMlRUMHUyS1FGTEo3cHZJc1Z6a3FWbmtRCkdoZXhwOGJQNlV2dU8wRFRMUHVv
QzhxU3RiVHl5UVpUNk10S2VRVy95OHMKLS0tIE9zbUNUU3ZINU1JNGtmd2trS2tI
d3YxREtHcTBJYU1sNU9vMGZTUGh6NXMKtGKMnnamCAeftkQ0+Ygb/yg1NdyKDz1W
UjYvW2PYKzkx8IWmIgzdAI3fWDOiE7tmBTMlX9C3/2PKR6dCc/a+SQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1uvsvf5ljaezh5wze32p685kfentyle0l2mvysc67yvgct2h4850qqph9lv
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxc3pna3R3aG85bmt2WERa
aG9TaDBKTlNMTUVwaFlIdkV0UmFJQStYSHdvCmNuYWJpN2M3QjRkV2s0MHJ4TzZP
ZkhKc0xPUFBrblVFR1U4SUdjYzQ2cm8KLS0tIDVuNW9tRGoxanVKOUJYa2QwNFNz
OTRiU0cxeXp5K1FjaWRGTnBHcnpUYmcKVVlueEj/DELe9Xi9iaBddpPPRmoUmD48
wyjtlvKzS20zishE/D7GkHZ2ZdNsLD3AOnYZ6r6ATAndssC2YT/SXA==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2025-08-29T07:40:00Z"
mac: ENC[AES256_GCM,data:QeQ5NOrcq3uNmt+MiVF+Jr3JWWBNGPw5A8pSdd1WR426WWqHTRP7NHAaVbS3st9VSmoYY5NI6JKeizuAq/NCvzOZL3Idy9mP+3HD9VZwn1GNSEGfhn+KZT02AY0JHq29KxcZlAYiWZOL4p+blG2aWfGm9+zy1GHoEXoo3OVhaEg=,iv:8uIoOE0ZJZYGZoQaskCXQKr7vl6wjsmJ4iudhvtgqtY=,tag:fRLGalP92dDyF8q+zT97BQ==,type:str]

View file

@ -4,7 +4,6 @@
username,
...
}:
let
inherit username;
@ -13,10 +12,10 @@ let
sshPortsString = builtins.concatStringsSep ", " (builtins.map (p: builtins.toString p) sshPorts);
personal = {
inherit (config.networking) domain;
ip = "10.0.0.1/24";
interface = "wg0";
port = 51820;
domain = config.networking.domain;
range = "10.0.0.0/24";
full = "10.0.0.1/25";
restrict = "10.0.0.128/25";
@ -160,11 +159,13 @@ in
kube.port
25565
kube.masterAPIServerPort
5359
];
allowedTCPPorts = sshPorts ++ [
53
25565
kube.masterAPIServerPort
5359
];
};
@ -237,8 +238,7 @@ in
listenPort = personal.port;
privateKeyFile = config.sops.secrets."wireguard/privateKey".path;
peers = builtins.map (r: {
publicKey = r.publicKey;
allowedIPs = r.allowedIPs;
inherit (r) publicKey allowedIPs;
}) (fullRoute ++ meshRoute);
};
@ -254,7 +254,11 @@ in
extraHosts = "${kube.masterIP} ${kube.masterHostname}";
};
services.postgresql = {
services = {
dbus.enable = true;
blueman.enable = true;
postgresql = {
enable = lib.mkDefault true;
authentication = ''
host powerdnsadmin powerdnsadmin 127.0.0.1/32 trust
@ -275,10 +279,6 @@ in
];
};
services = {
dbus.enable = true;
blueman.enable = true;
openssh = {
enable = true;
ports = sshPorts;
@ -293,6 +293,7 @@ in
enable = true;
extraConfig = ''
launch=gpgsql
loglevel=6
webserver-password=$WEB_PASSWORD
api=yes
api-key=$WEB_PASSWORD
@ -302,6 +303,8 @@ in
webserver=yes
webserver-port=8081
local-port=5359
dnsupdate=yes
allow-dnsupdate-from=10.0.0.0/24
'';
secretFile = config.sops.secrets.powerdns.path;
};
@ -310,6 +313,7 @@ in
enable = true;
forwardZones = {
"${config.networking.domain}." = "127.0.0.1:5359";
"pre7780.dn." = "127.0.0.1:5359";
};
forwardZonesRecurse = {
"." = "8.8.8.8";
@ -380,11 +384,16 @@ in
};
};
systemd.services.raspamd-trainer = {
after = [ "pdns-recursor.service" ];
};
services.nginx.virtualHosts = {
"powerdns.${config.networking.domain}" = {
enableACME = true;
forceSSL = true;
locations."/".proxyPass = "http://localhost:8000";
locations."/api".proxyPass = "http://127.0.0.1:8081";
locations."/".proxyPass = "http://127.0.0.1:8000";
};
"uptime.${config.networking.domain}" = {

View file

@ -41,7 +41,6 @@ in
postRun = ''
systemctl restart postfix.service
systemctl restart dovecot.service
systemctl restart rspamd-trainer.service
'';
};
"${cfg.domain}" = {

118
system/modules/stalwart.nix Normal file
View file

@ -0,0 +1,118 @@
{
adminPassFile,
dbPassFile,
domain ? null,
acmeConf ? null,
enableNginx ? true,
}:
{
config,
lib,
...
}:
let
inherit (lib) mkIf;
in
{
services.postgresql = {
enable = true;
ensureDatabases = [
"stalwart"
];
ensureUsers = [
{
name = "stalwart";
ensureDBOwnership = true;
}
];
};
services.stalwart-mail = {
enable = true;
openFirewall = true;
settings = {
server = {
hostname = if (domain != null) then "mx1.${domain}" else config.networking.fqdn;
auto-ban.scan.rate = "1000/1d";
tls = {
enable = true;
implicit = true;
};
listener = {
smtp = {
protocol = "smtp";
bind = "[::]:25";
};
submissions = {
protocol = "smtp";
bind = "[::]:465";
tls.implicit = true;
};
imaps = {
protocol = "imap";
bind = "[::]:993";
tls.implicit = true;
};
management = {
protocol = "http";
bind = [ "127.0.0.1:8080" ];
};
};
};
lookup.default = {
hostname = "mx1.${domain}";
domain = "${domain}";
};
acme."step-ca" = mkIf (acmeConf != null) acmeConf;
session.auth = {
mechanisms = "[plain]";
directory = "'in-memory'";
require = true;
allow-plain-text = true;
};
storage.data = "db";
store."db" = {
type = "postgresql";
host = "localhost";
port = 5432;
database = "stalwart";
user = "stalwart";
password = "%{file:${dbPassFile}}%";
};
directory = {
"imap".lookup.domains = [ domain ];
"in-memory" = {
type = "memory";
principals = [
{
name = "admin";
class = "admin";
secret = "%{file:${adminPassFile}}%";
email = [ "admin@${domain}" ];
}
];
};
};
authentication.fallback-admin = {
user = "admin";
secret = "%{file:${adminPassFile}}%";
};
tracer."stdout" = {
enable = true;
type = "console";
level = "debug";
};
};
};
services.nginx = mkIf enableNginx {
enable = true;
virtualHosts = {
"mail.${domain}" = {
locations."/".proxyPass = "http://127.0.0.1:8080";
enableACME = true;
forceSSL = true;
};
};
};
}