Self-host your own digital island

better secrets

+1 -4
modules/default.nix
···
username = mkOption {
type = str;
};
-
secretsDir = mkOption {
-
type = path;
-
};
serverIpv4 = mkOption {
type = str;
};
···
config = {
security.acme.defaults.email = "${config.eilean.username}@${config.networking.domain}";
-
networking.firewall.allowedTCPPorts = lib.mkIf config.services.nginx.enable [
+
networking.firewall.allowedTCPPorts = mkIf config.services.nginx.enable [
80 # HTTP
443 # HTTPS
];
+6 -5
modules/dns.nix
···
{ config, lib, ... }:
+
with lib;
let cfg = config.eilean; in
{
options.eilean.dns = {
-
enable = lib.mkEnableOption "dns";
-
nameservers = lib.mkOption {
-
type = lib.types.listOf lib.types.string;
+
enable = mkEnableOption "dns";
+
nameservers = mkOption {
+
type = types.listOf types.str;
default = [ "ns1" "ns2" ];
};
};
-
config.eilean.services.dns = lib.mkIf cfg.dns.enable {
+
config.eilean.services.dns = mkIf cfg.dns.enable {
enable = true;
zones.${config.networking.domain} = {
-
soa.serial = lib.mkDefault 0;
+
soa.serial = mkDefault 0;
records = builtins.concatMap (ns: [
{
name = "@";
+27 -22
modules/gitea.nix
···
{ pkgs, config, lib, ... }:
+
with lib;
let
cfg = config.eilean;
domain = config.networking.domain;
in {
options.eilean.gitea = {
-
enable = lib.mkEnableOption "gitea";
-
sshPort = lib.mkOption {
-
type = lib.types.int;
+
enable = mkEnableOption "gitea";
+
sshPort = mkOption {
+
type = types.int;
default = 3001;
};
+
databasePasswordFile = mkOption {
+
type = types.nullOr types.path;
+
default = null;
+
};
};
-
config = lib.mkIf cfg.gitea.enable {
+
config = mkIf cfg.gitea.enable {
services.nginx = {
enable = true;
recommendedProxySettings = true;
···
enable = true;
user = "git";
appName = "git | ${domain}";
-
mailerPasswordFile = "${config.eilean.secretsDir}/email-pswd-unhashed";
+
mailerPasswordFile = cfg.mailserver.systemAccountPasswordFile;
settings = {
server = {
ROOT_URL = "https://git.${domain}/";
···
FROM = "git@${domain}";
MAILER_TYPE = "smtp";
HOST = "mail.${domain}:465";
-
USER = "misc@${domain}";
+
USER = "system@${domain}";
IS_TLS_ENABLED = true;
};
repository.DEFAULT_BRANCH = "main";
···
};
database = {
type = "postgres";
-
passwordFile = "${config.eilean.secretsDir}/gitea-db";
+
passwordFile = cfg.gitea.databasePasswordFile;
user = "git";
name = "git";
#createDatabase = true;
···
# https://github.com/NixOS/nixpkgs/issues/103446
systemd.services.gitea.serviceConfig = {
ReadWritePaths = [ "/var/lib/postfix/queue/maildrop" ];
-
NoNewPrivileges = lib.mkForce false;
-
PrivateDevices = lib.mkForce false;
-
PrivateUsers = lib.mkForce false;
-
ProtectHostname = lib.mkForce false;
-
ProtectClock = lib.mkForce false;
-
ProtectKernelTunables = lib.mkForce false;
-
ProtectKernelModules = lib.mkForce false;
-
ProtectKernelLogs = lib.mkForce false;
-
RestrictAddressFamilies = lib.mkForce [ ];
-
LockPersonality = lib.mkForce false;
-
MemoryDenyWriteExecute = lib.mkForce false;
-
RestrictRealtime = lib.mkForce false;
-
RestrictSUIDSGID = lib.mkForce false;
-
SystemCallArchitectures = lib.mkForce "";
-
SystemCallFilter = lib.mkForce [];
+
NoNewPrivileges = mkForce false;
+
PrivateDevices = mkForce false;
+
PrivateUsers = mkForce false;
+
ProtectHostname = mkForce false;
+
ProtectClock = mkForce false;
+
ProtectKernelTunables = mkForce false;
+
ProtectKernelModules = mkForce false;
+
ProtectKernelLogs = mkForce false;
+
RestrictAddressFamilies = mkForce [ ];
+
LockPersonality = mkForce false;
+
MemoryDenyWriteExecute = mkForce false;
+
RestrictRealtime = mkForce false;
+
RestrictSUIDSGID = mkForce false;
+
SystemCallArchitectures = mkForce "";
+
SystemCallFilter = mkForce [];
};
eilean.dns.enable = true;
+2 -1
modules/headscale.nix
···
{ pkgs, config, lib, ... }:
+
with lib;
let
cfg = config.eilean;
in {
···
};
};
-
config = lib.mkIf cfg.headscale.enable {
+
config = mkIf cfg.headscale.enable {
# To set up:
# `headscale namespaces create <namespace_name>`
# To add a node:
+17 -20
modules/mailserver.nix
···
-
{ config, lib, ... }:
+
{ pkgs, config, lib, ... }:
+
with lib;
let
cfg = config.eilean;
domain = config.networking.domain;
in {
-
options.eilean.mailserver.enable = lib.mkEnableOption "mailserver";
+
options.eilean.mailserver = {
+
enable = mkEnableOption "mailserver";
+
systemAccountPasswordFile = mkOption {
+
type = types.nullOr types.str;
+
default = null;
+
};
+
};
-
config = lib.mkIf cfg.mailserver.enable {
+
config = mkIf cfg.mailserver.enable {
mailserver = {
enable = true;
fqdn = "mail.${domain}";
domains = [ "${domain}" ];
-
# A list of all login accounts. To create the password hashes, use
-
# nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
loginAccounts = {
-
"${cfg.username}@${domain}" = {
-
hashedPasswordFile = "${config.eilean.secretsDir}/email-pswd";
-
aliases = [
-
"dns@${domain}"
-
"postmaster@${domain}"
-
];
-
};
-
"misc@${domain}" = {
-
hashedPasswordFile = "${config.eilean.secretsDir}/email-pswd";
-
aliases = [
-
"git@${domain}"
-
"mastodon@${domain}"
-
];
-
catchAll = [ "${domain}" ];
-
};
+
"system@${domain}" = {
+
passwordFile = cfg.mailserver.systemAccountPasswordFile;
+
aliases = [
+
(mkIf cfg.gitea.enable "git@${domain}")
+
(mkIf cfg.mastodon.enable "mastodon@${domain}")
+
];
+
};
};
# Use Let's Encrypt certificates. Note that this needs to set up a stripped
+4 -4
modules/mailserver/common.nix
···
else throw "Error: Certificate Scheme must be in { 1, 2, 3 }";
passwordFiles = let
-
mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash;
+
mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password" hash;
in
lib.mapAttrs (name: value:
-
if value.hashedPasswordFile == null then
-
builtins.toString (mkHashFile name value.hashedPassword)
-
else value.hashedPasswordFile) cfg.loginAccounts;
+
if value.passwordFile == null then
+
builtins.toString (mkHashFile name value.password)
+
else value.passwordFile) cfg.loginAccounts;
}
+7 -15
modules/mailserver/default.nix
···
description = "Username";
};
-
hashedPassword = mkOption {
+
password = mkOption {
type = with types; nullOr str;
default = null;
example = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
description = ''
-
The user's hashed password. Use `htpasswd` as follows
-
-
```
-
nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
-
```
+
The password.
Warning: this is stored in plaintext in the Nix store!
-
Use `hashedPasswordFile` instead.
+
Use `passwordFile` instead.
'';
};
-
hashedPasswordFile = mkOption {
+
passwordFile = mkOption {
type = with types; nullOr path;
default = null;
example = "/run/keys/user1-passwordhash";
description = ''
-
A file containing the user's hashed password. Use `htpasswd` as follows
-
-
```
-
nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
-
```
+
A file containing the user's password.
'';
};
···
}));
example = {
user1 = {
-
hashedPassword = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
+
password = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
};
user2 = {
-
hashedPassword = "$6$oE0ZNv2n7Vk9gOf$9xcZWCCLGdMflIfuA0vR1Q1Xblw6RZqPrP94mEit2/81/7AKj2bqUai5yPyWE.QYPyv6wLMHZvjw3Rlg7yTCD/";
+
password = "$6$oE0ZNv2n7Vk9gOf$9xcZWCCLGdMflIfuA0vR1Q1Xblw6RZqPrP94mEit2/81/7AKj2bqUai5yPyWE.QYPyv6wLMHZvjw3Rlg7yTCD/";
};
};
description = ''
+2 -1
modules/mailserver/dovecot.nix
···
cat <<EOF > ${passwdFile}
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
-
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}:${builtins.toString cfg.vmailUID}:${builtins.toString cfg.vmailUID}::${cfg.mailDirectory}:/run/current-system/sw/bin/nologin:"
+
let passwordHash = ''$(head -n 1 ${passwordFiles."${name}"} | ${pkgs.findutils}/bin/xargs --null ${pkgs.apacheHttpd}/bin/htpasswd -nbB "" | cut -d: -f2)''; in
+
"${name}:${passwordHash}:${builtins.toString cfg.vmailUID}:${builtins.toString cfg.vmailUID}::${cfg.mailDirectory}:/run/current-system/sw/bin/nologin:"
+ (if lib.isString value.quota
then "userdb_quota_rule=*:storage=${value.quota}"
else "")
+4 -4
modules/mailserver/users.nix
···
config = lib.mkIf enable {
# assert that all accounts provide a password
assertions = (map (acct: {
-
assertion = (acct.hashedPassword != null || acct.hashedPasswordFile != null);
-
message = "${acct.name} must provide either a hashed password or a password hash file";
+
assertion = (acct.password != null || acct.passwordFile != null);
+
message = "${acct.name} must provide either a password or a password file";
}) (lib.attrValues loginAccounts));
# warn for accounts that specify both password and file
warnings = (map
-
(acct: "${acct.name} specifies both a password hash and hash file; hash file will be used")
+
(acct: "${acct.name} specifies both a password and password file; password file will be used")
(lib.filter
-
(acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null))
+
(acct: (acct.password != null && acct.passwordFile != null))
(lib.attrValues loginAccounts)));
# set the vmail gid to a specific value
+7 -4
modules/mastodon.nix
···
{ pkgs, config, lib, ... }:
+
with lib;
let
cfg = config.eilean;
domain = config.networking.domain;
in {
-
options.eilean.mastodon.enable = lib.mkEnableOption "mastodon";
+
options.eilean.mastodon = {
+
enable = mkEnableOption "mastodon";
+
};
-
config = lib.mkIf cfg.mastodon.enable {
+
config = mkIf cfg.mastodon.enable {
services.mastodon = {
enable = true;
enableUnixSocket = false;
···
streamingProcesses = 3;
smtp = {
#createLocally = false;
-
user = "misc@${domain}";
+
user = "system@${domain}";
port = 465;
host = "mail.${domain}";
authenticate = true;
-
passwordFile = "${config.eilean.secretsDir}/email-pswd-unhashed";
+
passwordFile = cfg.mailserver.systemAccountPasswordFile;
fromAddress = "mastodon@${domain}";
};
extraConfig = {
+38 -10
modules/matrix.nix
···
{ config, pkgs, lib, ... }:
-
let cfg = config.eilean; in
+
with lib;
+
let
+
cfg = config.eilean;
+
turnSharedSecretFile = "/run/matrix-synapse/turn-shared-secret";
+
in
{
options.eilean.matrix = {
-
enable = lib.mkEnableOption "matrix";
-
turn = lib.mkOption {
-
type = lib.types.bool;
+
enable = mkEnableOption "matrix";
+
turn = mkOption {
+
type = types.bool;
default = true;
};
+
registrationSecretFile = mkOption {
+
type = types.nullOr types.str;
+
default = null;
+
};
};
-
config = lib.mkIf cfg.matrix.enable {
+
config = mkIf cfg.matrix.enable {
services.postgresql.enable = true;
services.postgresql.package = pkgs.postgresql_13;
services.postgresql.initialScript = pkgs.writeText "synapse-init.sql" ''
···
services.matrix-synapse = {
enable = true;
-
settings = lib.mkMerge [
+
settings = mkMerge [
{
server_name = config.networking.domain;
enable_registration = true;
registration_requires_token = true;
-
registration_shared_secret_path = "${config.eilean.secretsDir}/matrix-shared-secret";
+
registration_shared_secret_path = cfg.matrix.registrationSecretFile;
listeners = [
{
port = 8008;
···
];
max_upload_size = "100M";
}
-
(lib.mkIf cfg.matrix.turn {
+
(mkIf cfg.matrix.turn {
turn_uris = with config.services.coturn; [
"turn:${realm}:3478?transport=udp"
"turn:${realm}:3478?transport=tcp"
···
turn_user_lifetime = "1h";
})
];
-
extraConfigFiles = [ "${config.eilean.secretsDir}/matrix-turn-shared-secret" ];
+
extraConfigFiles = mkIf cfg.matrix.turn (
+
[ turnSharedSecretFile ]
+
);
};
-
eilean.turn.enable = lib.mkIf cfg.matrix.turn true;
+
systemd.services = mkIf cfg.matrix.turn {
+
matrix-synapse-turn-shared-secret-generator = {
+
description = "Generate matrix synapse turn shared secret config file";
+
script = ''
+
mkdir -p "$(dirname '${turnSharedSecretFile}')"
+
echo "turn_shared_secret: $(cat '${cfg.turn.secretFile}')" > '${turnSharedSecretFile}'
+
chmod 770 '${turnSharedSecretFile}'
+
chown ${config.systemd.services.matrix-synapse.serviceConfig.User}:${config.systemd.services.matrix-synapse.serviceConfig.Group} '${turnSharedSecretFile}'
+
'';
+
serviceConfig.Type = "oneshot";
+
serviceConfig.RemainAfterExit = true;
+
};
+
"matrix-synapse" = {
+
after = [ "matrix-synapse-turn-shared-secret-generator.service" ];
+
requires = [ "matrix-synapse-turn-shared-secret-generator.service" ];
+
};
+
};
+
+
eilean.turn.enable = mkIf cfg.matrix.turn true;
eilean.dns.enable = true;
eilean.services.dns.zones.${config.networking.domain}.records = [
+2 -3
modules/services/dns/default.nix
···
{ pkgs, config, lib, ... }:
with lib;
-
let
zoneOptions.options = {
ttl = mkOption {
···
imports = [ ./bind.nix ];
options.eilean.services.dns = {
-
enable = lib.mkEnableOption "DNS server";
+
enable = mkEnableOption "DNS server";
server = mkOption {
type = types.enum [ "bind" ];
default = "bind";
···
};
};
-
config.networking.firewall = lib.mkIf config.eilean.services.dns.openFirewall {
+
config.networking.firewall = mkIf config.eilean.services.dns.openFirewall {
allowedTCPPorts = [ 53 ];
allowedUDPPorts = [ 53 ];
};
+10 -3
modules/turn.nix
···
{ config, pkgs, lib, ... }:
+
with lib;
let
cfg = config.eilean;
domain = config.networking.domain;
in
{
-
options.eilean.turn.enable = lib.mkEnableOption "TURN server";
+
options.eilean.turn = {
+
enable = mkEnableOption "TURN server";
+
secretFile = mkOption {
+
type = types.nullOr types.str;
+
default = null;
+
};
+
};
-
config = lib.mkIf cfg.turn.enable {
+
config = mkIf cfg.turn.enable {
services.coturn = rec {
enable = true;
no-cli = true;
no-tcp-relay = true;
secure-stun = true;
use-auth-secret = true;
-
static-auth-secret-file = "${config.eilean.secretsDir}/coturn";
+
static-auth-secret-file = "${cfg.turn.secretFile}";
realm = "turn.${domain}";
relay-ips = with config.eilean; [
serverIpv4
+5 -2
modules/wireguard/default.nix
···
{ pkgs, config, lib, ... }:
with lib;
-
let cfg = config.wireguard; in
{
options.wireguard = {
···
type = with types; nullOr int;
default = null;
};
+
privateKeyFile = mkOption {
+
type = types.nullOr types.str;
+
default = null;
+
};
};
};
in mkOption {
···
[ "${cfg.hosts."${hostName}".ip}/24" ]
else [ ];
listenPort = 51820;
-
privateKeyFile = "${config.eilean.secretsDir}/wireguard-key-${hostName}";
+
privateKeyFile = cfg.hosts."${hostName}".privateKeyFile;
peers =
let
serverPeers = attrsets.mapAttrsToList
-2
template/configuration.nix
···
# mastodon.enable = true;
# gitea.enable = true;
# headscale.enable = true;
-
-
# secretsDir = "/secrets";
};
# This value determines the NixOS release from which the default