push sheeet
Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s

This commit is contained in:
Dark Steveneq
2025-10-09 14:15:47 +02:00
commit 646b892680
49168 changed files with 5897842 additions and 0 deletions

View File

@@ -0,0 +1,291 @@
{
config,
options,
pkgs,
lib,
...
}:
let
inherit (lib)
concatStringsSep
literalExpression
makeLibraryPath
mkEnableOption
mkForce
mkIf
mkOption
mkPackageOption
mkRemovedOptionModule
optional
types
;
cfg = config.services.aesmd;
opt = options.services.aesmd;
sgx-psw = cfg.package;
configFile =
with cfg.settings;
pkgs.writeText "aesmd.conf" (
concatStringsSep "\n" (
optional (whitelistUrl != null) "whitelist url = ${whitelistUrl}"
++ optional (proxy != null) "aesm proxy = ${proxy}"
++ optional (proxyType != null) "proxy type = ${proxyType}"
++ optional (defaultQuotingType != null) "default quoting type = ${defaultQuotingType}"
++
# Newline at end of file
[ "" ]
)
);
in
{
imports = [
(mkRemovedOptionModule [ "services" "aesmd" "debug" ] ''
Enable debug mode by overriding the aesmd package directly:
services.aesmd.package = pkgs.sgx-psw.override { debug = true; };
'')
];
options.services.aesmd = {
enable = mkEnableOption "Intel's Architectural Enclave Service Manager (AESM) for Intel SGX";
package = mkPackageOption pkgs "sgx-psw" { };
environment = mkOption {
type = with types; attrsOf str;
default = { };
description = "Additional environment variables to pass to the AESM service.";
# Example environment variable for `sgx-azure-dcap-client` provider library
example = {
AZDCAP_COLLATERAL_VERSION = "v2";
AZDCAP_DEBUG_LOG_LEVEL = "INFO";
};
};
quoteProviderLibrary = mkOption {
type = with types; nullOr path;
default = null;
example = literalExpression "pkgs.sgx-azure-dcap-client";
description = "Custom quote provider library to use.";
};
settings = mkOption {
description = "AESM configuration";
default = { };
type = types.submodule {
options.whitelistUrl = mkOption {
type = with types; nullOr str;
default = null;
example = "http://whitelist.trustedservices.intel.com/SGX/LCWL/Linux/sgx_white_list_cert.bin";
description = "URL to retrieve authorized Intel SGX enclave signers.";
};
options.proxy = mkOption {
type = with types; nullOr str;
default = null;
example = "http://proxy_url:1234";
description = "HTTP network proxy.";
};
options.proxyType = mkOption {
type =
with types;
nullOr (enum [
"default"
"direct"
"manual"
]);
default = if (cfg.settings.proxy != null) then "manual" else null;
defaultText = literalExpression ''
if (config.${opt.settings}.proxy != null) then "manual" else null
'';
example = "default";
description = ''
Type of proxy to use. The `default` uses the system's default proxy.
If `direct` is given, uses no proxy.
A value of `manual` uses the proxy from
{option}`services.aesmd.settings.proxy`.
'';
};
options.defaultQuotingType = mkOption {
type =
with types;
nullOr (enum [
"ecdsa_256"
"epid_linkable"
"epid_unlinkable"
]);
default = null;
example = "ecdsa_256";
description = "Attestation quote type.";
};
};
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = !(config.boot.specialFileSystems."/dev".options ? "noexec");
message = "SGX requires exec permission for /dev";
}
];
hardware.cpu.intel.sgx.provision.enable = true;
# Make sure the AESM service can find the SGX devices until
# https://github.com/intel/linux-sgx/issues/772 is resolved
# and updated in nixpkgs.
hardware.cpu.intel.sgx.enableDcapCompat = mkForce true;
systemd.services.aesmd =
let
storeAesmFolder = "${sgx-psw}/aesm";
# Hardcoded path AESM_DATA_FOLDER in psw/ae/aesm_service/source/oal/linux/aesm_util.cpp
aesmDataFolder = "/var/opt/aesmd/data";
in
{
description = "Intel Architectural Enclave Service Manager";
wantedBy = [ "multi-user.target" ];
after = [
"auditd.service"
"network.target"
];
environment = {
NAME = "aesm_service";
AESM_PATH = storeAesmFolder;
LD_LIBRARY_PATH = makeLibraryPath [ cfg.quoteProviderLibrary ];
}
// cfg.environment;
# Make sure any of the SGX application enclave devices is available
unitConfig.AssertPathExists = [
# legacy out-of-tree driver
"|/dev/isgx"
# DCAP driver
"|/dev/sgx/enclave"
# in-tree driver
"|/dev/sgx_enclave"
];
serviceConfig = {
ExecStartPre = pkgs.writeShellScript "copy-aesmd-data-files.sh" ''
set -euo pipefail
whiteListFile="${aesmDataFolder}/white_list_cert_to_be_verify.bin"
if [[ ! -f "$whiteListFile" ]]; then
${pkgs.coreutils}/bin/install -m 644 -D \
"${storeAesmFolder}/data/white_list_cert_to_be_verify.bin" \
"$whiteListFile"
fi
'';
ExecStart = "${sgx-psw}/bin/aesm_service --no-daemon";
ExecReload = ''${pkgs.coreutils}/bin/kill -SIGHUP "$MAINPID"'';
Restart = "on-failure";
RestartSec = "15s";
DynamicUser = true;
Group = "sgx";
SupplementaryGroups = [
config.hardware.cpu.intel.sgx.provision.group
];
Type = "simple";
WorkingDirectory = storeAesmFolder;
StateDirectory = "aesmd";
StateDirectoryMode = "0700";
RuntimeDirectory = "aesmd";
RuntimeDirectoryMode = "0750";
# Hardening
# chroot into the runtime directory
RootDirectory = "%t/aesmd";
BindReadOnlyPaths = [
builtins.storeDir
# Hardcoded path AESM_CONFIG_FILE in psw/ae/aesm_service/source/utils/aesm_config.cpp
"${configFile}:/etc/aesmd.conf"
];
BindPaths = [
# Hardcoded path CONFIG_SOCKET_PATH in psw/ae/aesm_service/source/core/ipc/SocketConfig.h
"%t/aesmd:/var/run/aesmd"
"%S/aesmd:/var/opt/aesmd"
];
# PrivateDevices=true will mount /dev noexec which breaks AESM
PrivateDevices = false;
DevicePolicy = "closed";
DeviceAllow = [
# legacy out-of-tree driver
"/dev/isgx rw"
# DCAP driver
"/dev/sgx rw"
# in-tree driver
"/dev/sgx_enclave rw"
"/dev/sgx_provision rw"
];
# Requires Internet access for attestation
PrivateNetwork = false;
RestrictAddressFamilies = [
# Allocates the socket /var/run/aesmd/aesm.socket
"AF_UNIX"
# Uses the HTTP protocol to initialize some services
"AF_INET"
"AF_INET6"
];
# True breaks stuff
MemoryDenyWriteExecute = false;
# needs the ipc syscall in order to run
SystemCallFilter = [
"@system-service"
"~@aio"
"~@chown"
"~@clock"
"~@cpu-emulation"
"~@debug"
"~@keyring"
"~@memlock"
"~@module"
"~@mount"
"~@privileged"
"~@raw-io"
"~@reboot"
"~@resources"
"~@setuid"
"~@swap"
"~@sync"
"~@timer"
];
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
CapabilityBoundingSet = "";
KeyringMode = "private";
LockPersonality = true;
NoNewPrivileges = true;
NotifyAccess = "none";
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RemoveIPC = true;
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
UMask = "0066";
};
};
};
}

View File

@@ -0,0 +1,485 @@
{
lib,
pkgs,
config,
...
}:
let
cfg = config.services.authelia;
format = pkgs.formats.yaml { };
autheliaOpts =
with lib;
{ name, ... }:
{
options = {
enable = mkEnableOption "Authelia instance";
name = mkOption {
type = types.str;
default = name;
description = ''
Name is used as a suffix for the service name, user, and group.
By default it takes the value you use for `<instance>` in:
{option}`services.authelia.<instance>`
'';
};
package = mkPackageOption pkgs "authelia" { };
user = mkOption {
default = "authelia-${name}";
type = types.str;
description = "The name of the user for this authelia instance.";
};
group = mkOption {
default = "authelia-${name}";
type = types.str;
description = "The name of the group for this authelia instance.";
};
secrets = mkOption {
description = ''
It is recommended you keep your secrets separate from the configuration.
It's especially important to keep the raw secrets out of your nix configuration,
as the values will be preserved in your nix store.
This attribute allows you to configure the location of secret files to be loaded at runtime.
<https://www.authelia.com/configuration/methods/secrets/>
'';
default = { };
type = types.submodule {
options = {
manual = mkOption {
default = false;
example = true;
description = ''
Configuring authelia's secret files via the secrets attribute set
is intended to be convenient and help catch cases where values are required
to run at all.
If a user wants to set these values themselves and bypass the validation they can set this value to true.
'';
type = types.bool;
};
# required
jwtSecretFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to your JWT secret used during identity verificaton.
'';
};
oidcIssuerPrivateKeyFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to your private key file used to encrypt OIDC JWTs.
'';
};
oidcHmacSecretFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to your HMAC secret used to sign OIDC JWTs.
'';
};
sessionSecretFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to your session secret. Only used when redis is used as session storage.
'';
};
# required
storageEncryptionKeyFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to your storage encryption key.
'';
};
};
};
};
environmentVariables = mkOption {
type = types.attrsOf types.str;
description = ''
Additional environment variables to provide to authelia.
If you are providing secrets please consider the options under {option}`services.authelia.<instance>.secrets`
or make sure you use the `_FILE` suffix.
If you provide the raw secret rather than the location of a secret file that secret will be preserved in the nix store.
For more details: <https://www.authelia.com/configuration/methods/secrets/>
'';
default = { };
};
settings = mkOption {
description = ''
Your Authelia config.yml as a Nix attribute set.
There are several values that are defined and documented in nix such as `default_2fa_method`,
but additional items can also be included.
<https://github.com/authelia/authelia/blob/master/config.template.yml>
'';
default = { };
example = ''
{
theme = "light";
default_2fa_method = "totp";
log.level = "debug";
server.disable_healthcheck = true;
}
'';
type = types.submodule {
freeformType = format.type;
options = {
theme = mkOption {
type = types.enum [
"light"
"dark"
"grey"
"auto"
];
default = "light";
example = "dark";
description = "The theme to display.";
};
default_2fa_method = mkOption {
type = types.enum [
""
"totp"
"webauthn"
"mobile_push"
];
default = "";
example = "webauthn";
description = ''
Default 2FA method for new users and fallback for preferred but disabled methods.
'';
};
server = {
address = mkOption {
type = types.str;
default = "tcp://:9091/";
example = "unix:///var/run/authelia.sock?path=authelia&umask=0117";
description = "The address to listen on.";
};
};
log = {
level = mkOption {
type = types.enum [
"trace"
"debug"
"info"
"warn"
"error"
];
default = "debug";
example = "info";
description = "Level of verbosity for logs.";
};
format = mkOption {
type = types.enum [
"json"
"text"
];
default = "json";
example = "text";
description = "Format the logs are written as.";
};
file_path = mkOption {
type = types.nullOr types.path;
default = null;
example = "/var/log/authelia/authelia.log";
description = "File path where the logs will be written. If not set logs are written to stdout.";
};
keep_stdout = mkOption {
type = types.bool;
default = false;
example = true;
description = "Whether to also log to stdout when a `file_path` is defined.";
};
};
telemetry = {
metrics = {
enabled = mkOption {
type = types.bool;
default = false;
example = true;
description = "Enable Metrics.";
};
address = mkOption {
type = types.str;
default = "tcp://127.0.0.1:9959";
example = "tcp://0.0.0.0:8888";
description = "The address to listen on for metrics. This should be on a different port to the main `server.port` value.";
};
};
};
};
};
};
settingsFiles = mkOption {
type = types.listOf types.path;
default = [ ];
example = [
"/etc/authelia/config.yml"
"/etc/authelia/access-control.yml"
"/etc/authelia/config/"
];
description = ''
Here you can provide authelia with configuration files or directories.
It is possible to give authelia multiple files and use the nix generated configuration
file set via {option}`services.authelia.<instance>.settings`.
'';
};
};
};
writeOidcJwksConfigFile =
oidcIssuerPrivateKeyFile:
pkgs.writeText "oidc-jwks.yaml" ''
identity_providers:
oidc:
jwks:
- key: {{ secret "${oidcIssuerPrivateKeyFile}" | mindent 10 "|" | msquote }}
'';
# Remove an attribute in a nested set
# https://discourse.nixos.org/t/modify-an-attrset-in-nix/29919/5
removeAttrByPath =
set: pathList:
lib.updateManyAttrsByPath [
{
path = lib.init pathList;
update = old: lib.filterAttrs (n: v: n != (lib.last pathList)) old;
}
] set;
in
{
options.services.authelia.instances =
with lib;
mkOption {
default = { };
type = types.attrsOf (types.submodule autheliaOpts);
description = ''
Multi-domain protection currently requires multiple instances of Authelia.
If you don't require multiple instances of Authelia you can define just the one.
<https://www.authelia.com/roadmap/active/multi-domain-protection/>
'';
example = ''
{
main = {
enable = true;
secrets.storageEncryptionKeyFile = "/etc/authelia/storageEncryptionKeyFile";
secrets.jwtSecretFile = "/etc/authelia/jwtSecretFile";
settings = {
theme = "light";
default_2fa_method = "totp";
log.level = "debug";
server.disable_healthcheck = true;
};
};
preprod = {
enable = false;
secrets.storageEncryptionKeyFile = "/mnt/pre-prod/authelia/storageEncryptionKeyFile";
secrets.jwtSecretFile = "/mnt/pre-prod/jwtSecretFile";
settings = {
theme = "dark";
default_2fa_method = "webauthn";
server.host = "0.0.0.0";
};
};
test.enable = true;
test.secrets.manual = true;
test.settings.theme = "grey";
test.settings.server.disable_healthcheck = true;
test.settingsFiles = [ "/mnt/test/authelia" "/mnt/test-authelia.conf" ];
};
}
'';
};
config =
let
mkInstanceServiceConfig =
instance:
let
cleanedSettings =
if
(
instance.settings.server ? host
|| instance.settings.server ? port
|| instance.settings.server ? path
)
then
# Old settings are used: display a warning and remove the default value of server.address
# as authelia does not allow both old and new settings to be set
lib.warn
"Please replace services.authelia.instances.${instance.name}.settings.{host,port,path} with services.authelia.instances.${instance.name}.settings.address, before release 5.0.0"
(
removeAttrByPath instance.settings [
"server"
"address"
]
)
else
instance.settings;
execCommand = "${instance.package}/bin/authelia";
configFile = format.generate "config.yml" cleanedSettings;
oidcJwksConfigFile = lib.optional (instance.secrets.oidcIssuerPrivateKeyFile != null) (
writeOidcJwksConfigFile instance.secrets.oidcIssuerPrivateKeyFile
);
configArg = "--config ${
builtins.concatStringsSep "," (
lib.concatLists [
[ configFile ]
instance.settingsFiles
oidcJwksConfigFile
]
)
}";
in
{
description = "Authelia authentication and authorization server";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ]; # Checks SMTP notifier creds during startup
wants = [ "network-online.target" ];
environment =
(lib.filterAttrs (_: v: v != null) {
X_AUTHELIA_CONFIG_FILTERS = lib.mkIf (oidcJwksConfigFile != [ ]) "template";
AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE = instance.secrets.jwtSecretFile;
AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE = instance.secrets.storageEncryptionKeyFile;
AUTHELIA_SESSION_SECRET_FILE = instance.secrets.sessionSecretFile;
AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE = instance.secrets.oidcHmacSecretFile;
})
// instance.environmentVariables;
preStart = "${execCommand} ${configArg} validate-config";
serviceConfig = {
User = instance.user;
Group = instance.group;
ExecStart = "${execCommand} ${configArg}";
Restart = "always";
RestartSec = "5s";
StateDirectory = "authelia-${instance.name}";
StateDirectoryMode = "0700";
# Security options:
AmbientCapabilities = "";
CapabilityBoundingSet = "";
DeviceAllow = "";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateTmp = true;
PrivateDevices = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = "read-only";
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "noaccess";
ProtectSystem = "strict";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
SystemCallFilter = [
"@system-service"
"~@cpu-emulation"
"~@debug"
"~@keyring"
"~@memlock"
"~@obsolete"
"~@privileged"
"~@setuid"
];
};
};
mkInstanceUsersConfig = instance: {
groups."authelia-${instance.name}" = lib.mkIf (instance.group == "authelia-${instance.name}") {
name = "authelia-${instance.name}";
};
users."authelia-${instance.name}" = lib.mkIf (instance.user == "authelia-${instance.name}") {
name = "authelia-${instance.name}";
isSystemUser = true;
group = instance.group;
};
};
instances = lib.attrValues cfg.instances;
in
{
assertions = lib.flatten (
lib.flip lib.mapAttrsToList cfg.instances (
name: instance: [
{
assertion =
instance.secrets.manual
|| (instance.secrets.jwtSecretFile != null && instance.secrets.storageEncryptionKeyFile != null);
message = ''
Authelia requires a JWT Secret and a Storage Encryption Key to work.
Either set them like so:
services.authelia.${name}.secrets.jwtSecretFile = /my/path/to/jwtsecret;
services.authelia.${name}.secrets.storageEncryptionKeyFile = /my/path/to/encryptionkey;
Or set services.authelia.${name}.secrets.manual = true and provide them yourself via
environmentVariables or settingsFiles.
Do not include raw secrets in nix settings.
'';
}
]
)
);
systemd.services = lib.mkMerge (
map (
instance:
lib.mkIf instance.enable {
"authelia-${instance.name}" = mkInstanceServiceConfig instance;
}
) instances
);
users = lib.mkMerge (
map (instance: lib.mkIf instance.enable (mkInstanceUsersConfig instance)) instances
);
};
meta.maintainers = with lib.maintainers; [
jk
dit7ya
nicomem
];
}

View File

@@ -0,0 +1,339 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.bitwarden-directory-connector-cli;
in
{
options.services.bitwarden-directory-connector-cli = {
enable = mkEnableOption "Bitwarden Directory Connector";
package = mkPackageOption pkgs "bitwarden-directory-connector-cli" { };
domain = mkOption {
type = types.str;
description = "The domain the Bitwarden/Vaultwarden is accessible on.";
example = "https://vaultwarden.example.com";
};
user = mkOption {
type = types.str;
description = "User to run the program.";
default = "bwdc";
};
interval = mkOption {
type = types.str;
default = "*:0,15,30,45";
description = "The interval when to run the connector. This uses systemd's OnCalendar syntax.";
};
ldap = mkOption {
description = ''
Options to configure the LDAP connection.
If you used the desktop application to test the configuration you can find the settings by searching for `ldap` in `~/.config/Bitwarden\ Directory\ Connector/data.json`.
'';
default = { };
type = types.submodule (
{
config,
options,
...
}:
{
freeformType = types.attrsOf (pkgs.formats.json { }).type;
config.finalJSON = builtins.toJSON (
removeAttrs config (
filter (x: x == "finalJSON" || !options.${x}.isDefined or false) (attrNames options)
)
);
options = {
finalJSON = mkOption {
type = (pkgs.formats.json { }).type;
internal = true;
readOnly = true;
visible = false;
};
ssl = mkOption {
type = types.bool;
default = false;
description = "Whether to use TLS.";
};
startTls = mkOption {
type = types.bool;
default = false;
description = "Whether to use STARTTLS.";
};
hostname = mkOption {
type = types.str;
description = "The host the LDAP is accessible on.";
example = "ldap.example.com";
};
port = mkOption {
type = types.port;
default = 389;
description = "Port LDAP is accessible on.";
};
ad = mkOption {
type = types.bool;
default = false;
description = "Whether the LDAP Server is an Active Directory.";
};
pagedSearch = mkOption {
type = types.bool;
default = false;
description = "Whether the LDAP server paginates search results.";
};
rootPath = mkOption {
type = types.str;
description = "Root path for LDAP.";
example = "dc=example,dc=com";
};
username = mkOption {
type = types.str;
description = "The user to authenticate as.";
example = "cn=admin,dc=example,dc=com";
};
};
}
);
};
sync = mkOption {
description = ''
Options to configure what gets synced.
If you used the desktop application to test the configuration you can find the settings by searching for `sync` in `~/.config/Bitwarden\ Directory\ Connector/data.json`.
'';
default = { };
type = types.submodule (
{
config,
options,
...
}:
{
freeformType = types.attrsOf (pkgs.formats.json { }).type;
config.finalJSON = builtins.toJSON (
removeAttrs config (
filter (x: x == "finalJSON" || !options.${x}.isDefined or false) (attrNames options)
)
);
options = {
finalJSON = mkOption {
type = (pkgs.formats.json { }).type;
internal = true;
readOnly = true;
visible = false;
};
removeDisabled = mkOption {
type = types.bool;
default = true;
description = "Remove users from bitwarden groups if no longer in the ldap group.";
};
overwriteExisting = mkOption {
type = types.bool;
default = false;
description = "Remove and re-add users/groups, See <https://bitwarden.com/help/user-group-filters/#overwriting-syncs> for more details.";
};
largeImport = mkOption {
type = types.bool;
default = false;
description = "Enable if you are syncing more than 2000 users/groups.";
};
memberAttribute = mkOption {
type = types.str;
description = "Attribute that lists members in a LDAP group.";
example = "uniqueMember";
};
creationDateAttribute = mkOption {
type = types.str;
description = "Attribute that lists a user's creation date.";
example = "whenCreated";
};
useEmailPrefixSuffix = mkOption {
type = types.bool;
default = false;
description = "If a user has no email address, combine a username prefix with a suffix value to form an email.";
};
emailPrefixAttribute = mkOption {
type = types.str;
description = "The attribute that contains the users username.";
example = "accountName";
};
emailSuffix = mkOption {
type = types.str;
description = "Suffix for the email, normally @example.com.";
example = "@example.com";
};
users = mkOption {
type = types.bool;
default = false;
description = "Sync users.";
};
userPath = mkOption {
type = types.str;
description = "User directory, relative to root.";
default = "ou=users";
};
userObjectClass = mkOption {
type = types.str;
description = "Class that users must have.";
default = "inetOrgPerson";
};
userEmailAttribute = mkOption {
type = types.str;
description = "Attribute for a users email.";
default = "mail";
};
userFilter = mkOption {
type = types.str;
description = "LDAP filter for users.";
example = "(memberOf=cn=sales,ou=groups,dc=example,dc=com)";
default = "";
};
groups = mkOption {
type = types.bool;
default = false;
description = "Whether to sync ldap groups into BitWarden.";
};
groupPath = mkOption {
type = types.str;
description = "Group directory, relative to root.";
default = "ou=groups";
};
groupObjectClass = mkOption {
type = types.str;
description = "A class that groups will have.";
default = "groupOfNames";
};
groupNameAttribute = mkOption {
type = types.str;
description = "Attribute for a name of group.";
default = "cn";
};
groupFilter = mkOption {
type = types.str;
description = "LDAP filter for groups.";
example = "(cn=sales)";
default = "";
};
};
}
);
};
secrets = {
ldap = mkOption {
type = types.str;
description = "Path to file that contains LDAP password for user in {option}`ldap.username";
};
bitwarden = {
client_path_id = mkOption {
type = types.str;
description = "Path to file that contains Client ID.";
};
client_path_secret = mkOption {
type = types.str;
description = "Path to file that contains Client Secret.";
};
};
};
};
config = mkIf cfg.enable {
users.groups."${cfg.user}" = { };
users.users."${cfg.user}" = {
isSystemUser = true;
group = cfg.user;
};
systemd = {
timers.bitwarden-directory-connector-cli = {
description = "Sync timer for Bitwarden Directory Connector";
wantedBy = [ "timers.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
timerConfig = {
OnCalendar = cfg.interval;
Unit = "bitwarden-directory-connector-cli.service";
Persistent = true;
};
};
services.bitwarden-directory-connector-cli = {
description = "Main process for Bitwarden Directory Connector";
path = [ pkgs.jq ];
environment = {
BITWARDENCLI_CONNECTOR_APPDATA_DIR = "/tmp";
BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS = "true";
};
preStart = ''
set -eo pipefail
# create the config file
${lib.getExe cfg.package} data-file
touch /tmp/data.json.tmp
chmod 600 /tmp/data.json{,.tmp}
${lib.getExe cfg.package} config server ${cfg.domain}
# now login to set credentials
export BW_CLIENTID="$(< ${escapeShellArg cfg.secrets.bitwarden.client_path_id})"
export BW_CLIENTSECRET="$(< ${escapeShellArg cfg.secrets.bitwarden.client_path_secret})"
${lib.getExe cfg.package} login
jq '.authenticatedAccounts[0] as $account
| .[$account].directoryConfigurations.ldap |= $ldap_data
| .[$account].directorySettings.organizationId |= $orgID
| .[$account].directorySettings.sync |= $sync_data' \
--argjson ldap_data ${escapeShellArg cfg.ldap.finalJSON} \
--arg orgID "''${BW_CLIENTID//organization.}" \
--argjson sync_data ${escapeShellArg cfg.sync.finalJSON} \
/tmp/data.json \
> /tmp/data.json.tmp
mv -f /tmp/data.json.tmp /tmp/data.json
# final config
${lib.getExe cfg.package} config directory 0
${lib.getExe cfg.package} config ldap.password --secretfile ${cfg.secrets.ldap}
'';
serviceConfig = {
Type = "oneshot";
User = "${cfg.user}";
PrivateTmp = true;
ExecStart = "${lib.getExe cfg.package} sync";
};
};
};
};
meta.maintainers = with maintainers; [ Silver-Golden ];
}

View File

@@ -0,0 +1,392 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.canaille;
inherit (lib)
mkOption
mkIf
mkEnableOption
mkPackageOption
types
getExe
optional
converge
filterAttrsRecursive
;
dataDir = "/var/lib/canaille";
secretsDir = "${dataDir}/secrets";
settingsFormat = pkgs.formats.toml { };
# Remove null values, so we can document optional/forbidden values that don't end up in the generated TOML file.
filterConfig = converge (filterAttrsRecursive (_: v: v != null));
finalPackage = cfg.package.overridePythonAttrs (old: {
dependencies =
old.dependencies
++ old.optional-dependencies.front
++ old.optional-dependencies.oidc
++ old.optional-dependencies.scim
++ old.optional-dependencies.ldap
++ old.optional-dependencies.sentry
++ old.optional-dependencies.postgresql
++ old.optional-dependencies.otp
++ old.optional-dependencies.sms;
makeWrapperArgs = (old.makeWrapperArgs or [ ]) ++ [
"--set CONFIG /etc/canaille/config.toml"
"--set SECRETS_DIR \"${secretsDir}\""
];
});
inherit (finalPackage) python;
pythonEnv = python.buildEnv.override {
extraLibs = with python.pkgs; [
(toPythonModule finalPackage)
celery
];
};
commonServiceConfig = {
WorkingDirectory = dataDir;
User = "canaille";
Group = "canaille";
StateDirectory = "canaille";
StateDirectoryMode = "0750";
PrivateTmp = true;
};
postgresqlHost = "postgresql://localhost/canaille?host=/run/postgresql";
createLocalPostgresqlDb = cfg.settings.CANAILLE_SQL.DATABASE_URI == postgresqlHost;
in
{
options.services.canaille = {
enable = mkEnableOption "Canaille";
package = mkPackageOption pkgs "canaille" { };
secretKeyFile = mkOption {
description = ''
File containing the Flask secret key. Its content is going to be
provided to Canaille as `SECRET_KEY`. Make sure it has appropriate
permissions. For example, copy the output of this to the specified
file:
```
python3 -c 'import secrets; print(secrets.token_hex())'
```
'';
type = types.path;
};
smtpPasswordFile = mkOption {
description = ''
File containing the SMTP password. Make sure it has appropriate permissions.
'';
default = null;
type = types.nullOr types.path;
};
jwtPrivateKeyFile = mkOption {
description = ''
File containing the JWT private key. Make sure it has appropriate permissions.
You can generate one using
```
openssl genrsa -out private.pem 4096
openssl rsa -in private.pem -pubout -outform PEM -out public.pem
```
'';
default = null;
type = types.nullOr types.path;
};
ldapBindPasswordFile = mkOption {
description = ''
File containing the LDAP bind password.
'';
default = null;
type = types.nullOr types.path;
};
settings = mkOption {
default = { };
description = "Settings for Canaille. See [the documentation](https://canaille.readthedocs.io/en/latest/references/configuration.html) for details.";
type = types.submodule {
freeformType = settingsFormat.type;
options = {
SECRET_KEY = mkOption {
readOnly = true;
description = ''
Flask Secret Key. Can't be set and must be provided through
`services.canaille.settings.secretKeyFile`.
'';
default = null;
type = types.nullOr types.str;
};
SERVER_NAME = mkOption {
description = "The domain name on which canaille will be served.";
example = "auth.example.org";
type = types.str;
};
PREFERRED_URL_SCHEME = mkOption {
description = "The url scheme by which canaille will be served.";
default = "https";
type = types.enum [
"http"
"https"
];
};
CANAILLE = {
ACL = mkOption {
default = null;
description = ''
Access Control Lists.
See also [the documentation](https://canaille.readthedocs.io/en/latest/references/configuration.html#canaille.core.configuration.ACLSettings).
'';
type = types.nullOr (
types.submodule {
freeformType = settingsFormat.type;
options = { };
}
);
};
SMTP = mkOption {
default = null;
example = { };
description = ''
SMTP configuration. By default, sending emails is not enabled.
Set to an empty attrs to send emails from localhost without
authentication.
See also [the documentation](https://canaille.readthedocs.io/en/latest/references/configuration.html#canaille.core.configuration.SMTPSettings).
'';
type = types.nullOr (
types.submodule {
freeformType = settingsFormat.type;
options = {
PASSWORD = mkOption {
readOnly = true;
description = ''
SMTP Password. Can't be set and has to be provided using
`services.canaille.smtpPasswordFile`.
'';
default = null;
type = types.nullOr types.str;
};
};
}
);
};
};
CANAILLE_OIDC = mkOption {
default = null;
description = ''
OpenID Connect settings. See [the documentation](https://canaille.readthedocs.io/en/latest/references/configuration.html#canaille.oidc.configuration.OIDCSettings).
'';
type = types.nullOr (
types.submodule {
freeformType = settingsFormat.type;
options = {
JWT.PRIVATE_KEY = mkOption {
readOnly = true;
description = ''
JWT private key. Can't be set and has to be provided using
`services.canaille.jwtPrivateKeyFile`.
'';
default = null;
type = types.nullOr types.str;
};
};
}
);
};
CANAILLE_LDAP = mkOption {
default = null;
description = ''
Configuration for the LDAP backend. This storage backend is not
yet supported by the module, so use at your own risk!
'';
type = types.nullOr (
types.submodule {
freeformType = settingsFormat.type;
options = {
BIND_PW = mkOption {
readOnly = true;
description = ''
The LDAP bind password. Can't be set and has to be provided using
`services.canaille.ldapBindPasswordFile`.
'';
default = null;
type = types.nullOr types.str;
};
};
}
);
};
CANAILLE_SQL = {
DATABASE_URI = mkOption {
description = ''
The SQL server URI. Will configure a local PostgreSQL db if
left to default. Please note that the NixOS module only really
supports PostgreSQL for now. Change at your own risk!
'';
default = postgresqlHost;
type = types.str;
};
};
};
};
};
};
config = mkIf cfg.enable {
# We can use some kind of fix point for the config anyways, and
# /etc/canaille is recommended by upstream. The alternative would be to use
# a double wrapped canaille executable, to avoid having to rebuild Canaille
# on every config change.
environment.etc."canaille/config.toml" = {
source = settingsFormat.generate "config.toml" (filterConfig cfg.settings);
user = "canaille";
group = "canaille";
};
# Secrets management is unfortunately done in a semi stateful way, due to these constraints:
# - Canaille uses Pydantic, which currently only accepts an env file or a single
# directory (SECRETS_DIR) for loading settings from files.
# - The canaille user needs access to secrets, as it needs to run the CLI
# for e.g. user creation. Therefore specifying the SECRETS_DIR as systemd's
# CREDENTIALS_DIRECTORY is not an option.
#
# See this for how Pydantic maps file names/env vars to config settings:
# https://docs.pydantic.dev/latest/concepts/pydantic_settings/#parsing-environment-variable-values
systemd.tmpfiles.rules = [
"Z ${secretsDir} 700 canaille canaille - -"
"L+ ${secretsDir}/SECRET_KEY - - - - ${cfg.secretKeyFile}"
]
++ optional (
cfg.smtpPasswordFile != null
) "L+ ${secretsDir}/CANAILLE_SMTP__PASSWORD - - - - ${cfg.smtpPasswordFile}"
++ optional (
cfg.jwtPrivateKeyFile != null
) "L+ ${secretsDir}/CANAILLE_OIDC__JWT__PRIVATE_KEY - - - - ${cfg.jwtPrivateKeyFile}"
++ optional (
cfg.ldapBindPasswordFile != null
) "L+ ${secretsDir}/CANAILLE_LDAP__BIND_PW - - - - ${cfg.ldapBindPasswordFile}";
# This is not a migration, just an initial setup of schemas
systemd.services.canaille-install = {
# We want this on boot, not on socket activation
wantedBy = [ "multi-user.target" ];
after = optional createLocalPostgresqlDb "postgresql.target";
serviceConfig = commonServiceConfig // {
Type = "oneshot";
ExecStart = "${getExe finalPackage} install";
};
};
systemd.services.canaille = {
description = "Canaille";
documentation = [ "https://canaille.readthedocs.io/en/latest/tutorial/deployment.html" ];
after = [
"network.target"
"canaille-install.service"
]
++ optional createLocalPostgresqlDb "postgresql.target";
requires = [
"canaille-install.service"
"canaille.socket"
];
environment = {
PYTHONPATH = "${pythonEnv}/${python.sitePackages}/";
CONFIG = "/etc/canaille/config.toml";
SECRETS_DIR = secretsDir;
};
serviceConfig = commonServiceConfig // {
Restart = "on-failure";
ExecStart =
let
gunicorn = python.pkgs.gunicorn.overridePythonAttrs (old: {
# Allows Gunicorn to set a meaningful process name
dependencies = (old.dependencies or [ ]) ++ old.optional-dependencies.setproctitle;
});
in
''
${getExe gunicorn} \
--name=canaille \
--bind='unix:///run/canaille.socket' \
'canaille:create_app()'
'';
};
restartTriggers = [ "/etc/canaille/config.toml" ];
};
systemd.sockets.canaille = {
before = [ "nginx.service" ];
wantedBy = [ "sockets.target" ];
socketConfig = {
ListenStream = "/run/canaille.socket";
SocketUser = "canaille";
SocketGroup = "canaille";
SocketMode = "770";
};
};
services.nginx.enable = true;
services.nginx.recommendedGzipSettings = true;
services.nginx.recommendedProxySettings = true;
services.nginx.virtualHosts."${cfg.settings.SERVER_NAME}" = {
forceSSL = true;
enableACME = true;
# Config from https://canaille.readthedocs.io/en/latest/tutorial/deployment.html#nginx
extraConfig = ''
charset utf-8;
client_max_body_size 10M;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "same-origin" always;
'';
locations = {
"/".proxyPass = "http://unix:///run/canaille.socket";
"/static" = {
root = "${finalPackage}/${python.sitePackages}/canaille";
};
"~* ^/static/.+\\.(?:css|cur|js|jpe?g|gif|htc|ico|png|html|xml|otf|ttf|eot|woff|woff2|svg)$" = {
root = "${finalPackage}/${python.sitePackages}/canaille";
extraConfig = ''
access_log off;
expires 30d;
more_set_headers Cache-Control public;
'';
};
};
};
services.postgresql = mkIf createLocalPostgresqlDb {
enable = true;
ensureUsers = [
{
name = "canaille";
ensureDBOwnership = true;
}
];
ensureDatabases = [ "canaille" ];
};
users.users.canaille = {
isSystemUser = true;
group = "canaille";
packages = [ finalPackage ];
};
users.groups.canaille.members = [ config.services.nginx.user ];
};
meta.maintainers = with lib.maintainers; [ erictapen ];
}

View File

@@ -0,0 +1,227 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.certmgr;
specs = lib.mapAttrsToList (n: v: rec {
name = n + ".json";
path = if lib.isAttrs v then pkgs.writeText name (builtins.toJSON v) else v;
}) cfg.specs;
allSpecs = pkgs.linkFarm "certmgr.d" specs;
certmgrYaml = pkgs.writeText "certmgr.yaml" (
builtins.toJSON {
dir = allSpecs;
default_remote = cfg.defaultRemote;
svcmgr = cfg.svcManager;
before = cfg.validMin;
interval = cfg.renewInterval;
inherit (cfg) metricsPort metricsAddress;
}
);
specPaths = map dirOf (
lib.concatMap (
spec:
if lib.isAttrs spec then
lib.collect lib.isString (lib.filterAttrsRecursive (n: v: lib.isAttrs v || n == "path") spec)
else
[ spec ]
) (lib.attrValues cfg.specs)
);
preStart = ''
${lib.concatStringsSep " \\\n" ([ "mkdir -p" ] ++ map lib.escapeShellArg specPaths)}
${cfg.package}/bin/certmgr -f ${certmgrYaml} check
'';
in
{
options.services.certmgr = {
enable = lib.mkEnableOption "certmgr";
package = lib.mkPackageOption pkgs "certmgr" { };
defaultRemote = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1:8888";
description = "The default CA host:port to use.";
};
validMin = lib.mkOption {
default = "72h";
type = lib.types.str;
description = "The interval before a certificate expires to start attempting to renew it.";
};
renewInterval = lib.mkOption {
default = "30m";
type = lib.types.str;
description = "How often to check certificate expirations and how often to update the cert_next_expires metric.";
};
metricsAddress = lib.mkOption {
default = "127.0.0.1";
type = lib.types.str;
description = "The address for the Prometheus HTTP endpoint.";
};
metricsPort = lib.mkOption {
default = 9488;
type = lib.types.ints.u16;
description = "The port for the Prometheus HTTP endpoint.";
};
specs = lib.mkOption {
default = { };
example = lib.literalExpression ''
{
exampleCert =
let
domain = "example.com";
secret = name: "/var/lib/secrets/''${name}.pem";
in {
service = "nginx";
action = "reload";
authority = {
file.path = secret "ca";
};
certificate = {
path = secret domain;
};
private_key = {
owner = "root";
group = "root";
mode = "0600";
path = secret "''${domain}-key";
};
request = {
CN = domain;
hosts = [ "mail.''${domain}" "www.''${domain}" ];
key = {
algo = "rsa";
size = 2048;
};
names = {
O = "Example Organization";
C = "USA";
};
};
};
otherCert = "/var/certmgr/specs/other-cert.json";
}
'';
type =
with lib.types;
attrsOf (
either path (submodule {
options = {
service = lib.mkOption {
type = nullOr str;
default = null;
description = "The service on which to perform \\<action\\> after fetching.";
};
action = lib.mkOption {
type = addCheck str (
x:
cfg.svcManager == "command"
|| lib.elem x [
"restart"
"reload"
"nop"
]
);
default = "nop";
description = "The action to take after fetching.";
};
# These ought all to be specified according to certmgr spec def.
authority = lib.mkOption {
type = attrs;
description = "certmgr spec authority object.";
};
certificate = lib.mkOption {
type = nullOr attrs;
description = "certmgr spec certificate object.";
};
private_key = lib.mkOption {
type = nullOr attrs;
description = "certmgr spec private_key object.";
};
request = lib.mkOption {
type = nullOr attrs;
description = "certmgr spec request object.";
};
};
})
);
description = ''
Certificate specs as described by:
<https://github.com/cloudflare/certmgr#certificate-specs>
These will be added to the Nix store, so they will be world readable.
'';
};
svcManager = lib.mkOption {
default = "systemd";
type = lib.types.enum [
"circus"
"command"
"dummy"
"openrc"
"systemd"
"sysv"
];
description = ''
This specifies the service manager to use for restarting or reloading services.
See: <https://github.com/cloudflare/certmgr#certmgryaml>.
For how to use the "command" service manager in particular,
see: <https://github.com/cloudflare/certmgr#command-svcmgr-and-how-to-use-it>.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.specs != { };
message = "Certmgr specs cannot be empty.";
}
{
assertion =
!lib.any (lib.hasAttrByPath [
"authority"
"auth_key"
]) (lib.attrValues cfg.specs);
message = ''
Inline services.certmgr.specs are added to the Nix store rendering them world readable.
Specify paths as specs, if you want to use include auth_key - or use the auth_key_file option."
'';
}
];
systemd.services.certmgr = {
description = "certmgr";
path = lib.mkIf (cfg.svcManager == "command") [ pkgs.bash ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
inherit preStart;
serviceConfig = {
Restart = "always";
RestartSec = "10s";
ExecStart = "${cfg.package}/bin/certmgr -f ${certmgrYaml}";
};
};
};
}

View File

@@ -0,0 +1,235 @@
{
config,
options,
lib,
pkgs,
...
}:
let
cfg = config.services.cfssl;
in
{
options.services.cfssl = {
enable = lib.mkEnableOption "the CFSSL CA api-server";
dataDir = lib.mkOption {
default = "/var/lib/cfssl";
type = lib.types.path;
description = ''
The work directory for CFSSL.
::: {.note}
If left as the default value this directory will automatically be
created before the CFSSL server starts, otherwise you are
responsible for ensuring the directory exists with appropriate
ownership and permissions.
:::
'';
};
address = lib.mkOption {
default = "127.0.0.1";
type = lib.types.str;
description = "Address to bind.";
};
port = lib.mkOption {
default = 8888;
type = lib.types.port;
description = "Port to bind.";
};
ca = lib.mkOption {
defaultText = lib.literalExpression ''"''${cfg.dataDir}/ca.pem"'';
type = lib.types.str;
description = "CA used to sign the new certificate -- accepts '[file:]fname' or 'env:varname'.";
};
caKey = lib.mkOption {
defaultText = lib.literalExpression ''"file:''${cfg.dataDir}/ca-key.pem"'';
type = lib.types.str;
description = "CA private key -- accepts '[file:]fname' or 'env:varname'.";
};
caBundle = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.path;
description = "Path to root certificate store.";
};
intBundle = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.path;
description = "Path to intermediate certificate store.";
};
intDir = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.path;
description = "Intermediates directory.";
};
metadata = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.path;
description = ''
Metadata file for root certificate presence.
The content of the file is a json dictionary (k,v): each key k is
a SHA-1 digest of a root certificate while value v is a list of key
store filenames.
'';
};
remote = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = "Remote CFSSL server.";
};
configFile = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = "Path to configuration file. Do not put this in nix-store as it might contain secrets.";
};
responder = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.path;
description = "Certificate for OCSP responder.";
};
responderKey = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = "Private key for OCSP responder certificate. Do not put this in nix-store.";
};
tlsKey = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = "Other endpoint's CA private key. Do not put this in nix-store.";
};
tlsCert = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.path;
description = "Other endpoint's CA to set up TLS protocol.";
};
mutualTlsCa = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.path;
description = "Mutual TLS - require clients be signed by this CA.";
};
mutualTlsCn = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = "Mutual TLS - regex for whitelist of allowed client CNs.";
};
tlsRemoteCa = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.path;
description = "CAs to trust for remote TLS requests.";
};
mutualTlsClientCert = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.path;
description = "Mutual TLS - client certificate to call remote instance requiring client certs.";
};
mutualTlsClientKey = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.path;
description = "Mutual TLS - client key to call remote instance requiring client certs. Do not put this in nix-store.";
};
dbConfig = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.path;
description = "Certificate db configuration file. Path must be writeable.";
};
logLevel = lib.mkOption {
default = 1;
type = lib.types.ints.between 0 5;
description = "Log level (0 = DEBUG, 5 = FATAL).";
};
disable = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.commas;
description = "Endpoints to disable (comma-separated list)";
};
};
config = lib.mkIf cfg.enable {
users.groups.cfssl = {
gid = config.ids.gids.cfssl;
};
users.users.cfssl = {
description = "cfssl user";
home = cfg.dataDir;
group = "cfssl";
uid = config.ids.uids.cfssl;
};
systemd.services.cfssl = {
description = "CFSSL CA API server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = lib.mkMerge [
{
WorkingDirectory = cfg.dataDir;
Restart = "always";
User = "cfssl";
Group = "cfssl";
ExecStart =
with cfg;
let
opt = n: v: lib.optionalString (v != null) ''-${n}="${v}"'';
in
lib.concatStringsSep " \\\n" [
"${pkgs.cfssl}/bin/cfssl serve"
(opt "address" address)
(opt "port" (toString port))
(opt "ca" ca)
(opt "ca-key" caKey)
(opt "ca-bundle" caBundle)
(opt "int-bundle" intBundle)
(opt "int-dir" intDir)
(opt "metadata" metadata)
(opt "remote" remote)
(opt "config" configFile)
(opt "responder" responder)
(opt "responder-key" responderKey)
(opt "tls-key" tlsKey)
(opt "tls-cert" tlsCert)
(opt "mutual-tls-ca" mutualTlsCa)
(opt "mutual-tls-cn" mutualTlsCn)
(opt "mutual-tls-client-key" mutualTlsClientKey)
(opt "mutual-tls-client-cert" mutualTlsClientCert)
(opt "tls-remote-ca" tlsRemoteCa)
(opt "db-config" dbConfig)
(opt "loglevel" (toString logLevel))
(opt "disable" disable)
];
}
(lib.mkIf (cfg.dataDir == options.services.cfssl.dataDir.default) {
StateDirectory = baseNameOf cfg.dataDir;
StateDirectoryMode = 700;
})
];
};
services.cfssl = {
ca = lib.mkDefault "${cfg.dataDir}/ca.pem";
caKey = lib.mkDefault "${cfg.dataDir}/ca-key.pem";
};
};
}

View File

@@ -0,0 +1,344 @@
{
config,
lib,
pkgs,
...
}:
let
clamavUser = "clamav";
stateDir = "/var/lib/clamav";
clamavGroup = clamavUser;
cfg = config.services.clamav;
toKeyValue = lib.generators.toKeyValue {
mkKeyValue = lib.generators.mkKeyValueDefault { } " ";
listsAsDuplicateKeys = true;
};
clamdConfigFile = pkgs.writeText "clamd.conf" (toKeyValue cfg.daemon.settings);
freshclamConfigFile = pkgs.writeText "freshclam.conf" (toKeyValue cfg.updater.settings);
fangfrischConfigFile = pkgs.writeText "fangfrisch.conf" ''
${lib.generators.toINI { } cfg.fangfrisch.settings}
'';
in
{
imports = [
(lib.mkRemovedOptionModule [
"services"
"clamav"
"updater"
"config"
] "Use services.clamav.updater.settings instead.")
(lib.mkRemovedOptionModule [
"services"
"clamav"
"updater"
"extraConfig"
] "Use services.clamav.updater.settings instead.")
(lib.mkRemovedOptionModule [
"services"
"clamav"
"daemon"
"extraConfig"
] "Use services.clamav.daemon.settings instead.")
];
options = {
services.clamav = {
package = lib.mkPackageOption pkgs "clamav" { };
daemon = {
enable = lib.mkEnableOption "ClamAV clamd daemon";
settings = lib.mkOption {
type =
with lib.types;
attrsOf (oneOf [
bool
int
str
(listOf str)
]);
default = { };
description = ''
ClamAV configuration. Refer to <https://linux.die.net/man/5/clamd.conf>,
for details on supported values.
'';
};
};
updater = {
enable = lib.mkEnableOption "ClamAV freshclam updater";
frequency = lib.mkOption {
type = lib.types.int;
default = 12;
description = ''
Number of database checks per day.
'';
};
interval = lib.mkOption {
type = lib.types.str;
default = "hourly";
description = ''
How often freshclam is invoked. See {manpage}`systemd.time(7)` for more
information about the format.
'';
};
settings = lib.mkOption {
type =
with lib.types;
attrsOf (oneOf [
bool
int
str
(listOf str)
]);
default = { };
description = ''
freshclam configuration. Refer to <https://linux.die.net/man/5/freshclam.conf>,
for details on supported values.
'';
};
};
fangfrisch = {
enable = lib.mkEnableOption "ClamAV fangfrisch updater";
interval = lib.mkOption {
type = lib.types.str;
default = "hourly";
description = ''
How often freshclam is invoked. See {manpage}`systemd.time(7)` for more
information about the format.
'';
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType =
with lib.types;
attrsOf (
attrsOf (oneOf [
str
int
bool
])
);
};
default = { };
example = {
securiteinfo = {
enabled = "yes";
customer_id = "your customer_id";
};
};
description = ''
fangfrisch configuration. Refer to <https://rseichter.github.io/fangfrisch/#_configuration>,
for details on supported values.
Note that by default urlhaus and sanesecurity are enabled.
'';
};
};
scanner = {
enable = lib.mkEnableOption "ClamAV scanner";
interval = lib.mkOption {
type = lib.types.str;
default = "*-*-* 04:00:00";
description = ''
How often clamdscan is invoked. See {manpage}`systemd.time(7)` for more
information about the format.
By default this runs using 10 cores at most, be sure to run it at a time of low traffic.
'';
};
scanDirectories = lib.mkOption {
type = with lib.types; listOf str;
default = [
"/home"
"/var/lib"
"/tmp"
"/etc"
"/var/tmp"
];
description = ''
List of directories to scan.
The default includes everything I could think of that is valid for nixos. Feel free to contribute a PR to add to the default if you see something missing.
'';
};
};
};
};
config = lib.mkIf (cfg.updater.enable || cfg.daemon.enable) {
environment.systemPackages = [ cfg.package ];
users.users.${clamavUser} = {
uid = config.ids.uids.clamav;
group = clamavGroup;
description = "ClamAV daemon user";
home = stateDir;
};
users.groups.${clamavGroup} = {
gid = config.ids.gids.clamav;
};
services.clamav.daemon.settings = {
DatabaseDirectory = stateDir;
LocalSocket = "/run/clamav/clamd.ctl";
PidFile = "/run/clamav/clamd.pid";
User = "clamav";
Foreground = true;
};
services.clamav.updater.settings = {
DatabaseDirectory = stateDir;
Foreground = true;
Checks = cfg.updater.frequency;
DatabaseMirror = [ "database.clamav.net" ];
};
services.clamav.fangfrisch.settings = {
DEFAULT.db_url = lib.mkDefault "sqlite:////var/lib/clamav/fangfrisch_db.sqlite";
DEFAULT.local_directory = lib.mkDefault stateDir;
DEFAULT.log_level = lib.mkDefault "INFO";
urlhaus.enabled = lib.mkDefault "yes";
urlhaus.max_size = lib.mkDefault "2MB";
sanesecurity.enabled = lib.mkDefault "yes";
};
environment.etc."clamav/freshclam.conf".source = freshclamConfigFile;
environment.etc."clamav/clamd.conf".source = clamdConfigFile;
systemd.slices.system-clamav = {
description = "ClamAV Antivirus Slice";
};
systemd.services.clamav-daemon = lib.mkIf cfg.daemon.enable {
description = "ClamAV daemon (clamd)";
documentation = [ "man:clamd(8)" ];
after = lib.optionals cfg.updater.enable [ "clamav-freshclam.service" ];
wants = lib.optionals cfg.updater.enable [ "clamav-freshclam.service" ];
wantedBy = [ "multi-user.target" ];
restartTriggers = [ clamdConfigFile ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/clamd";
ExecReload = "${pkgs.coreutils}/bin/kill -USR2 $MAINPID";
User = clamavUser;
Group = clamavGroup;
StateDirectory = "clamav";
RuntimeDirectory = "clamav";
PrivateTmp = "yes";
PrivateDevices = "yes";
PrivateNetwork = "yes";
Slice = "system-clamav.slice";
};
};
systemd.timers.clamav-freshclam = lib.mkIf cfg.updater.enable {
description = "Timer for ClamAV virus database updater (freshclam)";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = cfg.updater.interval;
Unit = "clamav-freshclam.service";
};
};
systemd.services.clamav-freshclam = lib.mkIf cfg.updater.enable {
description = "ClamAV virus database updater (freshclam)";
documentation = [ "man:freshclam(1)" ];
restartTriggers = [ freshclamConfigFile ];
requires = [ "network-online.target" ];
after = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${cfg.package}/bin/freshclam";
SuccessExitStatus = "1"; # if databases are up to date
StateDirectory = "clamav";
User = clamavUser;
Group = clamavGroup;
PrivateTmp = "yes";
PrivateDevices = "yes";
Slice = "system-clamav.slice";
};
};
systemd.services.clamav-fangfrisch-init = lib.mkIf cfg.fangfrisch.enable {
wantedBy = [ "multi-user.target" ];
# if the sqlite file can be found assume the database has already been initialised
script = ''
db_url="${cfg.fangfrisch.settings.DEFAULT.db_url}"
db_path="''${db_url#sqlite:///}"
if [ ! -f "$db_path" ]; then
${pkgs.fangfrisch}/bin/fangfrisch --conf ${fangfrischConfigFile} initdb
fi
'';
serviceConfig = {
Type = "oneshot";
StateDirectory = "clamav";
User = clamavUser;
Group = clamavGroup;
PrivateTmp = "yes";
PrivateDevices = "yes";
Slice = "system-clamav.slice";
};
};
systemd.timers.clamav-fangfrisch = lib.mkIf cfg.fangfrisch.enable {
description = "Timer for ClamAV virus database updater (fangfrisch)";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = cfg.fangfrisch.interval;
Unit = "clamav-fangfrisch.service";
};
};
systemd.services.clamav-fangfrisch = lib.mkIf cfg.fangfrisch.enable {
description = "ClamAV virus database updater (fangfrisch)";
restartTriggers = [ fangfrischConfigFile ];
requires = [ "network-online.target" ];
after = [
"network-online.target"
"clamav-fangfrisch-init.service"
];
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.fangfrisch}/bin/fangfrisch --conf ${fangfrischConfigFile} refresh";
StateDirectory = "clamav";
User = clamavUser;
Group = clamavGroup;
PrivateTmp = "yes";
PrivateDevices = "yes";
Slice = "system-clamav.slice";
};
};
systemd.timers.clamdscan = lib.mkIf cfg.scanner.enable {
description = "Timer for ClamAV virus scanner";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = cfg.scanner.interval;
Unit = "clamdscan.service";
};
};
systemd.services.clamdscan = lib.mkIf cfg.scanner.enable {
description = "ClamAV virus scanner";
documentation = [ "man:clamdscan(1)" ];
after = lib.optionals cfg.updater.enable [ "clamav-freshclam.service" ];
wants = lib.optionals cfg.updater.enable [ "clamav-freshclam.service" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${cfg.package}/bin/clamdscan --multiscan --fdpass --infected --allmatch ${lib.concatStringsSep " " cfg.scanner.scanDirectories}";
Slice = "system-clamav.slice";
};
};
};
}

View File

@@ -0,0 +1,992 @@
{
config,
pkgs,
lib,
...
}:
let
format = pkgs.formats.yaml { };
rootDir = "/var/lib/crowdsec";
stateDir = "${rootDir}/state";
confDir = "/etc/crowdsec/";
hubDir = "${stateDir}/hub/";
notificationsDir = "${confDir}/notifications/";
pluginDir = "${confDir}/plugins/";
parsersDir = "${confDir}/parsers/";
localPostOverflowsDir = "${confDir}/postoverflows/";
localPostOverflowsS01WhitelistDir = "${localPostOverflowsDir}/s01-whitelist/";
localScenariosDir = "${confDir}/scenarios/";
localParsersS00RawDir = "${parsersDir}/s00-raw/";
localParsersS01ParseDir = "${parsersDir}/s01-parse/";
localParsersS02EnrichDir = "${parsersDir}/s02-enrich/";
localContextsDir = "${confDir}/contexts/";
in
{
options.services.crowdsec = {
enable = lib.mkEnableOption "CrowdSec Security Engine";
package = lib.mkPackageOption pkgs "crowdsec" { };
autoUpdateService = lib.mkEnableOption "if `true` `cscli hub update` will be executed daily. See `https://docs.crowdsec.net/docs/cscli/cscli_hub_update/` for more information";
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = ''
Whether to automatically open firewall ports for `crowdsec`.
'';
};
user = lib.mkOption {
type = lib.types.str;
description = "The user to run crowdsec as";
default = "crowdsec";
};
group = lib.mkOption {
type = lib.types.str;
description = "The group to run crowdsec as";
default = "crowdsec";
};
name = lib.mkOption {
type = lib.types.str;
description = ''
Name of the machine when registering it at the central or local api.
'';
default = config.networking.hostName;
defaultText = lib.literalExpression "config.networking.hostName";
};
localConfig = lib.mkOption {
type = lib.types.submodule {
options = {
acquisitions = lib.mkOption {
type = lib.types.listOf format.type;
default = [ ];
description = ''
A list of acquisition specifications, which define the data sources you want to be parsed.
See <https://docs.crowdsec.net/docs/data_sources/intro> for details.
'';
example = [
{
source = "journalctl";
journalctl_filter = [ "_SYSTEMD_UNIT=sshd.service" ];
labels = {
type = "syslog";
};
}
];
};
scenarios = lib.mkOption {
type = lib.types.listOf format.type;
default = [ ];
description = ''
A list of scenarios specifications.
See <https://docs.crowdsec.net/docs/scenarios/intro> for details.
'';
example = [
{
type = "leaky";
name = "crowdsecurity/myservice-bf";
description = "Detect myservice bruteforce";
filter = "evt.Meta.log_type == 'myservice_failed_auth'";
leakspeed = "10s";
capacity = 5;
groupby = "evt.Meta.source_ip";
}
];
};
parsers = lib.mkOption {
type = lib.types.submodule {
options = {
s00Raw = lib.mkOption {
type = lib.types.listOf format.type;
default = [ ];
description = ''
A list of stage s00-raw specifications. Most of the time, those are already included in the hub, but are presented here anyway.
See <https://docs.crowdsec.net/docs/parsers/intro> for details.
'';
};
s01Parse = lib.mkOption {
type = lib.types.listOf format.type;
default = [ ];
description = ''
A list of stage s01-parse specifications.
See <https://docs.crowdsec.net/docs/parsers/intro> for details.
'';
example = [
{
filter = "1=1";
debug = true;
onsuccess = "next_stage";
name = "example/custom-service-logs";
description = "Parsing custom service logs";
grok = {
pattern = "^%{DATA:some_data}$";
apply_on = "message";
};
statics = [
{
parsed = "is_my_custom_service";
value = "yes";
}
];
}
];
};
s02Enrich = lib.mkOption {
type = lib.types.listOf format.type;
default = [ ];
description = ''
A list of stage s02-enrich specifications. Inside this list, you can specify Parser Whitelists.
See <https://docs.crowdsec.net/docs/whitelist/intro> for details.
'';
example = [
{
name = "myips/whitelist";
description = "Whitelist parse events from my IPs";
whitelist = {
reason = "My IP ranges";
ip = [
"1.2.3.4"
];
cidr = [
"1.2.3.0/24"
];
};
}
];
};
};
};
description = ''
The set of parser specifications.
See <https://docs.crowdsec.net/docs/parsers/intro> for details.
'';
default = { };
};
postOverflows = lib.mkOption {
type = lib.types.submodule {
options = {
s01Whitelist = lib.mkOption {
type = lib.types.listOf format.type;
default = [ ];
description = ''
A list of stage s01-whitelist specifications. Inside this list, you can specify Postoverflows Whitelists.
See <https://docs.crowdsec.net/docs/whitelist/intro> for details.
'';
example = [
{
name = "postoverflows/whitelist_my_dns_domain";
description = "Whitelist my reverse DNS";
whitelist = {
reason = "Don't ban me";
expression = [
"evt.Enriched.reverse_dns endsWith '.local.'"
];
};
}
];
};
};
};
description = ''
The set of Postoverflows specifications.
See <https://docs.crowdsec.net/docs/next/log_processor/parsers/intro#postoverflows> for details.
'';
default = { };
};
contexts = lib.mkOption {
type = lib.types.listOf format.type;
description = ''
A list of additional contexts to specify.
See <https://docs.crowdsec.net/docs/next/log_processor/alert_context/intro> for details.
'';
example = [
{
context = {
target_uri = [ "evt.Meta.http_path" ];
user_agent = [ "evt.Meta.http_user_agent" ];
method = [ "evt.Meta.http_verb" ];
status = [ "evt.Meta.http_status" ];
};
}
];
default = [ ];
};
notifications = lib.mkOption {
type = lib.types.listOf format.type;
description = ''
A list of notifications to enable and use in your profiles. Note that for now, only the plugins shipped by default with CrowdSec are supported.
See <https://docs.crowdsec.net/docs/notification_plugins/intro> for details.
'';
example = [
{
type = "http";
name = "default_http_notification";
log_level = "info";
format = ''
{{.|toJson}}
'';
url = "https://example.com/hook";
method = "POST";
}
];
default = [ ];
};
profiles = lib.mkOption {
type = lib.types.listOf format.type;
description = ''
A list of profiles to enable.
See <https://docs.crowdsec.net/docs/profiles/intro> for more details.
'';
example = [
{
name = "default_ip_remediation";
filters = [
"Alert.Remediation == true && Alert.GetScope() == 'Ip'"
];
decisions = [
{
type = "ban";
duration = "4h";
}
];
on_success = "break";
}
{
name = "default_range_remediation";
filters = [
"Alert.Remediation == true && Alert.GetScope() == 'Range'"
];
decisions = [
{
type = "ban";
duration = "4h";
}
];
on_success = "break";
}
];
default = [
{
name = "default_ip_remediation";
filters = [
"Alert.Remediation == true && Alert.GetScope() == 'Ip'"
];
decisions = [
{
type = "ban";
duration = "4h";
}
];
on_success = "break";
}
{
name = "default_range_remediation";
filters = [
"Alert.Remediation == true && Alert.GetScope() == 'Range'"
];
decisions = [
{
type = "ban";
duration = "4h";
}
];
on_success = "break";
}
];
};
patterns = lib.mkOption {
type = lib.types.listOf lib.types.package;
description = ''
A list of files containing custom grok patterns.
'';
default = [ ];
example = lib.literalExpression ''
[ (pkgs.writeTextDir "custom_service_logs" (builtins.readFile ./custom_service_logs)) ]
'';
};
};
};
description = ''
The configuration for a crowdsec security engine.
'';
default = { };
};
hub = lib.mkOption {
type = lib.types.submodule {
options = {
collections = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "List of hub collections to install";
example = [ "crowdsecurity/linux" ];
};
scenarios = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "List of hub scenarios to install";
example = [ "crowdsecurity/ssh-bf" ];
};
parsers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "List of hub parsers to install";
example = [ "crowdsecurity/sshd-logs" ];
};
postOverflows = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "List of hub postoverflows to install";
example = [ "crowdsecurity/auditd-nix-wrappers-whitelist-process" ];
};
appSecConfigs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "List of hub appsec configurations to install";
example = [ "crowdsecurity/appsec-default" ];
};
appSecRules = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "List of hub appsec rules to install";
example = [ "crowdsecurity/base-config" ];
};
branch = lib.mkOption {
type = lib.types.str;
default = "master";
description = ''
The git branch on which cscli is going to fetch configurations.
See `https://docs.crowdsec.net/docs/configuration/crowdsec_configuration/#hub_branch` for more information.
'';
example = [
"master"
"v1.4.3"
"v1.4.2"
];
};
};
};
default = { };
description = ''
Hub collections, parsers, AppSec rules, etc.
'';
};
settings = lib.mkOption {
type = lib.types.submodule {
options = {
general = lib.mkOption {
description = ''
Settings for the main CrowdSec configuration file.
Refer to the defaults at <https://github.com/crowdsecurity/crowdsec/blob/master/config/config.yaml>.
'';
type = format.type;
default = { };
};
simulation = lib.mkOption {
type = format.type;
default = {
simulation = false;
};
description = ''
Attributes inside the simulation.yaml file.
'';
};
lapi = lib.mkOption {
type = lib.types.submodule {
options = {
credentialsFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
example = "/run/crowdsec/lapi.yaml";
description = ''
The LAPI credential file to use.
'';
default = null;
};
};
};
description = ''
LAPI Configuration attributes
'';
default = { };
};
capi = lib.mkOption {
type = lib.types.submodule {
options = {
credentialsFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
example = "/run/crowdsec/capi.yaml";
description = ''
The CAPI credential file to use.
'';
default = null;
};
};
};
description = ''
CAPI Configuration attributes
'';
default = { };
};
console = lib.mkOption {
type = lib.types.submodule {
options = {
tokenFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
example = "/run/crowdsec/console_token.yaml";
description = ''
The Console Token file to use.
'';
default = null;
};
configuration = lib.mkOption {
type = format.type;
default = {
share_manual_decisions = false;
share_custom = false;
share_tainted = false;
share_context = false;
};
description = ''
Attributes inside the console.yaml file.
'';
};
};
};
description = ''
Console Configuration attributes
'';
default = { };
};
};
};
description = ''
Set of various configuration attributes
'';
};
};
config =
let
cfg = config.services.crowdsec;
configFile = format.generate "crowdsec.yaml" cfg.settings.general;
simulationFile = format.generate "simulation.yaml" cfg.settings.simulation;
consoleFile = format.generate "console.yaml" cfg.settings.console.configuration;
patternsDir = pkgs.buildPackages.symlinkJoin {
name = "crowdsec-patterns";
paths = [
cfg.localConfig.patterns
"${lib.attrsets.getOutput "out" cfg.package}/share/crowdsec/config/patterns/"
];
};
cscli = pkgs.writeShellScriptBin "cscli" ''
set -euo pipefail
# cscli needs crowdsec on it's path in order to be able to run `cscli explain`
export PATH="$PATH:${lib.makeBinPath [ cfg.package ]}"
sudo=exec
if [ "$USER" != "${cfg.user}" ]; then
${
if config.security.sudo.enable then
"sudo='exec ${config.security.wrapperDir}/sudo -u ${cfg.user}'"
else
">&2 echo 'Aborting, cscli must be run as user `${cfg.user}`!'; exit 2"
}
fi
$sudo ${lib.getExe' cfg.package "cscli"} -c=${configFile} "$@"
'';
localScenariosMap = (map (format.generate "scenario.yaml") cfg.localConfig.scenarios);
localParsersS00RawMap = (
map (format.generate "parsers-s00-raw.yaml") cfg.localConfig.parsers.s00Raw
);
localParsersS01ParseMap = (
map (format.generate "parsers-s01-parse.yaml") cfg.localConfig.parsers.s01Parse
);
localParsersS02EnrichMap = (
map (format.generate "parsers-s02-enrich.yaml") cfg.localConfig.parsers.s02Enrich
);
localPostOverflowsS01WhitelistMap = (
map (format.generate "postoverflows-s01-whitelist.yaml") cfg.localConfig.postOverflows.s01Whitelist
);
localContextsMap = (map (format.generate "context.yaml") cfg.localConfig.contexts);
localNotificationsMap = (map (format.generate "notification.yaml") cfg.localConfig.notifications);
localProfilesFile = pkgs.writeText "local_profiles.yaml" ''
---
${lib.strings.concatMapStringsSep "\n---\n" builtins.toJSON cfg.localConfig.profiles}
---
'';
localAcquisisionFile = pkgs.writeText "local_acquisisions.yaml" ''
---
${lib.strings.concatMapStringsSep "\n---\n" builtins.toJSON cfg.localConfig.acquisitions}
---
'';
scriptArray = [
"set -euo pipefail"
"${lib.getExe' pkgs.coreutils "mkdir"} -p '${hubDir}'"
"${lib.getExe cscli} hub update"
]
++ lib.optionals (cfg.hub.collections != [ ]) [
"${lib.getExe cscli} collections install ${
lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.collections
}"
]
++ lib.optionals (cfg.hub.scenarios != [ ]) [
"${lib.getExe cscli} scenarios install ${
lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.scenarios
}"
]
++ lib.optionals (cfg.hub.parsers != [ ]) [
"${lib.getExe cscli} parsers install ${
lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.parsers
}"
]
++ lib.optionals (cfg.hub.postOverflows != [ ]) [
"${lib.getExe cscli} postoverflows install ${
lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.postOverflows
}"
]
++ lib.optionals (cfg.hub.appSecConfigs != [ ]) [
"${lib.getExe cscli} appsec-configs install ${
lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.appSecConfigs
}"
]
++ lib.optionals (cfg.hub.appSecRules != [ ]) [
"${lib.getExe cscli} appsec-rules install ${
lib.strings.concatMapStringsSep " " (x: lib.escapeShellArg x) cfg.hub.appSecRules
}"
]
++ lib.optionals (cfg.settings.general.api.server.enable) [
''
if [ ! -s "${cfg.settings.general.api.client.credentials_path}" ]; then
${lib.getExe cscli} machine add "${cfg.name}" --auto
fi
''
]
++ lib.optionals (cfg.settings.capi.credentialsFile != null) [
''
if ! ${lib.getExe pkgs.gnugrep} -q password "${cfg.settings.capi.credentialsFile}" ]; then
${lib.getExe cscli} capi register
fi
''
]
++ lib.optionals (cfg.settings.console.tokenFile != null) [
''
if [ ! -e "${cfg.settings.console.tokenFile}" ]; then
${lib.getExe cscli} console enroll "$(cat ${cfg.settings.console.tokenFile})" --name ${cfg.name}
fi
''
];
setupScript = pkgs.writeShellScriptBin "crowdsec-setup" (
lib.strings.concatStringsSep "\n" scriptArray
);
in
lib.mkIf (cfg.enable) {
warnings =
[ ]
++ lib.optionals (cfg.localConfig.profiles == [ ]) [
"By not specifying profiles in services.crowdsec.localConfig.profiles, CrowdSec will not react to any alert by default."
]
++ lib.optionals (cfg.localConfig.acquisitions == [ ]) [
"By not specifying acquisitions in services.crowdsec.localConfig.acquisitions, CrowdSec will not look for any data source."
];
services.crowdsec.settings.general = {
common = {
daemonize = false;
log_media = "stdout";
};
config_paths = {
config_dir = confDir;
data_dir = stateDir;
simulation_path = simulationFile;
hub_dir = hubDir;
index_path = lib.strings.normalizePath "${stateDir}/hub/.index.json";
notification_dir = notificationsDir;
plugin_dir = pluginDir;
pattern_dir = patternsDir;
};
db_config = {
type = lib.mkDefault "sqlite";
db_path = lib.mkDefault (lib.strings.normalizePath "${stateDir}/crowdsec.db");
use_wal = lib.mkDefault true;
};
crowdsec_service = {
enable = lib.mkDefault true;
acquisition_path = lib.mkDefault localAcquisisionFile;
};
api = {
client = {
credentials_path = cfg.settings.lapi.credentialsFile;
};
server = {
enable = lib.mkDefault false;
listen_uri = lib.mkDefault "127.0.0.1:8080";
console_path = lib.mkDefault consoleFile;
profiles_path = lib.mkDefault localProfilesFile;
online_client = lib.mkDefault {
sharing = lib.mkDefault true;
pull = lib.mkDefault {
community = lib.mkDefault true;
blocklists = lib.mkDefault true;
};
credentials_path = cfg.settings.capi.credentialsFile;
};
};
};
prometheus = {
enabled = lib.mkDefault true;
level = lib.mkDefault "full";
listen_addr = lib.mkDefault "127.0.0.1";
listen_port = lib.mkDefault 6060;
};
cscli = {
hub_branch = cfg.hub.branch;
};
};
environment = {
systemPackages = [ cscli ];
};
systemd.packages = [ cfg.package ];
systemd.timers.crowdsec-update-hub = lib.mkIf (cfg.autoUpdateService) {
description = "Update the crowdsec hub index";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "daily";
RandomizedDelaySec = 300;
Persistent = "yes";
Unit = "crowdsec-update-hub.service";
};
};
systemd.services = {
crowdsec-update-hub = lib.mkIf (cfg.autoUpdateService) {
description = "Update the crowdsec hub index";
serviceConfig = {
Type = "oneshot";
User = cfg.user;
Group = cfg.group;
LimitNOFILE = 65536;
NoNewPrivileges = true;
LockPersonality = true;
RemoveIPC = true;
ReadWritePaths = [
rootDir
confDir
];
ProtectSystem = "strict";
PrivateUsers = true;
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectHostname = true;
UMask = "0077";
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
ProtectProc = "invisible";
SystemCallFilter = [
" " # This is needed to clear the SystemCallFilter existing definitions
"~@reboot"
"~@swap"
"~@obsolete"
"~@mount"
"~@module"
"~@debug"
"~@cpu-emulation"
"~@clock"
"~@raw-io"
"~@privileged"
"~@resources"
];
CapabilityBoundingSet = [
" " # Reset all capabilities to an empty set
];
RestrictAddressFamilies = [
" " # This is needed to clear the RestrictAddressFamilies existing definitions
"none" # Remove all addresses families
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
DevicePolicy = "closed";
ProtectKernelLogs = true;
SystemCallArchitectures = "native";
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
ExecStart = "${lib.getExe cscli} --error hub update";
ExecStartPost = "systemctl reload crowdsec.service";
DynamicUser = true;
};
};
crowdsec = {
description = "CrowdSec agent";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
path = lib.mkForce [ ];
environment = {
LC_ALL = "C";
LANG = "C";
};
serviceConfig = {
User = cfg.user;
Group = cfg.group;
Type = "notify";
RestartSec = 60;
LimitNOFILE = 65536;
NoNewPrivileges = true;
LockPersonality = true;
RemoveIPC = true;
ReadWritePaths = [
rootDir
confDir
];
ProtectSystem = "strict";
PrivateUsers = true;
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectHostname = true;
ProtectClock = true;
UMask = "0077";
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
ProtectProc = "invisible";
SystemCallFilter = [
" " # This is needed to clear the SystemCallFilter existing definitions
"~@reboot"
"~@swap"
"~@obsolete"
"~@mount"
"~@module"
"~@debug"
"~@cpu-emulation"
"~@clock"
"~@raw-io"
"~@privileged"
"~@resources"
];
CapabilityBoundingSet = [
" " # Reset all capabilities to an empty set
"CAP_SYSLOG" # Add capability to read syslog
];
RestrictAddressFamilies = [
" " # This is needed to clear the RestrictAddressFamilies existing definitions
"none" # Remove all addresses families
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
DevicePolicy = "closed";
ProtectKernelLogs = true;
SystemCallArchitectures = "native";
DynamicUser = true;
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
ExecReload = [
" " # This is needed to clear the ExecReload definitions from upstream
];
ExecStart = [
" " # This is needed to clear the ExecStart definitions from upstream
"${lib.getExe' cfg.package "crowdsec"} -c ${configFile} -info"
];
ExecStartPre = [
" " # This is needed to clear the ExecStartPre definitions from upstream
"${lib.getExe setupScript}"
"${lib.getExe' cfg.package "crowdsec"} -c ${configFile} -t -error"
];
};
};
};
systemd.tmpfiles.settings = {
"10-crowdsec" =
builtins.listToAttrs (
map
(dirName: {
inherit cfg;
name = lib.strings.normalizePath dirName;
value = {
d = {
user = cfg.user;
group = cfg.group;
mode = "0750";
};
};
})
[
stateDir
hubDir
confDir
localScenariosDir
localPostOverflowsDir
localPostOverflowsS01WhitelistDir
parsersDir
localParsersS00RawDir
localParsersS01ParseDir
localParsersS02EnrichDir
localContextsDir
notificationsDir
pluginDir
]
)
// builtins.listToAttrs (
map (scenarioFile: {
inherit cfg;
name = lib.strings.normalizePath "${localScenariosDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf scenarioFile)}";
value = {
link = {
type = "L+";
argument = "${scenarioFile}";
};
};
}) localScenariosMap
)
// builtins.listToAttrs (
map (parser: {
inherit cfg;
name = lib.strings.normalizePath "${localParsersS00RawDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf parser)}";
value = {
link = {
type = "L+";
argument = "${parser}";
};
};
}) localParsersS00RawMap
)
// builtins.listToAttrs (
map (parser: {
inherit cfg;
name = lib.strings.normalizePath "${localParsersS01ParseDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf parser)}";
value = {
link = {
type = "L+";
argument = "${parser}";
};
};
}) localParsersS01ParseMap
)
// builtins.listToAttrs (
map (parser: {
inherit cfg;
name = lib.strings.normalizePath "${localParsersS02EnrichDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf parser)}";
value = {
link = {
type = "L+";
argument = "${parser}";
};
};
}) localParsersS02EnrichMap
)
// builtins.listToAttrs (
map (postoverflow: {
inherit cfg;
name = lib.strings.normalizePath "${localPostOverflowsS01WhitelistDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf postoverflow)}";
value = {
link = {
type = "L+";
argument = "${postoverflow}";
};
};
}) localPostOverflowsS01WhitelistMap
)
// builtins.listToAttrs (
map (context: {
inherit cfg;
name = lib.strings.normalizePath "${localContextsDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf context)}";
value = {
link = {
type = "L+";
argument = "${context}";
};
};
}) localContextsMap
)
// builtins.listToAttrs (
map (notification: {
inherit cfg;
name = lib.strings.normalizePath "${notificationsDir}/${builtins.unsafeDiscardStringContext (builtins.baseNameOf notification)}";
value = {
link = {
type = "L+";
argument = "${notification}";
};
};
}) localNotificationsMap
);
};
users.users.${cfg.user} = {
name = cfg.user;
description = lib.mkDefault "CrowdSec service user";
isSystemUser = true;
group = cfg.group;
extraGroups = [ "systemd-journal" ];
};
users.groups.${cfg.group} = lib.mapAttrs (name: lib.mkDefault) { };
networking.firewall.allowedTCPPorts =
let
parsePortFromURLOption =
url: option:
builtins.addErrorContext "extracting a port from URL: `${option}` requires a port to be specified, but we failed to parse a port from '${url}'" (
lib.strings.toInt (lib.last (lib.strings.splitString ":" url))
);
in
lib.mkIf cfg.openFirewall [
cfg.settings.general.prometheus.listen_port
(parsePortFromURLOption cfg.settings.general.api.server.listen_uri "config.services.crowdsec.settings.general.api.server.listen_uri")
];
};
meta = {
maintainers = with lib.maintainers; [
m0ustach3
tornax
jk
];
};
}

View File

@@ -0,0 +1,50 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.e-imzo;
in
{
options = {
services.e-imzo = {
enable = lib.mkEnableOption "E-IMZO";
package = lib.mkPackageOption pkgs "e-imzo" {
extraDescription = "Official mirror deletes old versions as soon as they release new one. Feel free to use either unstable or your own custom e-imzo package and ping maintainer.";
};
};
};
config = lib.mkIf cfg.enable {
systemd.user.services.e-imzo = {
enable = true;
description = "E-IMZO, uzbek state web signing service";
documentation = [ "https://github.com/xinux-org/e-imzo" ];
after = [
"network-online.target"
"graphical.target"
];
wants = [
"network-online.target"
"graphical.target"
];
wantedBy = [ "default.target" ];
serviceConfig = {
Type = "simple";
Restart = "always";
RestartSec = 1;
ExecStart = lib.getExe cfg.package;
NoNewPrivileges = true;
SystemCallArchitectures = "native";
};
};
};
meta.maintainers = lib.teams.uzinfocom.members;
}

View File

@@ -0,0 +1,162 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.endlessh-go;
in
{
options.services.endlessh-go = {
enable = lib.mkEnableOption "endlessh-go service";
package = lib.mkPackageOption pkgs "endlessh-go" { };
listenAddress = lib.mkOption {
type = lib.types.str;
default = "0.0.0.0";
example = "[::]";
description = ''
Interface address to bind the endlessh-go daemon to SSH connections.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 2222;
example = 22;
description = ''
Specifies on which port the endlessh-go daemon listens for SSH
connections.
Setting this to `22` may conflict with {option}`services.openssh`.
'';
};
prometheus = {
enable = lib.mkEnableOption "Prometheus integration";
listenAddress = lib.mkOption {
type = lib.types.str;
default = "0.0.0.0";
example = "[::]";
description = ''
Interface address to bind the endlessh-go daemon to answer Prometheus
queries.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 2112;
example = 9119;
description = ''
Specifies on which port the endlessh-go daemon listens for Prometheus
queries.
'';
};
};
extraOptions = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
example = [
"-conn_type=tcp4"
"-max_clients=8192"
];
description = ''
Additional command line options to pass to the endlessh-go daemon.
'';
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to open a firewall port for the SSH listener.
'';
};
};
config = lib.mkIf cfg.enable {
systemd.services.endlessh-go = {
description = "SSH tarpit";
requires = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig =
let
needsPrivileges = cfg.port < 1024 || cfg.prometheus.port < 1024;
capabilities = [ "" ] ++ lib.optionals needsPrivileges [ "CAP_NET_BIND_SERVICE" ];
rootDirectory = "/run/endlessh-go";
in
{
Restart = "always";
ExecStart =
with cfg;
lib.concatStringsSep " " (
[
(lib.getExe cfg.package)
"-logtostderr"
"-host=${listenAddress}"
"-port=${toString port}"
]
++ lib.optionals prometheus.enable [
"-enable_prometheus"
"-prometheus_host=${prometheus.listenAddress}"
"-prometheus_port=${toString prometheus.port}"
]
++ extraOptions
);
DynamicUser = true;
RootDirectory = rootDirectory;
BindReadOnlyPaths = [
builtins.storeDir
"-/etc/hosts"
"-/etc/localtime"
"-/etc/nsswitch.conf"
"-/etc/resolv.conf"
];
InaccessiblePaths = [ "-+${rootDirectory}" ];
RuntimeDirectory = baseNameOf rootDirectory;
RuntimeDirectoryMode = "700";
AmbientCapabilities = capabilities;
CapabilityBoundingSet = capabilities;
UMask = "0077";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = !needsPrivileges;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
ProtectProc = "noaccess";
ProcSubset = "pid";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
};
};
networking.firewall.allowedTCPPorts = with cfg; lib.optionals openFirewall [ port ];
};
meta.maintainers = with lib.maintainers; [ azahi ];
}

View File

@@ -0,0 +1,117 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.endlessh;
in
{
options.services.endlessh = {
enable = lib.mkEnableOption "endlessh service";
port = lib.mkOption {
type = lib.types.port;
default = 2222;
example = 22;
description = ''
Specifies on which port the endlessh daemon listens for SSH
connections.
Setting this to `22` may conflict with {option}`services.openssh`.
'';
};
extraOptions = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
example = [
"-6"
"-d 9000"
"-v"
];
description = ''
Additional command line options to pass to the endlessh daemon.
'';
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to open a firewall port for the SSH listener.
'';
};
};
config = lib.mkIf cfg.enable {
systemd.services.endlessh = {
description = "SSH tarpit";
documentation = [ "man:endlessh(1)" ];
requires = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig =
let
needsPrivileges = cfg.port < 1024;
capabilities = [ "" ] ++ lib.optionals needsPrivileges [ "CAP_NET_BIND_SERVICE" ];
rootDirectory = "/run/endlessh";
in
{
Restart = "always";
ExecStart =
with cfg;
lib.concatStringsSep " " (
[
"${pkgs.endlessh}/bin/endlessh"
"-p ${toString port}"
]
++ extraOptions
);
DynamicUser = true;
RootDirectory = rootDirectory;
BindReadOnlyPaths = [ builtins.storeDir ];
InaccessiblePaths = [ "-+${rootDirectory}" ];
RuntimeDirectory = baseNameOf rootDirectory;
RuntimeDirectoryMode = "700";
AmbientCapabilities = capabilities;
CapabilityBoundingSet = capabilities;
UMask = "0077";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = !needsPrivileges;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
ProtectProc = "noaccess";
ProcSubset = "pid";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@resources"
"~@privileged"
];
};
};
networking.firewall.allowedTCPPorts = with cfg; lib.optionals openFirewall [ port ];
};
meta.maintainers = with lib.maintainers; [ azahi ];
}

View File

@@ -0,0 +1,70 @@
{
lib,
config,
pkgs,
...
}:
let
cfg = config.services.esdm;
in
{
imports = [
# removed option 'services.esdm.cuseRandomEnable'
(lib.mkRemovedOptionModule [ "services" "esdm" "cuseRandomEnable" ] ''
Use services.esdm.enableLinuxCompatServices instead.
'')
# removed option 'services.esdm.cuseUrandomEnable'
(lib.mkRemovedOptionModule [ "services" "esdm" "cuseUrandomEnable" ] ''
Use services.esdm.enableLinuxCompatServices instead.
'')
# removed option 'services.esdm.procEnable'
(lib.mkRemovedOptionModule [ "services" "esdm" "procEnable" ] ''
Use services.esdm.enableLinuxCompatServices instead.
'')
# removed option 'services.esdm.verbose'
(lib.mkRemovedOptionModule [ "services" "esdm" "verbose" ] ''
There is no replacement.
'')
];
options.services.esdm = {
enable = lib.mkEnableOption "ESDM service configuration";
package = lib.mkPackageOption pkgs "esdm" { };
enableLinuxCompatServices = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Enable /dev/random, /dev/urandom and /proc/sys/kernel/random/* userspace wrapper.
'';
};
};
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
systemd.packages = [ cfg.package ];
systemd.services."esdm-server".wantedBy = [ "basic.target" ];
}
# It is necessary to set those options for these services to be started by systemd in NixOS
(lib.mkIf cfg.enableLinuxCompatServices {
systemd.targets."esdm-linux-compat".wantedBy = [ "basic.target" ];
systemd.services."esdm-server-suspend".wantedBy = [
"sleep.target"
"suspend.target"
"hibernate.target"
];
systemd.services."esdm-server-resume".wantedBy = [
"sleep.target"
"suspend.target"
"hibernate.target"
];
})
]
);
meta.maintainers = with lib.maintainers; [
orichter
thillux
];
}

View File

@@ -0,0 +1,464 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.fail2ban;
settingsFormat = pkgs.formats.keyValue { };
configFormat = pkgs.formats.ini {
mkKeyValue = lib.generators.mkKeyValueDefault { } " = ";
};
mkJailConfig =
name: attrs:
lib.optionalAttrs (name != "DEFAULT") { inherit (attrs) enabled; }
// lib.optionalAttrs (attrs.filter != null) {
filter = if (builtins.isString lib.filter) then lib.filter else name;
}
// attrs.settings;
mkFilter =
name: attrs:
lib.nameValuePair "fail2ban/filter.d/${name}.conf" {
source = configFormat.generate "filter.d/${name}.conf" attrs.filter;
};
fail2banConf = configFormat.generate "fail2ban.local" cfg.daemonSettings;
strJails = lib.filterAttrs (_: builtins.isString) cfg.jails;
attrsJails = lib.filterAttrs (_: builtins.isAttrs) cfg.jails;
jailConf =
let
configFile = configFormat.generate "jail.local" (
{ INCLUDES.before = "paths-nixos.conf"; } // (lib.mapAttrs mkJailConfig attrsJails)
);
extraConfig = lib.concatStringsSep "\n" (
lib.attrValues (
lib.mapAttrs (
name: def:
lib.optionalString (def != "") ''
[${name}]
${def}
''
) strJails
)
);
in
pkgs.concatText "jail.local" [
configFile
(pkgs.writeText "extra-jail.local" extraConfig)
];
pathsConf = pkgs.writeText "paths-nixos.conf" ''
# NixOS
[INCLUDES]
before = paths-common.conf
after = paths-overrides.local
[DEFAULT]
'';
in
{
imports = [
(lib.mkRemovedOptionModule [
"services"
"fail2ban"
"daemonConfig"
] "The daemon is now configured through the attribute set `services.fail2ban.daemonSettings`.")
(lib.mkRemovedOptionModule [ "services" "fail2ban" "extraSettings" ]
"The extra default configuration can now be set using `services.fail2ban.jails.DEFAULT.settings`."
)
];
###### interface
options = {
services.fail2ban = {
enable = lib.mkOption {
default = false;
type = lib.types.bool;
description = ''
Whether to enable the fail2ban service.
See the documentation of [](#opt-services.fail2ban.jails)
for what jails are enabled by default.
'';
};
package = lib.mkPackageOption pkgs "fail2ban" {
example = "fail2ban_0_11";
};
packageFirewall = lib.mkOption {
default = config.networking.firewall.package;
defaultText = lib.literalExpression "config.networking.firewall.package";
type = lib.types.package;
description = "The firewall package used by fail2ban service. Defaults to the package for your firewall (iptables or nftables).";
};
extraPackages = lib.mkOption {
default = [ ];
type = lib.types.listOf lib.types.package;
example = lib.literalExpression "[ pkgs.ipset ]";
description = ''
Extra packages to be made available to the fail2ban service. The example contains
the packages needed by the `iptables-ipset-proto6` action.
'';
};
bantime = lib.mkOption {
default = "10m";
type = lib.types.str;
example = "1h";
description = "Number of seconds that a host is banned.";
};
maxretry = lib.mkOption {
default = 3;
type = lib.types.ints.unsigned;
description = "Number of failures before a host gets banned.";
};
banaction = lib.mkOption {
default = if config.networking.nftables.enable then "nftables-multiport" else "iptables-multiport";
defaultText = lib.literalExpression ''if config.networking.nftables.enable then "nftables-multiport" else "iptables-multiport"'';
type = lib.types.str;
description = ''
Default banning action (e.g. iptables, iptables-new, iptables-multiport,
iptables-ipset-proto6-allports, shorewall, etc). It is used to
define action_* variables. Can be overridden globally or per
section within jail.local file
'';
};
banaction-allports = lib.mkOption {
default = if config.networking.nftables.enable then "nftables-allports" else "iptables-allports";
defaultText = lib.literalExpression ''if config.networking.nftables.enable then "nftables-allports" else "iptables-allports"'';
type = lib.types.str;
description = ''
Default banning action (e.g. iptables, iptables-new, iptables-multiport,
shorewall, etc) for "allports" jails. It is used to define action_* variables. Can be overridden
globally or per section within jail.local file
'';
};
bantime-increment.enable = lib.mkOption {
default = false;
type = lib.types.bool;
description = ''
"bantime.increment" allows to use database for searching of previously banned ip's to increase
a default ban time using special formula, default it is banTime * 1, 2, 4, 8, 16, 32 ...
'';
};
bantime-increment.rndtime = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
example = "8m";
description = ''
"bantime.rndtime" is the max number of seconds using for mixing with random time
to prevent "clever" botnets calculate exact time IP can be unbanned again
'';
};
bantime-increment.maxtime = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
example = "48h";
description = ''
"bantime.maxtime" is the max number of seconds using the ban time can reach (don't grows further)
'';
};
bantime-increment.factor = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
example = "4";
description = ''
"bantime.factor" is a coefficient to calculate exponent growing of the formula or common multiplier,
default value of factor is 1 and with default value of formula, the ban time grows by 1, 2, 4, 8, 16 ...
'';
};
bantime-increment.formula = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
example = "ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)";
description = ''
"bantime.formula" used by default to calculate next value of ban time, default value below,
the same ban time growing will be reached by multipliers 1, 2, 4, 8, 16, 32 ...
'';
};
bantime-increment.multipliers = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
example = "1 2 4 8 16 32 64";
description = ''
"bantime.multipliers" used to calculate next value of ban time instead of formula, corresponding
previously ban count and given "bantime.factor" (for multipliers default is 1);
following example grows ban time by 1, 2, 4, 8, 16 ... and if last ban count greater as multipliers count,
always used last multiplier (64 in example), for factor '1' and original ban time 600 - 10.6 hours
'';
};
bantime-increment.overalljails = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.bool;
example = true;
description = ''
"bantime.overalljails" (if true) specifies the search of IP in the database will be executed
cross over all jails, if false (default), only current jail of the ban IP will be searched.
'';
};
ignoreIP = lib.mkOption {
default = [ ];
type = lib.types.listOf lib.types.str;
example = [
"192.168.0.0/16"
"2001:DB8::42"
];
description = ''
"ignoreIP" can be a list of IP addresses, CIDR masks or DNS hosts. Fail2ban will not ban a host which
matches an address in this list. Several addresses can be defined using space (and/or comma) separator.
'';
};
daemonSettings = lib.mkOption {
inherit (configFormat) type;
defaultText = lib.literalExpression ''
{
Definition = {
logtarget = "SYSLOG";
socket = "/run/fail2ban/fail2ban.sock";
pidfile = "/run/fail2ban/fail2ban.pid";
dbfile = "/var/lib/fail2ban/fail2ban.sqlite3";
};
}
'';
description = ''
The contents of Fail2ban's main configuration file.
It's generally not necessary to change it.
'';
};
jails = lib.mkOption {
default = { };
example = lib.literalExpression ''
{
apache-nohome-iptables = {
settings = {
# Block an IP address if it accesses a non-existent
# home directory more than 5 times in 10 minutes,
# since that indicates that it's scanning.
filter = "apache-nohome";
action = '''iptables-multiport[name=HTTP, port="http,https"]''';
logpath = "/var/log/httpd/error_log*";
backend = "auto";
findtime = 600;
bantime = 600;
maxretry = 5;
};
};
dovecot = {
settings = {
# block IPs which failed to log-in
# aggressive mode add blocking for aborted connections
filter = "dovecot[mode=aggressive]";
maxretry = 3;
};
};
};
'';
type =
with lib.types;
attrsOf (
either lines (
submodule (
{ name, ... }:
{
options = {
enabled = lib.mkEnableOption "this jail" // {
default = true;
readOnly = name == "DEFAULT";
};
filter = lib.mkOption {
type = nullOr (either str configFormat.type);
default = null;
description = "Content of the filter used for this jail.";
};
settings = lib.mkOption {
inherit (settingsFormat) type;
default = { };
description = "Additional settings for this jail.";
};
};
}
)
)
);
description = ''
The configuration of each Fail2ban jail. A jail
consists of an action (such as blocking a port using
{command}`iptables`) that is triggered when a
filter applied to a log file triggers more than a certain
number of times in a certain time period. Actions are
defined in {file}`/etc/fail2ban/action.d`,
while filters are defined in
{file}`/etc/fail2ban/filter.d`.
NixOS comes with a default `sshd` jail;
for it to work well,
[](#opt-services.openssh.settings.LogLevel) should be set to
`"VERBOSE"` or higher so that fail2ban
can observe failed login attempts.
This module sets it to `"VERBOSE"` if
not set otherwise, so enabling fail2ban can make SSH logs
more verbose.
'';
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.bantime-increment.formula == null || cfg.bantime-increment.multipliers == null;
message = ''
Options `services.fail2ban.bantime-increment.formula` and `services.fail2ban.bantime-increment.multipliers` cannot be both specified.
'';
}
];
warnings = lib.mkIf (!config.networking.firewall.enable && !config.networking.nftables.enable) [
"fail2ban can not be used without a firewall"
];
environment.systemPackages = [ cfg.package ];
environment.etc = {
"fail2ban/fail2ban.local".source = fail2banConf;
"fail2ban/jail.local".source = jailConf;
"fail2ban/fail2ban.conf".source = "${cfg.package}/etc/fail2ban/fail2ban.conf";
"fail2ban/jail.conf".source = "${cfg.package}/etc/fail2ban/jail.conf";
"fail2ban/paths-common.conf".source = "${cfg.package}/etc/fail2ban/paths-common.conf";
"fail2ban/paths-nixos.conf".source = pathsConf;
"fail2ban/action.d".source = "${cfg.package}/etc/fail2ban/action.d/*.conf";
"fail2ban/filter.d".source = "${cfg.package}/etc/fail2ban/filter.d/*.conf";
}
// (lib.mapAttrs' mkFilter (
lib.filterAttrs (_: v: v.filter != null && !builtins.isString v.filter) attrsJails
));
systemd.packages = [ cfg.package ];
systemd.services.fail2ban = {
wantedBy = [ "multi-user.target" ];
partOf = lib.optional config.networking.firewall.enable "firewall.service";
restartTriggers = [
fail2banConf
jailConf
pathsConf
];
path = [
cfg.package
cfg.packageFirewall
pkgs.iproute2
]
++ cfg.extraPackages;
serviceConfig = {
# Capabilities
CapabilityBoundingSet = [
"CAP_AUDIT_READ"
"CAP_DAC_READ_SEARCH"
"CAP_NET_ADMIN"
"CAP_NET_RAW"
];
# Security
NoNewPrivileges = true;
# Directory
RuntimeDirectory = "fail2ban";
RuntimeDirectoryMode = "0750";
StateDirectory = "fail2ban";
StateDirectoryMode = "0750";
LogsDirectory = "fail2ban";
LogsDirectoryMode = "0750";
# Sandboxing
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectHostname = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
};
};
# Defaults for the daemon settings
services.fail2ban.daemonSettings.Definition = {
logtarget = lib.mkDefault "SYSLOG";
socket = lib.mkDefault "/run/fail2ban/fail2ban.sock";
pidfile = lib.mkDefault "/run/fail2ban/fail2ban.pid";
dbfile = lib.mkDefault "/var/lib/fail2ban/fail2ban.sqlite3";
};
# Add some reasonable default jails. The special "DEFAULT" jail
# sets default values for all other jails.
services.fail2ban.jails = lib.mkMerge [
{
DEFAULT.settings =
(lib.optionalAttrs cfg.bantime-increment.enable (
{
"bantime.increment" = cfg.bantime-increment.enable;
}
// (lib.mapAttrs' (name: lib.nameValuePair "bantime.${name}") (
lib.filterAttrs (n: v: v != null && n != "enable") cfg.bantime-increment
))
))
// {
# Miscellaneous options
inherit (cfg) banaction maxretry bantime;
ignoreip = ''127.0.0.1/8 ${lib.optionalString config.networking.enableIPv6 "::1"} ${lib.concatStringsSep " " cfg.ignoreIP}'';
backend = "systemd";
# Actions
banaction_allports = cfg.banaction-allports;
};
}
# Block SSH if there are too many failing connection attempts.
(lib.mkIf config.services.openssh.enable {
sshd.settings.port = lib.mkDefault (
lib.concatMapStringsSep "," builtins.toString config.services.openssh.ports
);
})
];
# Benefits from verbose sshd logging to observe failed login attempts,
# so we set that here unless the user overrode it.
services.openssh.settings.LogLevel = lib.mkDefault "VERBOSE";
};
}

View File

@@ -0,0 +1,64 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.fprintd;
fprintdPkg = if cfg.tod.enable then pkgs.fprintd-tod else pkgs.fprintd;
in
{
###### interface
options = {
services.fprintd = {
enable = lib.mkEnableOption "fprintd daemon and PAM module for fingerprint readers handling";
package = lib.mkOption {
type = lib.types.package;
default = fprintdPkg;
defaultText = lib.literalExpression "if config.services.fprintd.tod.enable then pkgs.fprintd-tod else pkgs.fprintd";
description = ''
fprintd package to use.
'';
};
tod = {
enable = lib.mkEnableOption "Touch OEM Drivers library support";
driver = lib.mkOption {
type = lib.types.package;
example = lib.literalExpression "pkgs.libfprint-2-tod1-goodix";
description = ''
Touch OEM Drivers (TOD) package to use.
'';
};
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
services.dbus.packages = [ cfg.package ];
environment.systemPackages = [ cfg.package ];
systemd.packages = [ cfg.package ];
systemd.services.fprintd.environment = lib.mkIf cfg.tod.enable {
FP_TOD_DRIVERS_DIR = "${cfg.tod.driver}${cfg.tod.driver.driverPath}";
};
};
}

View File

@@ -0,0 +1,90 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.haveged;
in
{
###### interface
options = {
services.haveged = {
enable = lib.mkEnableOption ''
haveged entropy daemon, which refills /dev/random when low.
NOTE: does nothing on kernels newer than 5.6
'';
# source for the note https://github.com/jirka-h/haveged/issues/57
refill_threshold = lib.mkOption {
type = lib.types.int;
default = 1024;
description = ''
The number of bits of available entropy beneath which
haveged should refill the entropy pool.
'';
};
};
};
config = lib.mkIf cfg.enable {
# https://github.com/jirka-h/haveged/blob/a4b69d65a8dfc5a9f52ff8505c7f58dcf8b9234f/contrib/Fedora/haveged.service
systemd.services.haveged = {
description = "Entropy Daemon based on the HAVEGE algorithm";
unitConfig = {
Documentation = "man:haveged(8)";
DefaultDependencies = false;
ConditionKernelVersion = "<5.6";
};
wantedBy = [ "sysinit.target" ];
after = [ "systemd-tmpfiles-setup-dev.service" ];
before = [
"sysinit.target"
"shutdown.target"
"systemd-journald.service"
];
serviceConfig = {
ExecStart = "${pkgs.haveged}/bin/haveged -w ${toString cfg.refill_threshold} --Foreground -v 1";
Restart = "always";
SuccessExitStatus = "137 143";
SecureBits = "noroot-locked";
CapabilityBoundingSet = [
"CAP_SYS_ADMIN"
"CAP_SYS_CHROOT"
];
# We can *not* set PrivateTmp=true as it can cause an ordering cycle.
PrivateTmp = false;
PrivateDevices = true;
ProtectSystem = "full";
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
RestrictNamespaces = true;
RestrictRealtime = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"newuname"
"~@mount"
];
SystemCallErrorNumber = "EPERM";
};
};
};
}

View File

@@ -0,0 +1,115 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.hockeypuck;
settingsFormat = pkgs.formats.toml { };
in
{
meta.maintainers = [ ];
options.services.hockeypuck = {
enable = lib.mkEnableOption "Hockeypuck OpenPGP Key Server";
port = lib.mkOption {
default = 11371;
type = lib.types.port;
description = "HKP port to listen on.";
};
settings = lib.mkOption {
type = settingsFormat.type;
default = { };
example = lib.literalExpression ''
{
hockeypuck = {
loglevel = "INFO";
logfile = "/var/log/hockeypuck/hockeypuck.log";
indexTemplate = "''${pkgs.hockeypuck-web}/share/templates/index.html.tmpl";
vindexTemplate = "''${pkgs.hockeypuck-web}/share/templates/index.html.tmpl";
statsTemplate = "''${pkgs.hockeypuck-web}/share/templates/stats.html.tmpl";
webroot = "''${pkgs.hockeypuck-web}/share/webroot";
hkp.bind = ":''${toString cfg.port}";
openpgp.db = {
driver = "postgres-jsonb";
dsn = "database=hockeypuck host=/var/run/postgresql sslmode=disable";
};
};
}
'';
description = ''
Configuration file for hockeypuck, here you can override
certain settings (`loglevel` and
`openpgp.db.dsn`) by just setting those values.
For other settings you need to use lib.mkForce to override them.
This service doesn't provision or enable postgres on your
system, it rather assumes that you enable postgres and create
the database yourself.
Example:
```
services.postgresql = {
enable = true;
ensureDatabases = [ "hockeypuck" ];
ensureUsers = [{
name = "hockeypuck";
ensureDBOwnership = true;
}];
};
```
'';
};
};
config = lib.mkIf cfg.enable {
services.hockeypuck.settings.hockeypuck = {
loglevel = lib.mkDefault "INFO";
logfile = "/var/log/hockeypuck/hockeypuck.log";
indexTemplate = "${pkgs.hockeypuck-web}/share/templates/index.html.tmpl";
vindexTemplate = "${pkgs.hockeypuck-web}/share/templates/index.html.tmpl";
statsTemplate = "${pkgs.hockeypuck-web}/share/templates/stats.html.tmpl";
webroot = "${pkgs.hockeypuck-web}/share/webroot";
hkp.bind = ":${toString cfg.port}";
openpgp.db = {
driver = "postgres-jsonb";
dsn = lib.mkDefault "database=hockeypuck host=/var/run/postgresql sslmode=disable";
};
};
users.users.hockeypuck = {
isSystemUser = true;
group = "hockeypuck";
description = "Hockeypuck user";
};
users.groups.hockeypuck = { };
systemd.services.hockeypuck = {
description = "Hockeypuck OpenPGP Key Server";
after = [
"network.target"
"postgresql.target"
];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
WorkingDirectory = "/var/lib/hockeypuck";
User = "hockeypuck";
ExecStart = "${pkgs.hockeypuck}/bin/hockeypuck -config ${settingsFormat.generate "config.toml" cfg.settings}";
Restart = "always";
RestartSec = "5s";
LogsDirectory = "hockeypuck";
LogsDirectoryMode = "0755";
StateDirectory = "hockeypuck";
};
};
};
}

View File

@@ -0,0 +1,69 @@
{
pkgs,
config,
lib,
...
}:
let
cfg = config.services.hologram-agent;
cfgFile = pkgs.writeText "hologram-agent.json" (
builtins.toJSON {
host = cfg.dialAddress;
}
);
in
{
options = {
services.hologram-agent = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to enable the Hologram agent for AWS instance credentials";
};
dialAddress = lib.mkOption {
type = lib.types.str;
default = "localhost:3100";
description = "Hologram server and port.";
};
httpPort = lib.mkOption {
type = lib.types.str;
default = "80";
description = "Port for metadata service to listen on.";
};
};
};
config = lib.mkIf cfg.enable {
boot.kernelModules = [ "dummy" ];
networking.interfaces.dummy0.ipv4.addresses = [
{
address = "169.254.169.254";
prefixLength = 32;
}
];
systemd.services.hologram-agent = {
description = "Provide EC2 instance credentials to machines outside of EC2";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
requires = [
"network-link-dummy0.service"
"network-addresses-dummy0.service"
];
preStart = ''
/run/current-system/sw/bin/rm -fv /run/hologram.sock
'';
serviceConfig = {
ExecStart = "${pkgs.hologram}/bin/hologram-agent -debug -conf ${cfgFile} -port ${cfg.httpPort}";
};
};
};
meta.maintainers = [ ];
}

View File

@@ -0,0 +1,135 @@
{
pkgs,
config,
lib,
...
}:
let
cfg = config.services.hologram-server;
cfgFile = pkgs.writeText "hologram-server.json" (
builtins.toJSON {
ldap = {
host = cfg.ldapHost;
bind = {
dn = cfg.ldapBindDN;
password = cfg.ldapBindPassword;
};
insecureldap = cfg.ldapInsecure;
userattr = cfg.ldapUserAttr;
baseDN = cfg.ldapBaseDN;
enableldapRoles = cfg.enableLdapRoles;
roleAttr = cfg.roleAttr;
groupClassAttr = cfg.groupClassAttr;
};
aws = {
account = cfg.awsAccount;
defaultrole = cfg.awsDefaultRole;
};
stats = cfg.statsAddress;
listen = cfg.listenAddress;
cachetimeout = cfg.cacheTimeoutSeconds;
}
);
in
{
options = {
services.hologram-server = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to enable the Hologram server for AWS instance credentials";
};
listenAddress = lib.mkOption {
type = lib.types.str;
default = "0.0.0.0:3100";
description = "Address and port to listen on";
};
ldapHost = lib.mkOption {
type = lib.types.str;
description = "Address of the LDAP server to use";
};
ldapInsecure = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to connect to LDAP over SSL or not";
};
ldapUserAttr = lib.mkOption {
type = lib.types.str;
default = "cn";
description = "The LDAP attribute for usernames";
};
ldapBaseDN = lib.mkOption {
type = lib.types.str;
description = "The base DN for your Hologram users";
};
ldapBindDN = lib.mkOption {
type = lib.types.str;
description = "DN of account to use to query the LDAP server";
};
ldapBindPassword = lib.mkOption {
type = lib.types.str;
description = "Password of account to use to query the LDAP server";
};
enableLdapRoles = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to assign user roles based on the user's LDAP group memberships";
};
groupClassAttr = lib.mkOption {
type = lib.types.str;
default = "groupOfNames";
description = "The objectclass attribute to search for groups when enableLdapRoles is true";
};
roleAttr = lib.mkOption {
type = lib.types.str;
default = "businessCategory";
description = "Which LDAP group attribute to search for authorized role ARNs";
};
awsAccount = lib.mkOption {
type = lib.types.str;
description = "AWS account number";
};
awsDefaultRole = lib.mkOption {
type = lib.types.str;
description = "AWS default role";
};
statsAddress = lib.mkOption {
type = lib.types.str;
default = "";
description = "Address of statsd server";
};
cacheTimeoutSeconds = lib.mkOption {
type = lib.types.int;
default = 3600;
description = "How often (in seconds) to refresh the LDAP cache";
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.hologram-server = {
description = "Provide EC2 instance credentials to machines outside of EC2";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${pkgs.hologram}/bin/hologram-server --debug --conf ${cfgFile}";
};
};
};
}

View File

@@ -0,0 +1,63 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.infnoise;
in
{
options = {
services.infnoise = {
enable = lib.mkEnableOption "the Infinite Noise TRNG driver";
fillDevRandom = lib.mkOption {
description = ''
Whether to run the infnoise driver as a daemon to refill /dev/random.
If disabled, you can use the `infnoise` command-line tool to
manually obtain randomness.
'';
type = lib.types.bool;
default = true;
};
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ pkgs.infnoise ];
services.udev.extraRules = ''
SUBSYSTEM=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6015", SYMLINK+="infnoise", TAG+="systemd", GROUP="dialout", MODE="0664", ENV{SYSTEMD_WANTS}="infnoise.service"
'';
systemd.services.infnoise = lib.mkIf cfg.fillDevRandom {
description = "Infinite Noise TRNG driver";
bindsTo = [ "dev-infnoise.device" ];
after = [ "dev-infnoise.device" ];
serviceConfig = {
ExecStart = "${pkgs.infnoise}/bin/infnoise --dev-random --debug";
Restart = "always";
User = "infnoise";
DynamicUser = true;
SupplementaryGroups = [ "dialout" ];
DeviceAllow = [ "/dev/infnoise" ];
DevicePolicy = "closed";
PrivateNetwork = true;
ProtectSystem = "strict";
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true; # only reads entropy pool size and watermark
RestrictNamespaces = true;
RestrictRealtime = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
};
};
};
}

View File

@@ -0,0 +1,38 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.intune;
in
{
options.services.intune = {
enable = lib.mkEnableOption "Microsoft Intune";
};
config = lib.mkIf cfg.enable {
users.users.microsoft-identity-broker = {
group = "microsoft-identity-broker";
isSystemUser = true;
};
users.groups.microsoft-identity-broker = { };
environment.systemPackages = [
pkgs.microsoft-identity-broker
pkgs.intune-portal
];
systemd.packages = [
pkgs.microsoft-identity-broker
pkgs.intune-portal
];
systemd.tmpfiles.packages = [ pkgs.intune-portal ];
services.dbus.packages = [ pkgs.microsoft-identity-broker ];
};
meta = {
maintainers = with lib.maintainers; [ rhysmdnz ];
};
}

View File

@@ -0,0 +1,22 @@
{
lib,
config,
pkgs,
...
}:
let
cfg = config.services.jitterentropy-rngd;
in
{
options.services.jitterentropy-rngd = {
enable = lib.mkEnableOption "jitterentropy-rngd service configuration";
package = lib.mkPackageOption pkgs "jitterentropy-rngd" { };
};
config = lib.mkIf cfg.enable {
systemd.packages = [ cfg.package ];
systemd.services."jitterentropy".wantedBy = [ "basic.target" ];
};
meta.maintainers = with lib.maintainers; [ thillux ];
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.munge;
in
{
###### interface
options = {
services.munge = {
enable = lib.mkEnableOption "munge service";
password = lib.mkOption {
default = "/etc/munge/munge.key";
type = lib.types.path;
description = ''
The path to a daemon's secret key.
'';
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
environment.systemPackages = [ pkgs.munge ];
users.users.munge = {
description = "Munge daemon user";
isSystemUser = true;
group = "munge";
};
users.groups.munge = { };
systemd.services.munged = {
documentation = [
"man:munged(8)"
"man:mungekey(8)"
];
wantedBy = [ "multi-user.target" ];
wants = [
"network-online.target"
"time-sync.target"
];
after = [
"network-online.target"
"time-sync.target"
];
path = [
pkgs.munge
pkgs.coreutils
];
serviceConfig = {
ExecStartPre = "+${pkgs.coreutils}/bin/chmod 0400 ${cfg.password}";
ExecStart = "${pkgs.munge}/bin/munged --foreground --key-file ${cfg.password}";
User = "munge";
Group = "munge";
StateDirectory = "munge";
StateDirectoryMode = "0711";
Restart = "on-failure";
RuntimeDirectory = "munge";
};
};
};
}

View File

@@ -0,0 +1,86 @@
{
config,
lib,
pkgs,
utils,
...
}:
let
cfg = config.services.nginx.sso;
format = pkgs.formats.yaml { };
configPath = "/var/lib/nginx-sso/config.yaml";
in
{
options.services.nginx.sso = {
enable = lib.mkEnableOption "nginx-sso service";
package = lib.mkPackageOption pkgs "nginx-sso" { };
configuration = lib.mkOption {
type = format.type;
default = { };
example = lib.literalExpression ''
{
listen = { addr = "127.0.0.1"; port = 8080; };
providers.token.tokens = {
myuser = {
_secret = "/path/to/secret/token.txt"; # File content should be the secret token
};
};
acl = {
rule_sets = [
{
rules = [ { field = "x-application"; equals = "MyApp"; } ];
allow = [ "myuser" ];
}
];
};
}
'';
description = ''
nginx-sso configuration
([documentation](https://github.com/Luzifer/nginx-sso/wiki/Main-Configuration))
as a Nix attribute set.
Options containing secret data should be set to an attribute set
with the singleton attribute `_secret` - a string value set to the path
to the file containing the secret value which should be used in the
configuration. This file must be readable by `nginx-sso`.
'';
};
};
config = lib.mkIf cfg.enable {
systemd.services.nginx-sso = {
description = "Nginx SSO Backend";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
StateDirectory = "nginx-sso";
WorkingDirectory = "/var/lib/nginx-sso";
ExecStartPre = pkgs.writeShellScript "merge-nginx-sso-config" ''
rm -f '${configPath}'
# Relies on YAML being a superset of JSON
${utils.genJqSecretsReplacementSnippet cfg.configuration configPath}
'';
ExecStart = ''
${lib.getExe cfg.package} \
--config ${configPath} \
--frontend-dir ${lib.getBin cfg.package}/share/frontend
'';
Restart = "always";
User = "nginx-sso";
Group = "nginx-sso";
};
};
users.users.nginx-sso = {
isSystemUser = true;
group = "nginx-sso";
};
users.groups.nginx-sso = { };
};
}

View File

@@ -0,0 +1,153 @@
{ config, lib, ... }:
let
cfg = config.services.oauth2-proxy.nginx;
in
{
options.services.oauth2-proxy.nginx = {
proxy = lib.mkOption {
type = lib.types.str;
default = config.services.oauth2-proxy.httpAddress;
defaultText = lib.literalExpression "config.services.oauth2-proxy.httpAddress";
description = ''
The address of the reverse proxy endpoint for oauth2-proxy
'';
};
domain = lib.mkOption {
type = lib.types.str;
description = ''
The domain under which the oauth2-proxy will be accesible and the path of cookies are set to.
This setting must be set to ensure back-redirects are working properly
if oauth2-proxy is configured with {option}`services.oauth2-proxy.cookie.domain`
or multiple {option}`services.oauth2-proxy.nginx.virtualHosts` that are not on the same domain.
'';
};
virtualHosts = lib.mkOption {
type =
let
vhostSubmodule = lib.types.submodule {
options = {
allowed_groups = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
description = "List of groups to allow access to this vhost, or null to allow all.";
default = null;
};
allowed_emails = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
description = "List of emails to allow access to this vhost, or null to allow all.";
default = null;
};
allowed_email_domains = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
description = "List of email domains to allow access to this vhost, or null to allow all.";
default = null;
};
};
};
oldType = lib.types.listOf lib.types.str;
convertFunc =
x:
lib.warn
"services.oauth2-proxy.nginx.virtualHosts should be an attrset, found ${lib.generators.toPretty { } x}"
lib.genAttrs
x
(_: { });
newType = lib.types.attrsOf vhostSubmodule;
in
lib.types.coercedTo oldType convertFunc newType;
default = { };
example = {
"protected.foo.com" = {
allowed_groups = [ "admins" ];
allowed_emails = [ "boss@foo.com" ];
};
};
description = ''
Nginx virtual hosts to put behind the oauth2 proxy.
You can exclude specific locations by setting `auth_request off;` in the locations extraConfig setting.
'';
};
};
config.services.oauth2-proxy =
lib.mkIf (cfg.virtualHosts != { } && (lib.hasPrefix "127.0.0.1:" cfg.proxy))
{
enable = true;
};
config.services.nginx = lib.mkIf (cfg.virtualHosts != { } && config.services.oauth2-proxy.enable) (
lib.mkMerge (
[
{
virtualHosts.${cfg.domain}.locations."/oauth2/" = {
proxyPass = cfg.proxy;
extraConfig = ''
auth_request off;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;
'';
};
}
]
++ lib.optional (cfg.virtualHosts != { }) {
recommendedProxySettings = true; # needed because duplicate headers
}
++ (lib.mapAttrsToList (vhost: conf: {
virtualHosts.${vhost} = {
locations = {
"/".extraConfig = ''
auth_request_set $user $upstream_http_x_auth_request_user;
auth_request_set $email $upstream_http_x_auth_request_email;
auth_request_set $auth_cookie $upstream_http_set_cookie;
# pass information via X-User and X-Email headers to backend, requires running with --set-xauthrequest flag
proxy_set_header X-User $user;
proxy_set_header X-Email $email;
# if you enabled --cookie-refresh, this is needed for it to work with auth_request
add_header Set-Cookie $auth_cookie;
'';
"= /oauth2/auth" =
let
maybeQueryArg =
name: value:
if value == null then
null
else
"${name}=${lib.concatStringsSep "," (builtins.map lib.escapeURL value)}";
allArgs = lib.mapAttrsToList maybeQueryArg conf;
cleanArgs = builtins.filter (x: x != null) allArgs;
cleanArgsStr = lib.concatStringsSep "&" cleanArgs;
in
{
# nginx doesn't support passing query string arguments to auth_request,
# so pass them here instead
proxyPass = "${cfg.proxy}/oauth2/auth?${cleanArgsStr}";
extraConfig = ''
auth_request off;
proxy_set_header X-Scheme $scheme;
# nginx auth_request includes headers but not body
proxy_set_header Content-Length "";
proxy_pass_request_body off;
'';
};
"@redirectToAuth2ProxyLogin" = {
return = "307 https://${cfg.domain}/oauth2/start?rd=$scheme://$host$request_uri";
extraConfig = ''
auth_request off;
'';
};
};
extraConfig = ''
auth_request /oauth2/auth;
error_page 401 = @redirectToAuth2ProxyLogin;
'';
};
}) cfg.virtualHosts)
)
);
}

View File

@@ -0,0 +1,639 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.oauth2-proxy;
# oauth2-proxy provides many options that are only relevant if you are using
# a certain provider. This set maps from provider name to a function that
# takes the configuration and returns a string that can be inserted into the
# command-line to launch oauth2-proxy.
providerSpecificOptions = {
azure = cfg: {
azure-tenant = cfg.azure.tenant;
resource = cfg.azure.resource;
};
github = cfg: {
github = {
inherit (cfg.github) org team;
};
};
google = cfg: {
google =
with cfg.google;
lib.optionalAttrs (groups != [ ]) {
admin-email = adminEmail;
service-account = serviceAccountJSON;
group = groups;
};
};
};
authenticatedEmailsFile = pkgs.writeText "authenticated-emails" cfg.email.addresses;
getProviderOptions = cfg: provider: providerSpecificOptions.${provider} or (_: { }) cfg;
allConfig =
with cfg;
{
inherit (cfg) provider scope upstream;
approval-prompt = approvalPrompt;
basic-auth-password = basicAuthPassword;
client-id = clientID;
client-secret = clientSecret;
custom-templates-dir = customTemplatesDir;
email-domain = email.domains;
http-address = httpAddress;
login-url = loginURL;
pass-access-token = passAccessToken;
pass-basic-auth = passBasicAuth;
pass-host-header = passHostHeader;
reverse-proxy = reverseProxy;
proxy-prefix = proxyPrefix;
profile-url = profileURL;
oidc-issuer-url = oidcIssuerUrl;
redeem-url = redeemURL;
redirect-url = redirectURL;
request-logging = requestLogging;
skip-auth-regex = skipAuthRegexes;
signature-key = signatureKey;
validate-url = validateURL;
htpasswd-file = htpasswd.file;
cookie = {
inherit (cookie)
domain
secure
expire
name
secret
refresh
;
httponly = cookie.httpOnly;
};
set-xauthrequest = setXauthrequest;
}
// lib.optionalAttrs (cfg.email.addresses != null) {
authenticated-emails-file = authenticatedEmailsFile;
}
// lib.optionalAttrs (cfg.passBasicAuth) {
basic-auth-password = cfg.basicAuthPassword;
}
// lib.optionalAttrs (cfg.htpasswd.file != null) {
display-htpasswd-form = cfg.htpasswd.displayForm;
}
// lib.optionalAttrs tls.enable {
tls-cert-file = tls.certificate;
tls-key-file = tls.key;
https-address = tls.httpsAddress;
}
// (getProviderOptions cfg cfg.provider)
// cfg.extraConfig;
mapConfig =
key: attr:
lib.optionalString (attr != null && attr != [ ]) (
if lib.isDerivation attr then
mapConfig key (toString attr)
else if (builtins.typeOf attr) == "set" then
lib.concatStringsSep " " (lib.mapAttrsToList (name: value: mapConfig (key + "-" + name) value) attr)
else if (builtins.typeOf attr) == "list" then
lib.concatMapStringsSep " " (mapConfig key) attr
else if (builtins.typeOf attr) == "bool" then
"--${key}=${lib.boolToString attr}"
else if (builtins.typeOf attr) == "string" then
"--${key}='${attr}'"
else
"--${key}=${toString attr}"
);
configString = lib.concatStringsSep " " (lib.mapAttrsToList mapConfig allConfig);
in
{
options.services.oauth2-proxy = {
enable = lib.mkEnableOption "oauth2-proxy";
package = lib.mkPackageOption pkgs "oauth2-proxy" { };
##############################################
# PROVIDER configuration
# Taken from: https://github.com/oauth2-proxy/oauth2-proxy/blob/master/providers/providers.go
provider = lib.mkOption {
type = lib.types.enum [
"adfs"
"azure"
"bitbucket"
"digitalocean"
"facebook"
"github"
"gitlab"
"google"
"keycloak"
"keycloak-oidc"
"linkedin"
"login.gov"
"nextcloud"
"oidc"
];
default = "google";
description = ''
OAuth provider.
'';
};
approvalPrompt = lib.mkOption {
type = lib.types.enum [
"force"
"auto"
];
default = "force";
description = ''
OAuth approval_prompt.
'';
};
clientID = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
The OAuth Client ID.
'';
example = "123456.apps.googleusercontent.com";
};
oidcIssuerUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
The OAuth issuer URL.
'';
example = "https://login.microsoftonline.com/{TENANT_ID}/v2.0";
};
clientSecret = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
The OAuth Client Secret.
'';
};
skipAuthRegexes = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Skip authentication for requests matching any of these regular
expressions.
'';
};
# XXX: Not clear whether these two options are mutually exclusive or not.
email = {
domains = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Authenticate emails with the specified domains. Use
`*` to authenticate any email.
'';
};
addresses = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
description = ''
Line-separated email addresses that are allowed to authenticate.
'';
};
};
loginURL = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Authentication endpoint.
You only need to set this if you are using a self-hosted provider (e.g.
Github Enterprise). If you're using a publicly hosted provider
(e.g github.com), then the default works.
'';
example = "https://provider.example.com/oauth/authorize";
};
redeemURL = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Token redemption endpoint.
You only need to set this if you are using a self-hosted provider (e.g.
Github Enterprise). If you're using a publicly hosted provider
(e.g github.com), then the default works.
'';
example = "https://provider.example.com/oauth/token";
};
validateURL = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Access token validation endpoint.
You only need to set this if you are using a self-hosted provider (e.g.
Github Enterprise). If you're using a publicly hosted provider
(e.g github.com), then the default works.
'';
example = "https://provider.example.com/user/emails";
};
redirectURL = lib.mkOption {
# XXX: jml suspects this is always necessary, but the command-line
# doesn't require it so making it optional.
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
The OAuth2 redirect URL.
'';
example = "https://internalapp.yourcompany.com/oauth2/callback";
};
azure = {
tenant = lib.mkOption {
type = lib.types.str;
default = "common";
description = ''
Go to a tenant-specific or common (tenant-independent) endpoint.
'';
};
resource = lib.mkOption {
type = lib.types.str;
description = ''
The resource that is protected.
'';
};
};
google = {
adminEmail = lib.mkOption {
type = lib.types.str;
description = ''
The Google Admin to impersonate for API calls.
Only users with access to the Admin APIs can access the Admin SDK
Directory API, thus the service account needs to impersonate one of
those users to access the Admin SDK Directory API.
See <https://developers.google.com/admin-sdk/directory/v1/guides/delegation#delegate_domain-wide_authority_to_your_service_account>.
'';
};
groups = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Restrict logins to members of these Google groups.
'';
};
serviceAccountJSON = lib.mkOption {
type = lib.types.path;
description = ''
The path to the service account JSON credentials.
'';
};
};
github = {
org = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Restrict logins to members of this organisation.
'';
};
team = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Restrict logins to members of this team.
'';
};
};
####################################################
# UPSTREAM Configuration
upstream = lib.mkOption {
type = with lib.types; coercedTo str (x: [ x ]) (listOf str);
default = [ ];
description = ''
The http url(s) of the upstream endpoint or `file://`
paths for static files. Routing is based on the path.
'';
};
passAccessToken = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Pass OAuth access_token to upstream via X-Forwarded-Access-Token header.
'';
};
passBasicAuth = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream.
'';
};
basicAuthPassword = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
The password to set when passing the HTTP Basic Auth header.
'';
};
passHostHeader = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Pass the request Host Header to upstream.
'';
};
signatureKey = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
GAP-Signature request signature key.
'';
example = "sha1:secret0";
};
cookie = {
domain = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Optional cookie domains to force cookies to (ie: `.yourcompany.com`).
The longest domain matching the request's host will be used (or the shortest
cookie domain if there is no match).
'';
example = ".yourcompany.com";
};
expire = lib.mkOption {
type = lib.types.str;
default = "168h0m0s";
description = ''
Expire timeframe for cookie.
'';
};
httpOnly = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Set HttpOnly cookie flag.
'';
};
name = lib.mkOption {
type = lib.types.str;
default = "_oauth2_proxy";
description = ''
The name of the cookie that the oauth_proxy creates.
'';
};
refresh = lib.mkOption {
# XXX: Unclear what the behavior is when this is not specified.
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Refresh the cookie after this duration; 0 to disable.
'';
example = "168h0m0s";
};
secret = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = ''
The seed string for secure cookies.
'';
};
secure = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Set secure (HTTPS) cookie flag.
'';
};
};
####################################################
# OAUTH2 PROXY configuration
httpAddress = lib.mkOption {
type = lib.types.str;
default = "http://127.0.0.1:4180";
description = ''
HTTPS listening address. This module does not expose the port by
default. If you want this URL to be accessible to other machines, please
add the port to `networking.firewall.allowedTCPPorts`.
'';
};
htpasswd = {
file = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Additionally authenticate against a htpasswd file. Entries must be
created with `htpasswd -s` for SHA encryption.
'';
};
displayForm = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Display username / password login form if an htpasswd file is provided.
'';
};
};
customTemplatesDir = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Path to custom HTML templates.
'';
};
reverseProxy = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
In case when running behind a reverse proxy, controls whether headers
like `X-Real-Ip` are accepted. Usage behind a reverse
proxy will require this flag to be set to avoid logging the reverse
proxy IP address.
'';
};
proxyPrefix = lib.mkOption {
type = lib.types.str;
default = "/oauth2";
description = ''
The url root path that this proxy should be nested under.
'';
};
tls = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to serve over TLS.
'';
};
certificate = lib.mkOption {
type = lib.types.path;
description = ''
Path to certificate file.
'';
};
key = lib.mkOption {
type = lib.types.path;
description = ''
Path to private key file.
'';
};
httpsAddress = lib.mkOption {
type = lib.types.str;
default = ":443";
description = ''
`addr:port` to listen on for HTTPS clients.
Remember to add `port` to
`allowedTCPPorts` if you want other machines to be
able to connect to it.
'';
};
};
requestLogging = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Log requests to stdout.
'';
};
####################################################
# UNKNOWN
# XXX: Is this mandatory? Is it part of another group? Is it part of the provider specification?
scope = lib.mkOption {
# XXX: jml suspects this is always necessary, but the command-line
# doesn't require it so making it optional.
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
OAuth scope specification.
'';
};
profileURL = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Profile access endpoint.
'';
};
setXauthrequest = lib.mkOption {
type = lib.types.nullOr lib.types.bool;
default = false;
description = ''
Set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode). Setting this to 'null' means using the upstream default (false).
'';
};
extraConfig = lib.mkOption {
default = { };
type = lib.types.attrsOf lib.types.anything;
description = ''
Extra config to pass to oauth2-proxy.
'';
};
keyFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
oauth2-proxy allows passing sensitive configuration via environment variables.
Make a file that contains lines like
OAUTH2_PROXY_CLIENT_SECRET=asdfasdfasdf.apps.googleuserscontent.com
and specify the path here.
'';
example = "/run/keys/oauth2-proxy";
};
};
imports = [
(lib.mkRenamedOptionModule [ "services" "oauth2_proxy" ] [ "services" "oauth2-proxy" ])
];
config = lib.mkIf cfg.enable {
services.oauth2-proxy = lib.mkIf (cfg.keyFile != null) {
clientID = lib.mkDefault null;
clientSecret = lib.mkDefault null;
cookie.secret = lib.mkDefault null;
};
users.users.oauth2-proxy = {
description = "OAuth2 Proxy";
isSystemUser = true;
group = "oauth2-proxy";
};
users.groups.oauth2-proxy = { };
systemd.services.oauth2-proxy =
let
needsKeycloak =
lib.elem cfg.provider [
"keycloak"
"keycloak-oidc"
]
&& config.services.keycloak.enable;
in
{
description = "OAuth2 Proxy";
path = [ cfg.package ];
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ] ++ lib.optionals needsKeycloak [ "keycloak.service" ];
after = [ "network-online.target" ] ++ lib.optionals needsKeycloak [ "keycloak.service" ];
restartTriggers = [ cfg.keyFile ];
serviceConfig = {
User = "oauth2-proxy";
Restart = "always";
ExecStart = "${lib.getExe cfg.package} ${configString}";
EnvironmentFile = lib.mkIf (cfg.keyFile != null) cfg.keyFile;
};
};
};
}

View File

@@ -0,0 +1,160 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.openbao;
settingsFormat = pkgs.formats.json { };
in
{
options = {
services.openbao = {
enable = lib.mkEnableOption "OpenBao daemon";
package = lib.mkPackageOption pkgs "openbao" {
example = "pkgs.openbao.override { withHsm = false; withUi = false; }";
};
settings = lib.mkOption {
description = ''
Settings of OpenBao.
See [documentation](https://openbao.org/docs/configuration) for more details.
'';
example = lib.literalExpression ''
{
ui = true;
listener.default = {
type = "tcp";
tls_acme_email = config.security.acme.defaults.email;
tls_acme_domains = [ "example.com" ];
tls_acme_disable_http_challenge = true;
};
cluster_addr = "http://127.0.0.1:8201";
api_addr = "https://example.com";
storage.raft.path = "/var/lib/openbao";
}
'';
type = lib.types.submodule {
freeformType = settingsFormat.type;
options = {
ui = lib.mkEnableOption "the OpenBao web UI";
listener = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (
{ config, ... }:
{
freeformType = settingsFormat.type;
options = {
type = lib.mkOption {
type = lib.types.enum [
"tcp"
"unix"
];
description = ''
The listener type to enable.
'';
};
address = lib.mkOption {
type = lib.types.str;
default = if config.type == "unix" then "/run/openbao/openbao.sock" else "127.0.0.1:8200";
defaultText = lib.literalExpression ''if config.services.openbao.settings.listener.<name>.type == "unix" then "/run/openbao/openbao.sock" else "127.0.0.1:8200"'';
description = ''
The TCP address or UNIX socket path to listen on.
'';
};
};
}
)
);
description = ''
Configure a listener for responding to requests.
'';
};
};
};
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Additional arguments given to OpenBao.
'';
};
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
systemd.services.openbao = {
description = "OpenBao - A tool for managing secrets";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
restartIfChanged = false; # do not restart on "nixos-rebuild switch". It would seal the storage and disrupt the clients.
serviceConfig = {
Type = "notify";
ExecStart = lib.escapeShellArgs (
[
(lib.getExe cfg.package)
"server"
"-config"
(settingsFormat.generate "openbao.hcl.json" cfg.settings)
]
++ cfg.extraArgs
);
ExecReload = "${lib.getExe' pkgs.coreutils "kill"} -SIGHUP $MAINPID";
StateDirectory = "openbao";
StateDirectoryMode = "0700";
RuntimeDirectory = "openbao";
RuntimeDirectoryMode = "0700";
CapabilityBoundingSet = "";
DynamicUser = true;
LimitCORE = 0;
LockPersonality = true;
MemorySwapMax = 0;
MemoryZSwapMax = 0;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
Restart = "on-failure";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"@resources"
"~@privileged"
];
UMask = "0077";
};
};
};
}

View File

@@ -0,0 +1,267 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.opensnitch;
format = pkgs.formats.json { };
predefinedRules = lib.flip lib.mapAttrs cfg.rules (
name: cfg: {
file = pkgs.writeText "rule" (builtins.toJSON cfg);
}
);
in
{
options = {
services.opensnitch = {
enable = lib.mkEnableOption "Opensnitch application firewall";
package = lib.mkPackageOption pkgs "opensnitch" { };
rules = lib.mkOption {
default = { };
example = lib.literalExpression ''
{
"tor" = {
"name" = "tor";
"enabled" = true;
"action" = "allow";
"duration" = "always";
"operator" = {
"type" ="simple";
"sensitive" = false;
"operand" = "process.path";
"data" = "''${lib.getBin pkgs.tor}/bin/tor";
};
};
};
'';
description = ''
Declarative configuration of firewall rules.
All rules will be stored in `/var/lib/opensnitch/rules` by default.
Rules path can be configured with `settings.Rules.Path`.
See [upstream documentation](https://github.com/evilsocket/opensnitch/wiki/Rules)
for available options.
'';
type = lib.types.submodule {
freeformType = format.type;
};
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = format.type;
options = {
Server = {
Address = lib.mkOption {
type = lib.types.str;
description = ''
Unix socket path (unix:///tmp/osui.sock, the "unix:///" part is
mandatory) or TCP socket (192.168.1.100:50051).
'';
};
LogFile = lib.mkOption {
type = lib.types.path;
description = ''
File to write logs to (use /dev/stdout to write logs to standard
output).
'';
};
};
DefaultAction = lib.mkOption {
type = lib.types.enum [
"allow"
"deny"
];
description = ''
Default action whether to block or allow application internet
access.
'';
};
InterceptUnknown = lib.mkOption {
type = lib.types.bool;
description = ''
Whether to intercept spare connections.
'';
};
ProcMonitorMethod = lib.mkOption {
type = lib.types.enum [
"ebpf"
"proc"
"ftrace"
"audit"
];
description = ''
Which process monitoring method to use.
'';
};
LogLevel = lib.mkOption {
type = lib.types.ints.between 0 4;
description = ''
Default log level from 0 to 4 (debug, info, important, warning,
error).
'';
};
Firewall = lib.mkOption {
type = lib.types.enum [
"iptables"
"nftables"
];
description = ''
Which firewall backend to use.
'';
};
Stats = {
MaxEvents = lib.mkOption {
type = lib.types.int;
description = ''
Max events to send to the GUI.
'';
};
MaxStats = lib.mkOption {
type = lib.types.int;
description = ''
Max stats per item to keep in backlog.
'';
};
};
Ebpf.ModulesPath = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default =
if cfg.settings.ProcMonitorMethod == "ebpf" then
"${config.boot.kernelPackages.opensnitch-ebpf}/etc/opensnitchd"
else
null;
defaultText = lib.literalExpression ''
if cfg.settings.ProcMonitorMethod == "ebpf" then
"\\$\\{config.boot.kernelPackages.opensnitch-ebpf\\}/etc/opensnitchd"
else null;
'';
description = ''
Configure eBPF modules path. Used when
`settings.ProcMonitorMethod` is set to `ebpf`.
'';
};
Audit.AudispSocketPath = lib.mkOption {
type = lib.types.path;
default = "/run/audit/audispd_events";
description = ''
Configure audit socket path. Used when
`settings.ProcMonitorMethod` is set to `audit`.
'';
};
Rules.Path = lib.mkOption {
type = lib.types.path;
default = "/var/lib/opensnitch/rules";
description = ''
Path to the directory where firewall rules can be found and will
get stored by the NixOS module.
'';
};
};
};
description = ''
opensnitchd configuration. Refer to [upstream documentation](https://github.com/evilsocket/opensnitch/wiki/Configurations)
for details on supported values.
'';
};
};
};
config = lib.mkIf cfg.enable {
# pkg.opensnitch is referred to elsewhere in the module so we don't need to worry about it being garbage collected
services.opensnitch.settings = lib.mapAttrs (_: v: lib.mkDefault v) (
builtins.fromJSON (
builtins.unsafeDiscardStringContext (
builtins.readFile "${cfg.package}/etc/opensnitchd/default-config.json"
)
)
);
security.auditd = lib.mkIf (cfg.settings.ProcMonitorMethod == "audit") {
enable = true;
plugins.af_unix.active = true;
};
systemd = {
packages = [ cfg.package ];
services.opensnitchd = {
wantedBy = [ "multi-user.target" ];
path = lib.optionals (cfg.settings.ProcMonitorMethod == "audit") [ pkgs.audit ];
serviceConfig = {
ExecStart =
let
preparedSettings = removeAttrs cfg.settings (
lib.optional (cfg.settings.ProcMonitorMethod != "ebpf") "Ebpf"
);
in
[
""
"${lib.getExe' cfg.package "opensnitchd"} --config-file ${format.generate "default-config.json" preparedSettings}"
];
};
preStart = lib.mkIf (cfg.rules != { }) (
let
rules = lib.flip lib.mapAttrsToList predefinedRules (
file: content: {
inherit (content) file;
local = "${cfg.settings.Rules.Path}/${file}.json";
}
);
in
''
# Remove all firewall rules from rules path (configured with
# cfg.settings.Rules.Path) that are symlinks to a store-path, but aren't
# declared in `cfg.rules` (i.e. all networks that were "removed" from
# `cfg.rules`).
find ${cfg.settings.Rules.Path} -type l -lname '${builtins.storeDir}/*' ${
lib.optionalString (rules != { }) ''
-not \( ${
lib.concatMapStringsSep " -o " ({ local, ... }: "-name '${baseNameOf local}*'") rules
} \) \
''
} -delete
${lib.concatMapStrings (
{ file, local }:
''
ln -sf '${file}' "${local}"
''
) rules}
''
);
};
tmpfiles.rules = [
"d ${cfg.settings.Rules.Path} 0750 root root - -"
"L+ /etc/opensnitchd/system-fw.json - - - - ${cfg.package}/etc/opensnitchd/system-fw.json"
];
};
};
meta.maintainers = with lib.maintainers; [
onny
grimmauld
];
}

View File

@@ -0,0 +1,133 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.paretosecurity;
in
{
options.services.paretosecurity = {
enable = lib.mkEnableOption "[ParetoSecurity](https://paretosecurity.com) [agent](https://github.com/ParetoSecurity/agent) and its root helper";
package = lib.mkPackageOption pkgs "paretosecurity" { };
trayIcon = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Set to false to disable the tray icon and run as a CLI tool only.";
};
users = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule {
options = {
inviteId = lib.mkOption {
type = lib.types.str;
description = ''
A unique ID that links the agent to Pareto Cloud.
Get it from the Join Team page on `https://cloud.paretosecurity.com/team/join/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
In Step 2, under Linux tab, enter your email then copy it from the generated command.
'';
example = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
};
};
}
);
default = { };
description = "Per-user Pareto Security configuration.";
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
systemd.packages = [ cfg.package ];
# In traditional Linux distributions, systemd would read the [Install] section from
# unit files and automatically create the appropriate symlinks to enable services.
# However, in NixOS, due to its immutable nature and the way the Nix store works,
# the [Install] sections are not processed during system activation. Instead, we
# must explicitly tell NixOS which units to enable by specifying their target
# dependencies here. This creates the necessary symlinks in the proper locations.
systemd.sockets.paretosecurity.wantedBy = [ "sockets.target" ];
# In NixOS, systemd services are configured with minimal PATH. However,
# paretosecurity helper looks for installed software to do its job, so
# it needs the full system PATH. For example, it runs `iptables` to see if
# firewall is configured. And it looks for various password managers to see
# if one is installed.
# The `paretosecurity-user` timer service that is configured lower has
# the same need.
systemd.services = {
paretosecurity.serviceConfig.Environment = [
"PATH=${config.system.path}/bin:${config.system.path}/sbin"
];
}
// (
# Each user can set their inviteID, which creates a systemd service
# that runs `paretosecurity link ...` to link their device to Pareto Cloud.
lib.mapAttrs' (
username: userConfig:
lib.nameValuePair "paretosecurity-link-${username}" {
description = "Link Pareto Desktop to Pareto Cloud for user ${username}";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = username;
StateDirectory = "paretosecurity/${username}";
ExecStart = pkgs.writeShellScript "paretosecurity-link-${username}" ''
set -euo pipefail
INVITE_ID="${userConfig.inviteId}"
STATE_FILE="/var/lib/paretosecurity/${username}/linked-$INVITE_ID"
CONFIG_FILE="$HOME/.config/pareto.toml"
# Check if already linked with this specific invite
if [ -f "$STATE_FILE" ]; then
echo "Device already linked with invite $INVITE_ID for user ${username}"
exit 0
fi
# Ensure config directory exists
mkdir -p "$(dirname "$CONFIG_FILE")"
# Perform linking
echo "Linking device to Pareto Cloud for user ${username}..."
${cfg.package}/bin/paretosecurity link \
"paretosecurity://linkDevice/?invite_id=$INVITE_ID"
# Verify linking succeeded
if [ -f "$CONFIG_FILE" ] && grep -q "TeamID" "$CONFIG_FILE"; then
echo "Successfully linked to Pareto Cloud for user ${username}"
touch "$STATE_FILE"
else
echo "Failed to link to Pareto Cloud for user ${username}"
exit 1
fi
'';
};
wantedBy = [ "multi-user.target" ];
}
) cfg.users
);
# Enable the tray icon and timer services if the trayIcon option is enabled
systemd.user = lib.mkIf cfg.trayIcon {
services = {
paretosecurity-trayicon.wantedBy = [ "graphical-session.target" ];
paretosecurity-user = {
wantedBy = [ "graphical-session.target" ];
serviceConfig.Environment = [
"PATH=${config.system.path}/bin:${config.system.path}/sbin"
];
};
};
timers.paretosecurity-user.wantedBy = [ "timers.target" ];
};
};
}

View File

@@ -0,0 +1,25 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.passSecretService;
in
{
options.services.passSecretService = {
enable = lib.mkEnableOption "pass secret service";
package = lib.mkPackageOption pkgs "pass-secret-service" {
example = "pass-secret-service.override { python3 = pkgs.python310 }";
};
};
config = lib.mkIf cfg.enable {
systemd.packages = [ cfg.package ];
services.dbus.packages = [ cfg.package ];
};
meta.maintainers = with lib.maintainers; [ aidalgol ];
}

View File

@@ -0,0 +1,157 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.physlock;
in
{
###### interface
options = {
services.physlock = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable the {command}`physlock` screen locking mechanism.
Enable this and then run {command}`systemctl start physlock`
to securely lock the screen.
This will switch to a new virtual terminal, turn off console
switching and disable SysRq mechanism (when
{option}`services.physlock.disableSysRq` is set)
until the root or user password is given.
'';
};
allowAnyUser = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to allow any user to lock the screen. This will install a
setuid wrapper to allow any user to start physlock as root, which
is a minor security risk. Call the physlock binary to use this instead
of using the systemd service.
'';
};
disableSysRq = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to disable SysRq when locked with physlock.
'';
};
lockMessage = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
Message to show on physlock login terminal.
'';
};
muteKernelMessages = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Disable kernel messages on console while physlock is running.
'';
};
lockOn = {
suspend = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to lock screen with physlock just before suspend.
'';
};
hibernate = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to lock screen with physlock just before hibernate.
'';
};
extraTargets = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "display-manager.service" ];
description = ''
Other targets to lock the screen just before.
Useful if you want to e.g. both autologin to X11 so that
your {file}`~/.xsession` gets executed and
still to have the screen locked so that the system can be
booted relatively unattended.
'';
};
};
};
};
###### implementation
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
# for physlock -l and physlock -L
environment.systemPackages = [ pkgs.physlock ];
systemd.services.physlock = {
enable = true;
documentation = [ "man:physlock(1)" ];
description = "Physlock";
wantedBy =
lib.optional cfg.lockOn.suspend "suspend.target"
++ lib.optional cfg.lockOn.hibernate "hibernate.target"
++ cfg.lockOn.extraTargets;
before =
lib.optional cfg.lockOn.suspend "systemd-suspend.service"
++ lib.optional cfg.lockOn.hibernate "systemd-hibernate.service"
++ lib.optional (
cfg.lockOn.hibernate || cfg.lockOn.suspend
) "systemd-suspend-then-hibernate.service"
++ cfg.lockOn.extraTargets;
serviceConfig = {
Type = "forking";
ExecStart = "${pkgs.physlock}/bin/physlock -d${lib.optionalString cfg.muteKernelMessages "m"}${lib.optionalString cfg.disableSysRq "s"}${
lib.optionalString (cfg.lockMessage != "") " -p \"${cfg.lockMessage}\""
}";
};
};
security.pam.services.physlock = { };
}
(lib.mkIf cfg.allowAnyUser {
security.wrappers.physlock = {
setuid = true;
owner = "root";
group = "root";
source = "${pkgs.physlock}/bin/physlock";
};
})
]
);
}

View File

@@ -0,0 +1,239 @@
{
lib,
pkgs,
config,
...
}:
let
inherit (lib)
concatMap
concatStringsSep
getExe
maintainers
mkEnableOption
mkIf
mkOption
mkPackageOption
optional
optionalAttrs
;
inherit (lib.types)
bool
path
str
submodule
;
cfg = config.services.pocket-id;
format = pkgs.formats.keyValue { };
settingsFile = format.generate "pocket-id-env-vars" cfg.settings;
in
{
meta.maintainers = with maintainers; [
gepbird
ymstnt
];
options.services.pocket-id = {
enable = mkEnableOption "Pocket ID server";
package = mkPackageOption pkgs "pocket-id" { };
environmentFile = mkOption {
type = path;
description = ''
Path to an environment file loaded for the Pocket ID service.
This can be used to securely store tokens and secrets outside of the world-readable Nix store.
Example contents of the file:
MAXMIND_LICENSE_KEY=your-license-key
'';
default = "/dev/null";
example = "/var/lib/secrets/pocket-id";
};
settings = mkOption {
type = submodule {
freeformType = format.type;
options = {
APP_URL = mkOption {
type = str;
description = ''
The URL where you will access the app.
'';
default = "http://localhost";
};
TRUST_PROXY = mkOption {
type = bool;
description = ''
Whether the app is behind a reverse proxy.
'';
default = false;
};
ANALYTICS_DISABLED = mkOption {
type = bool;
description = ''
Whether to disable analytics.
See [docs page](https://pocket-id.org/docs/configuration/analytics/).
'';
default = false;
};
};
};
default = { };
description = ''
Environment variables that will be passed to Pocket ID, see
[configuration options](https://pocket-id.org/docs/configuration/environment-variables)
for supported values.
'';
};
dataDir = mkOption {
type = path;
default = "/var/lib/pocket-id";
description = ''
The directory where Pocket ID will store its data, such as the database.
'';
};
user = mkOption {
type = str;
default = "pocket-id";
description = "User account under which Pocket ID runs.";
};
group = mkOption {
type = str;
default = "pocket-id";
description = "Group account under which Pocket ID runs.";
};
};
config = mkIf cfg.enable {
warnings =
optional (cfg.settings ? MAXMIND_LICENSE_KEY)
"config.services.pocket-id.settings.MAXMIND_LICENSE_KEY will be stored as plaintext in the Nix store. Use config.services.pocket-id.environmentFile instead."
++
concatMap
(
# Added 2025-05-27
setting:
optional (cfg.settings ? "${setting}") ''
config.services.pocket-id.settings.${setting} is deprecated.
See https://pocket-id.org/docs/setup/migrate-to-v1/ for migration instructions.
''
)
[
"PUBLIC_APP_URL"
"PUBLIC_UI_CONFIG_DISABLED"
"CADDY_DISABLED"
"CADDY_PORT"
"BACKEND_PORT"
"POSTGRES_CONNECTION_STRING"
"SQLITE_DB_PATH"
"INTERNAL_BACKEND_URL"
];
systemd.tmpfiles.rules = [
"d ${cfg.dataDir} 0755 ${cfg.user} ${cfg.group}"
];
systemd.services = {
pocket-id = {
description = "Pocket ID";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
restartTriggers = [
cfg.package
cfg.environmentFile
settingsFile
];
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
WorkingDirectory = cfg.dataDir;
ExecStart = getExe cfg.package;
Restart = "always";
EnvironmentFile = [
cfg.environmentFile
settingsFile
];
# Hardening
AmbientCapabilities = "";
CapabilityBoundingSet = "";
DeviceAllow = "";
DevicePolicy = "closed";
#IPAddressDeny = "any"; # provides the service through network
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateNetwork = false; # provides the service through network
PrivateTmp = true;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
ReadWritePaths = [ cfg.dataDir ];
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = concatStringsSep " " [
"~"
"@clock"
"@cpu-emulation"
"@debug"
"@module"
"@mount"
"@obsolete"
"@privileged"
"@raw-io"
"@reboot"
"@resources"
"@swap"
];
UMask = "0077";
};
};
};
users.users = optionalAttrs (cfg.user == "pocket-id") {
pocket-id = {
isSystemUser = true;
group = cfg.group;
description = "Pocket ID backend user";
home = cfg.dataDir;
};
};
users.groups = optionalAttrs (cfg.group == "pocket-id") {
pocket-id = { };
};
};
}

View File

@@ -0,0 +1,83 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.shibboleth-sp;
in
{
options = {
services.shibboleth-sp = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to enable the shibboleth service";
};
configFile = lib.mkOption {
type = lib.types.path;
example = lib.literalExpression ''"''${pkgs.shibboleth-sp}/etc/shibboleth/shibboleth2.xml"'';
description = "Path to shibboleth config file";
};
fastcgi.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to include the shibauthorizer and shibresponder FastCGI processes";
};
fastcgi.shibAuthorizerPort = lib.mkOption {
type = lib.types.port;
default = 9100;
description = "Port for shibauthorizer FastCGI process to bind to";
};
fastcgi.shibResponderPort = lib.mkOption {
type = lib.types.port;
default = 9101;
description = "Port for shibauthorizer FastCGI process to bind to";
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.shibboleth-sp = {
description = "Provides SSO and federation for web applications";
after = lib.optionals cfg.fastcgi.enable [
"shibresponder.service"
"shibauthorizer.service"
];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${pkgs.shibboleth-sp}/bin/shibd -F -d ${pkgs.shibboleth-sp} -c ${cfg.configFile}";
};
};
systemd.services.shibresponder = lib.mkIf cfg.fastcgi.enable {
description = "Provides SSO through Shibboleth via FastCGI";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ "${pkgs.spawn_fcgi}" ];
environment.SHIBSP_CONFIG = "${cfg.configFile}";
serviceConfig = {
ExecStart = "${pkgs.spawn_fcgi}/bin/spawn-fcgi -n -p ${toString cfg.fastcgi.shibResponderPort} ${pkgs.shibboleth-sp}/lib/shibboleth/shibresponder";
};
};
systemd.services.shibauthorizer = lib.mkIf cfg.fastcgi.enable {
description = "Provides SSO through Shibboleth via FastCGI";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ "${pkgs.spawn_fcgi}" ];
environment.SHIBSP_CONFIG = "${cfg.configFile}";
serviceConfig = {
ExecStart = "${pkgs.spawn_fcgi}/bin/spawn-fcgi -n -p ${toString cfg.fastcgi.shibAuthorizerPort} ${pkgs.shibboleth-sp}/lib/shibboleth/shibauthorizer";
};
};
};
meta.maintainers = [ ];
}

View File

@@ -0,0 +1,155 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.sks;
sksPkg = cfg.package;
dbConfig = pkgs.writeText "DB_CONFIG" ''
${cfg.extraDbConfig}
'';
in
{
meta.maintainers = with lib.maintainers; [
calbrecht
jcumming
];
options = {
services.sks = {
enable = lib.mkEnableOption ''
SKS (synchronizing key server for OpenPGP) and start the database
server. You need to create "''${dataDir}/dump/*.gpg" for the initial
import'';
package = lib.mkPackageOption pkgs "sks" { };
dataDir = lib.mkOption {
type = lib.types.path;
default = "/var/db/sks";
example = "/var/lib/sks";
# TODO: The default might change to "/var/lib/sks" as this is more
# common. There's also https://github.com/NixOS/nixpkgs/issues/26256
# and "/var/db" is not FHS compliant (seems to come from BSD).
description = ''
Data directory (-basedir) for SKS, where the database and all
configuration files are located (e.g. KDB, PTree, membership and
sksconf).
'';
};
extraDbConfig = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
Set contents of the files "KDB/DB_CONFIG" and "PTree/DB_CONFIG" within
the ''${dataDir} directory. This is used to configure options for the
database for the sks key server.
Documentation of available options are available in the file named
"sampleConfig/DB_CONFIG" in the following repository:
https://bitbucket.org/skskeyserver/sks-keyserver/src
'';
};
hkpAddress = lib.mkOption {
default = [
"127.0.0.1"
"::1"
];
type = lib.types.listOf lib.types.str;
description = ''
Domain names, IPv4 and/or IPv6 addresses to listen on for HKP
requests.
'';
};
hkpPort = lib.mkOption {
default = 11371;
type = lib.types.ints.u16;
description = "HKP port to listen on.";
};
webroot = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = "${sksPkg.webSamples}/OpenPKG";
defaultText = lib.literalExpression ''"''${package.webSamples}/OpenPKG"'';
description = ''
Source directory (will be symlinked, if not null) for the files the
built-in webserver should serve. SKS (''${pkgs.sks.webSamples})
provides the following examples: "HTML5", "OpenPKG", and "XHTML+ES".
The index file can be named index.html, index.htm, index.xhtm, or
index.xhtml. Files with the extensions .css, .es, .js, .jpg, .jpeg,
.png, or .gif are supported. Subdirectories and filenames with
anything other than alphanumeric characters and the '.' character
will be ignored.
'';
};
};
};
config = lib.mkIf cfg.enable {
users = {
users.sks = {
isSystemUser = true;
description = "SKS user";
home = cfg.dataDir;
createHome = true;
group = "sks";
useDefaultShell = true;
packages = [
sksPkg
pkgs.db
];
};
groups.sks = { };
};
systemd.services =
let
hkpAddress = "'" + (builtins.concatStringsSep " " cfg.hkpAddress) + "'";
hkpPort = builtins.toString cfg.hkpPort;
in
{
sks-db = {
description = "SKS database server";
documentation = [ "man:sks(8)" ];
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
preStart = ''
${lib.optionalString (cfg.webroot != null) "ln -sfT \"${cfg.webroot}\" web"}
mkdir -p dump
${sksPkg}/bin/sks build dump/*.gpg -n 10 -cache 100 || true #*/
${sksPkg}/bin/sks cleandb || true
${sksPkg}/bin/sks pbuild -cache 20 -ptree_cache 70 || true
# Check that both database configs are symlinks before overwriting them
# TODO: The initial build will be without DB_CONFIG, but this will
# hopefully not cause any significant problems. It might be better to
# create both directories manually but we have to check that this does
# not affect the initial build of the DB.
for CONFIG_FILE in KDB/DB_CONFIG PTree/DB_CONFIG; do
if [ -e $CONFIG_FILE ] && [ ! -L $CONFIG_FILE ]; then
echo "$CONFIG_FILE exists but is not a symlink." >&2
echo "Please remove $PWD/$CONFIG_FILE manually to continue." >&2
exit 1
fi
ln -sf ${dbConfig} $CONFIG_FILE
done
'';
serviceConfig = {
WorkingDirectory = "~";
User = "sks";
Group = "sks";
Restart = "always";
ExecStart = "${sksPkg}/bin/sks db -hkp_address ${hkpAddress} -hkp_port ${hkpPort}";
};
};
};
};
}

View File

@@ -0,0 +1,196 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.sshguard;
configFile =
let
args = lib.concatStringsSep " " (
[
"-afb"
"-p info"
"-o cat"
"-n1"
]
++ (map (name: "-t ${lib.escapeShellArg name}") cfg.services)
);
backend = if config.networking.nftables.enable then "sshg-fw-nft-sets" else "sshg-fw-ipset";
in
pkgs.writeText "sshguard.conf" ''
BACKEND="${pkgs.sshguard}/libexec/${backend}"
LOGREADER="LANG=C ${config.systemd.package}/bin/journalctl ${args}"
'';
in
{
###### interface
options = {
services.sshguard = {
enable = lib.mkOption {
default = false;
type = lib.types.bool;
description = "Whether to enable the sshguard service.";
};
attack_threshold = lib.mkOption {
default = 30;
type = lib.types.int;
description = ''
Block attackers when their cumulative attack score exceeds threshold. Most attacks have a score of 10.
'';
};
blacklist_threshold = lib.mkOption {
default = null;
example = 120;
type = lib.types.nullOr lib.types.int;
description = ''
Blacklist an attacker when its score exceeds threshold. Blacklisted addresses are loaded from and added to blacklist-file.
'';
};
blacklist_file = lib.mkOption {
default = "/var/lib/sshguard/blacklist.db";
type = lib.types.path;
description = ''
Blacklist an attacker when its score exceeds threshold. Blacklisted addresses are loaded from and added to blacklist-file.
'';
};
blocktime = lib.mkOption {
default = 120;
type = lib.types.int;
description = ''
Block attackers for initially blocktime seconds after exceeding threshold. Subsequent blocks increase by a factor of 1.5.
sshguard unblocks attacks at random intervals, so actual block times will be longer.
'';
};
detection_time = lib.mkOption {
default = 1800;
type = lib.types.int;
description = ''
Remember potential attackers for up to detection_time seconds before resetting their score.
'';
};
whitelist = lib.mkOption {
default = [ ];
example = [
"198.51.100.56"
"198.51.100.2"
];
type = lib.types.listOf lib.types.str;
description = ''
Whitelist a list of addresses, hostnames, or address blocks.
'';
};
services = lib.mkOption {
default = [ "sshd" ];
example = [
"sshd"
"exim"
];
type = lib.types.listOf lib.types.str;
description = ''
Systemd services sshguard should receive logs of.
'';
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
environment.etc."sshguard.conf".source = configFile;
systemd.services.sshguard = {
description = "SSHGuard brute-force attacks protection system";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
partOf = lib.optional config.networking.firewall.enable "firewall.service";
restartTriggers = [ configFile ];
path =
with pkgs;
if config.networking.nftables.enable then
[
nftables
iproute2
systemd
]
else
[
iptables
ipset
iproute2
systemd
];
# The sshguard ipsets must exist before we invoke
# iptables. sshguard creates the ipsets after startup if
# necessary, but if we let sshguard do it, we can't reliably add
# the iptables rules because postStart races with the creation
# of the ipsets. So instead, we create both the ipsets and
# firewall rules before sshguard starts.
preStart =
lib.optionalString config.networking.firewall.enable ''
${pkgs.ipset}/bin/ipset -quiet create -exist sshguard4 hash:net family inet
${pkgs.iptables}/bin/iptables -I INPUT -m set --match-set sshguard4 src -j DROP
''
+ lib.optionalString (config.networking.firewall.enable && config.networking.enableIPv6) ''
${pkgs.ipset}/bin/ipset -quiet create -exist sshguard6 hash:net family inet6
${pkgs.iptables}/bin/ip6tables -I INPUT -m set --match-set sshguard6 src -j DROP
'';
postStop =
lib.optionalString config.networking.firewall.enable ''
${pkgs.iptables}/bin/iptables -D INPUT -m set --match-set sshguard4 src -j DROP
${pkgs.ipset}/bin/ipset -quiet destroy sshguard4
''
+ lib.optionalString (config.networking.firewall.enable && config.networking.enableIPv6) ''
${pkgs.iptables}/bin/ip6tables -D INPUT -m set --match-set sshguard6 src -j DROP
${pkgs.ipset}/bin/ipset -quiet destroy sshguard6
'';
unitConfig.Documentation = "man:sshguard(8)";
serviceConfig = {
Type = "simple";
ExecStart =
let
args = lib.concatStringsSep " " (
[
"-a ${toString cfg.attack_threshold}"
"-p ${toString cfg.blocktime}"
"-s ${toString cfg.detection_time}"
(lib.optionalString (
cfg.blacklist_threshold != null
) "-b ${toString cfg.blacklist_threshold}:${cfg.blacklist_file}")
]
++ (map (name: "-w ${lib.escapeShellArg name}") cfg.whitelist)
);
in
"${pkgs.sshguard}/bin/sshguard ${args}";
Restart = "always";
ProtectSystem = "strict";
ProtectHome = "tmpfs";
RuntimeDirectory = "sshguard";
StateDirectory = "sshguard";
CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_RAW";
};
};
};
}

View File

@@ -0,0 +1,35 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.sslmate-agent;
in
{
meta.maintainers = [ ];
options = {
services.sslmate-agent = {
enable = lib.mkEnableOption "sslmate-agent, a daemon for managing SSL/TLS certificates on a server";
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = with pkgs; [ sslmate-agent ];
systemd = {
packages = [ pkgs.sslmate-agent ];
services.sslmate-agent = {
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ConfigurationDirectory = "sslmate-agent";
LogsDirectory = "sslmate-agent";
StateDirectory = "sslmate-agent";
};
};
};
};
}

View File

@@ -0,0 +1,136 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.step-ca;
settingsFormat = (pkgs.formats.json { });
in
{
meta.maintainers = [ ];
options = {
services.step-ca = {
enable = lib.mkEnableOption "the smallstep certificate authority server";
openFirewall = lib.mkEnableOption "opening the certificate authority server port";
package = lib.mkPackageOption pkgs "step-ca" { };
address = lib.mkOption {
type = lib.types.str;
example = "127.0.0.1";
description = ''
The address (without port) the certificate authority should listen at.
This combined with {option}`services.step-ca.port` overrides {option}`services.step-ca.settings.address`.
'';
};
port = lib.mkOption {
type = lib.types.port;
example = 8443;
description = ''
The port the certificate authority should listen on.
This combined with {option}`services.step-ca.address` overrides {option}`services.step-ca.settings.address`.
'';
};
settings = lib.mkOption {
type = with lib.types; attrsOf anything;
description = ''
Settings that go into {file}`ca.json`. See
[the step-ca manual](https://smallstep.com/docs/step-ca/configuration)
for more information. The easiest way to
configure this module would be to run `step ca init`
to generate {file}`ca.json` and then import it using
`builtins.fromJSON`.
[This article](https://smallstep.com/docs/step-cli/basic-crypto-operations#run-an-offline-x509-certificate-authority)
may also be useful if you want to customize certain aspects of
certificate generation for your CA.
You need to change the database storage path to {file}`/var/lib/step-ca/db`.
::: {.warning}
The {option}`services.step-ca.settings.address` option
will be ignored and overwritten by
{option}`services.step-ca.address` and
{option}`services.step-ca.port`.
:::
'';
};
intermediatePasswordFile = lib.mkOption {
type = lib.types.pathWith {
inStore = false;
absolute = true;
};
example = "/run/keys/smallstep-password";
description = ''
Path to the file containing the password for the intermediate
certificate private key.
::: {.warning}
Make sure to use a quoted absolute path instead of a path literal
to prevent it from being copied to the globally readable Nix
store.
:::
'';
};
};
};
config = lib.mkIf config.services.step-ca.enable (
let
configFile = settingsFormat.generate "ca.json" (
cfg.settings
// {
address = cfg.address + ":" + toString cfg.port;
}
);
in
{
systemd.packages = [ cfg.package ];
# configuration file indirection is needed to support reloading
environment.etc."smallstep/ca.json".source = configFile;
systemd.services."step-ca" = {
wantedBy = [ "multi-user.target" ];
restartTriggers = [ configFile ];
unitConfig = {
ConditionFileNotEmpty = ""; # override upstream
};
serviceConfig = {
User = "step-ca";
Group = "step-ca";
UMask = "0077";
Environment = "HOME=%S/step-ca";
WorkingDirectory = ""; # override upstream
ReadWritePaths = ""; # override upstream
# LocalCredential handles file permission problems arising from the use of DynamicUser.
LoadCredential = "intermediate_password:${cfg.intermediatePasswordFile}";
ExecStart = [
"" # override upstream
"${cfg.package}/bin/step-ca /etc/smallstep/ca.json --password-file \${CREDENTIALS_DIRECTORY}/intermediate_password"
];
# ProtectProc = "invisible"; # not supported by upstream yet
# ProcSubset = "pid"; # not supported by upstream yet
# PrivateUsers = true; # doesn't work with privileged ports therefore not supported by upstream
DynamicUser = true;
StateDirectory = "step-ca";
};
};
users.users.step-ca = {
home = "/var/lib/step-ca";
group = "step-ca";
isSystemUser = true;
};
users.groups.step-ca = { };
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ];
};
}
);
}

View File

@@ -0,0 +1,105 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.tang;
in
{
options.services.tang = {
enable = lib.mkEnableOption "tang";
package = lib.mkPackageOption pkgs "tang" { };
listenStream = lib.mkOption {
type = with lib.types; listOf str;
default = [ "7654" ];
example = [
"198.168.100.1:7654"
"[2001:db8::1]:7654"
"7654"
];
description = ''
Addresses and/or ports on which tang should listen.
For detailed syntax see ListenStream in {manpage}`systemd.socket(5)`.
'';
};
ipAddressAllow = lib.mkOption {
example = [ "192.168.1.0/24" ];
type = lib.types.listOf lib.types.str;
description = ''
Whitelist a list of address prefixes.
Preferably, internal addresses should be used.
'';
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
systemd.services."tangd@" = {
description = "Tang server";
path = [ cfg.package ];
serviceConfig = {
StandardInput = "socket";
StandardOutput = "socket";
StandardError = "journal";
DynamicUser = true;
StateDirectory = "tang";
RuntimeDirectory = "tang";
StateDirectoryMode = "700";
UMask = "0077";
CapabilityBoundingSet = [ "" ];
ExecStart = "${cfg.package}/libexec/tangd %S/tang";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
DeviceAllow = [ "/dev/stdin" ];
RestrictAddressFamilies = [ "AF_UNIX" ];
DevicePolicy = "strict";
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
"~@resources"
];
IPAddressDeny = "any";
IPAddressAllow = cfg.ipAddressAllow;
};
};
systemd.sockets.tangd = {
description = "Tang server";
wantedBy = [ "sockets.target" ];
socketConfig = {
ListenStream = cfg.listenStream;
Accept = "yes";
IPAddressDeny = "any";
IPAddressAllow = cfg.ipAddressAllow;
};
};
};
meta.maintainers = with lib.maintainers; [
jfroche
julienmalka
];
}

View File

@@ -0,0 +1,65 @@
{
pkgs,
lib,
config,
...
}:
let
cfg = config.services.timekpr;
targetBaseDir = "/var/lib/timekpr";
daemonUser = "root";
daemonGroup = "root";
in
{
options = {
services.timekpr = {
package = lib.mkPackageOption pkgs "timekpr" { };
enable = lib.mkEnableOption "Timekpr-nExT, a screen time managing application that helps optimizing time spent at computer for your subordinates, children or even for yourself";
adminUsers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"alice"
"bob"
];
description = ''
All listed users will become part of the `timekpr` group so they can manage timekpr settings without requiring sudo.
'';
};
};
};
config = lib.mkIf cfg.enable {
users.groups.timekpr = {
gid = 2000;
members = cfg.adminUsers;
};
environment.systemPackages = [
# Add timekpr to system packages so that polkit can find it
cfg.package
];
services.dbus.enable = true;
services.dbus.packages = [
cfg.package
];
environment.etc."timekpr" = {
source = "${cfg.package}/etc/timekpr";
};
systemd.packages = [
cfg.package
];
systemd.services.timekpr = {
enable = true;
wantedBy = [ "multi-user.target" ];
};
security.polkit.enable = true;
systemd.tmpfiles.rules = [
"d ${targetBaseDir} 0755 ${daemonUser} ${daemonGroup} -"
"d ${targetBaseDir}/config 0755 ${daemonUser} ${daemonGroup} -"
"d ${targetBaseDir}/work 0755 ${daemonUser} ${daemonGroup} -"
];
};
meta.maintainers = [ lib.maintainers.atry ];
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,84 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.tor;
torify = pkgs.writeTextFile {
name = "tsocks";
text = ''
#!${pkgs.runtimeShell}
TSOCKS_CONF_FILE=${pkgs.writeText "tsocks.conf" cfg.tsocks.config} LD_PRELOAD="${pkgs.tsocks}/lib/libtsocks.so $LD_PRELOAD" "$@"
'';
executable = true;
destination = "/bin/tsocks";
};
in
{
###### interface
options = {
services.tor.tsocks = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to build tsocks wrapper script to relay application traffic via Tor.
::: {.important}
You shouldn't use this unless you know what you're
doing because your installation of Tor already comes with
its own superior (doesn't leak DNS queries)
`torsocks` wrapper which does pretty much
exactly the same thing as this.
:::
'';
};
server = lib.mkOption {
type = lib.types.str;
default = "localhost:9050";
example = "192.168.0.20";
description = ''
IP address of TOR client to use.
'';
};
config = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
Extra configuration. Contents will be added verbatim to TSocks
configuration file.
'';
};
};
};
###### implementation
config = lib.mkIf cfg.tsocks.enable {
environment.systemPackages = [ torify ]; # expose it to the users
services.tor.tsocks.config = ''
server = ${toString (lib.head (lib.splitString ":" cfg.tsocks.server))}
server_port = ${toString (lib.tail (lib.splitString ":" cfg.tsocks.server))}
local = 127.0.0.0/255.128.0.0
local = 127.128.0.0/255.192.0.0
'';
};
}

View File

@@ -0,0 +1,124 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.tor.torsocks;
optionalNullStr = b: v: lib.optionalString (b != null) v;
configFile = server: ''
TorAddress ${toString (lib.head (lib.splitString ":" server))}
TorPort ${toString (lib.tail (lib.splitString ":" server))}
OnionAddrRange ${cfg.onionAddrRange}
${optionalNullStr cfg.socks5Username "SOCKS5Username ${cfg.socks5Username}"}
${optionalNullStr cfg.socks5Password "SOCKS5Password ${cfg.socks5Password}"}
AllowInbound ${if cfg.allowInbound then "1" else "0"}
'';
wrapTorsocks =
name: server:
pkgs.writeTextFile {
name = name;
text = ''
#!${pkgs.runtimeShell}
TORSOCKS_CONF_FILE=${pkgs.writeText "torsocks.conf" (configFile server)} ${pkgs.torsocks}/bin/torsocks "$@"
'';
executable = true;
destination = "/bin/${name}";
};
in
{
options = {
services.tor.torsocks = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to build `/etc/tor/torsocks.conf`
containing the specified global torsocks configuration.
'';
};
server = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1:9050";
example = "192.168.0.20:1234";
description = ''
IP/Port of the Tor SOCKS server. Currently, hostnames are
NOT supported by torsocks.
'';
};
fasterServer = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1:9063";
example = "192.168.0.20:1234";
description = ''
IP/Port of the Tor SOCKS server for torsocks-faster wrapper suitable for HTTP.
Currently, hostnames are NOT supported by torsocks.
'';
};
onionAddrRange = lib.mkOption {
type = lib.types.str;
default = "127.42.42.0/24";
description = ''
Tor hidden sites do not have real IP addresses. This
specifies what range of IP addresses will be handed to the
application as "cookies" for .onion names. Of course, you
should pick a block of addresses which you aren't going to
ever need to actually connect to. This is similar to the
MapAddress feature of the main tor daemon.
'';
};
socks5Username = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "bob";
description = ''
SOCKS5 username. The `TORSOCKS_USERNAME`
environment variable overrides this option if it is set.
'';
};
socks5Password = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "sekret";
description = ''
SOCKS5 password. The `TORSOCKS_PASSWORD`
environment variable overrides this option if it is set.
'';
};
allowInbound = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Set Torsocks to accept inbound connections. If set to
`true`, listen() and accept() will be
allowed to be used with non localhost address.
'';
};
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [
pkgs.torsocks
(wrapTorsocks "torsocks-faster" cfg.fasterServer)
];
environment.etc."tor/torsocks.conf" = {
source = pkgs.writeText "torsocks.conf" (configFile cfg.server);
};
};
}

View File

@@ -0,0 +1,288 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.usbguard;
# valid policy options
policy = (
lib.types.enum [
"allow"
"block"
"reject"
"keep"
"apply-policy"
]
);
# decide what file to use for rules
ruleFile = if cfg.rules != null then pkgs.writeText "usbguard-rules" cfg.rules else cfg.ruleFile;
daemonConf = ''
# generated by nixos/modules/services/security/usbguard.nix
RuleFile=${ruleFile}
ImplicitPolicyTarget=${cfg.implicitPolicyTarget}
PresentDevicePolicy=${cfg.presentDevicePolicy}
PresentControllerPolicy=${cfg.presentControllerPolicy}
InsertedDevicePolicy=${cfg.insertedDevicePolicy}
RestoreControllerDeviceState=${lib.boolToString cfg.restoreControllerDeviceState}
# this does not seem useful for endusers to change
DeviceManagerBackend=uevent
IPCAllowedUsers=${lib.concatStringsSep " " cfg.IPCAllowedUsers}
IPCAllowedGroups=${lib.concatStringsSep " " cfg.IPCAllowedGroups}
IPCAccessControlFiles=/var/lib/usbguard/IPCAccessControl.d/
DeviceRulesWithPort=${lib.boolToString cfg.deviceRulesWithPort}
# HACK: that way audit logs still land in the journal
AuditFilePath=/dev/null
'';
daemonConfFile = pkgs.writeText "usbguard-daemon-conf" daemonConf;
in
{
###### interface
options = {
services.usbguard = {
enable = lib.mkEnableOption "USBGuard daemon";
package = lib.mkPackageOption pkgs "usbguard" { };
ruleFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = "/var/lib/usbguard/rules.conf";
example = "/run/secrets/usbguard-rules";
description = ''
This tells the USBGuard daemon which file to load as policy rule set.
The file can be changed manually or via the IPC interface assuming it has the right file permissions.
For more details see {manpage}`usbguard-rules.conf(5)`.
'';
};
rules = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
example = ''
allow with-interface equals { 08:*:* }
'';
description = ''
The USBGuard daemon will load this as the policy rule set.
As these rules are NixOS managed they are immutable and can't
be changed by the IPC interface.
If you do not set this option, the USBGuard daemon will load
it's policy rule set from the option configured in `services.usbguard.ruleFile`.
Running `usbguard generate-policy` as root will
generate a config for your currently plugged in devices.
For more details see {manpage}`usbguard-rules.conf(5)`.
'';
};
implicitPolicyTarget = lib.mkOption {
type = lib.types.enum [
"allow"
"block"
"reject"
];
default = "block";
description = ''
How to treat USB devices that don't match any rule in the policy.
Target should be one of allow, block or reject (logically remove the
device node from the system).
'';
};
presentDevicePolicy = lib.mkOption {
type = policy;
default = "apply-policy";
description = ''
How to treat USB devices that are already connected when the daemon
starts. Policy should be one of allow, block, reject, keep (keep
whatever state the device is currently in) or apply-policy (evaluate
the rule set for every present device).
'';
};
presentControllerPolicy = lib.mkOption {
type = policy;
default = "keep";
description = ''
How to treat USB controller devices that are already connected when
the daemon starts. One of allow, block, reject, keep or apply-policy.
'';
};
insertedDevicePolicy = lib.mkOption {
type = lib.types.enum [
"block"
"reject"
"apply-policy"
];
default = "apply-policy";
description = ''
How to treat USB devices that are already connected after the daemon
starts. One of block, reject, apply-policy.
'';
};
restoreControllerDeviceState = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
The USBGuard daemon modifies some attributes of controller
devices like the default authorization state of new child device
instances. Using this setting, you can control whether the daemon
will try to restore the attribute values to the state before
modification on shutdown.
'';
};
IPCAllowedUsers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "root" ];
example = [
"root"
"yourusername"
];
description = ''
A list of usernames that the daemon will accept IPC connections from.
'';
};
IPCAllowedGroups = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "wheel" ];
description = ''
A list of groupnames that the daemon will accept IPC connections
from.
'';
};
deviceRulesWithPort = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Generate device specific rules including the "via-port" attribute.
'';
};
dbus.enable = lib.mkEnableOption "USBGuard dbus daemon";
};
};
###### implementation
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
systemd.services = {
usbguard = {
description = "USBGuard daemon";
wantedBy = [ "basic.target" ];
wants = [ "systemd-udevd.service" ];
# make sure an empty rule file exists
preStart = ''[ -f "${ruleFile}" ] || touch ${ruleFile}'';
serviceConfig = {
Type = "simple";
ExecStart = "${cfg.package}/bin/usbguard-daemon -P -k -c ${daemonConfFile}";
Restart = "on-failure";
StateDirectory = [
"usbguard"
"usbguard/IPCAccessControl.d"
];
AmbientCapabilities = "";
CapabilityBoundingSet = "CAP_CHOWN CAP_FOWNER";
DeviceAllow = "/dev/null rw";
DevicePolicy = "strict";
IPAddressDeny = "any";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectKernelModules = true;
ProtectSystem = true;
ReadOnlyPaths = "-/";
ReadWritePaths = "-/dev/shm -/tmp";
RestrictAddressFamilies = [
"AF_UNIX"
"AF_NETLINK"
];
RestrictNamespaces = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
SystemCallFilter = "@system-service";
UMask = "0077";
};
};
usbguard-dbus = lib.mkIf cfg.dbus.enable {
description = "USBGuard D-Bus Service";
wantedBy = [ "multi-user.target" ];
requires = [ "usbguard.service" ];
serviceConfig = {
Type = "dbus";
BusName = "org.usbguard1";
ExecStart = "${cfg.package}/bin/usbguard-dbus --system";
Restart = "on-failure";
};
aliases = [ "dbus-org.usbguard.service" ];
};
};
security.polkit.extraConfig =
let
groupCheck =
(lib.concatStrings (map (g: "subject.isInGroup(\"${g}\") || ") cfg.IPCAllowedGroups)) + "false";
in
lib.optionalString cfg.dbus.enable ''
polkit.addRule(function(action, subject) {
if ((action.id == "org.usbguard.Policy1.listRules" ||
action.id == "org.usbguard.Policy1.appendRule" ||
action.id == "org.usbguard.Policy1.removeRule" ||
action.id == "org.usbguard.Devices1.applyDevicePolicy" ||
action.id == "org.usbguard.Devices1.listDevices" ||
action.id == "org.usbguard1.getParameter" ||
action.id == "org.usbguard1.setParameter") &&
subject.active == true && subject.local == true &&
(${groupCheck})) {
return polkit.Result.YES;
}
});
'';
};
imports = [
(lib.mkRemovedOptionModule [ "services" "usbguard" "IPCAccessControlFiles" ]
"The usbguard module now hardcodes IPCAccessControlFiles to /var/lib/usbguard/IPCAccessControl.d."
)
(lib.mkRemovedOptionModule [
"services"
"usbguard"
"auditFilePath"
] "Removed usbguard module audit log files. Audit logs can be found in the systemd journal.")
(lib.mkRenamedOptionModule
[ "services" "usbguard" "implictPolicyTarget" ]
[ "services" "usbguard" "implicitPolicyTarget" ]
)
];
}

View File

@@ -0,0 +1,155 @@
{
config,
lib,
pkgs,
...
}:
let
format = pkgs.formats.json { };
commonOptions =
{
pkgName,
flavour ? pkgName,
}:
lib.mkOption {
default = { };
description = ''
Attribute set of ${flavour} instances.
Creates independent `${flavour}-''${name}.service` systemd units for each instance defined here.
'';
type =
with lib.types;
attrsOf (
submodule (
{ name, ... }:
{
options = {
enable = lib.mkEnableOption "this ${flavour} instance" // {
default = true;
};
package = lib.mkPackageOption pkgs pkgName { };
user = lib.mkOption {
type = types.str;
default = "root";
description = ''
User under which this instance runs.
'';
};
group = lib.mkOption {
type = types.str;
default = "root";
description = ''
Group under which this instance runs.
'';
};
settings = lib.mkOption {
type = types.submodule {
freeformType = format.type;
options = {
pid_file = lib.mkOption {
default = "/run/${flavour}/${name}.pid";
type = types.str;
description = ''
Path to use for the pid file.
'';
};
};
};
default = { };
description =
let
upstreamDocs =
if flavour == "vault-agent" then
"https://developer.hashicorp.com/vault/docs/agent#configuration-file-options"
else
"https://github.com/hashicorp/consul-template/blob/main/docs/configuration.md#configuration-file";
in
''
Free-form settings written directly to the `config.json` file.
Refer to <${upstreamDocs}> for supported values.
::: {.note}
Resulting format is JSON not HCL.
Refer to <https://www.hcl2json.com/> if you are unsure how to convert HCL options to JSON.
:::
'';
};
};
}
)
);
};
createAgentInstance =
{
instance,
name,
flavour,
}:
let
configFile = format.generate "${name}.json" instance.settings;
in
lib.mkIf (instance.enable) {
description = "${flavour} daemon - ${name}";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
path = [ pkgs.getent ];
startLimitIntervalSec = 60;
startLimitBurst = 3;
serviceConfig = {
User = instance.user;
Group = instance.group;
RuntimeDirectory = flavour;
ExecStart = "${lib.getExe instance.package} ${
lib.optionalString (flavour == "vault-agent") "agent"
} -config ${configFile}";
ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
KillSignal = "SIGINT";
TimeoutStopSec = "30s";
Restart = "on-failure";
};
};
in
{
options = {
services.consul-template.instances = commonOptions { pkgName = "consul-template"; };
services.vault-agent.instances = commonOptions {
pkgName = "vault";
flavour = "vault-agent";
};
};
config = lib.mkMerge (
map
(
flavour:
let
cfg = config.services.${flavour};
in
lib.mkIf (cfg.instances != { }) {
systemd.services = lib.mapAttrs' (
name: instance:
lib.nameValuePair "${flavour}-${name}" (createAgentInstance {
inherit name instance flavour;
})
) cfg.instances;
}
)
[
"consul-template"
"vault-agent"
]
);
meta.maintainers = with lib.maintainers; [
emilylange
tcheronneau
];
}

View File

@@ -0,0 +1,260 @@
{
config,
lib,
options,
pkgs,
...
}:
let
cfg = config.services.vault;
opt = options.services.vault;
configFile = pkgs.writeText "vault.hcl" ''
# vault in dev mode will refuse to start if its configuration sets listener
${lib.optionalString (!cfg.dev) ''
listener "tcp" {
address = "${cfg.address}"
${
if (cfg.tlsCertFile == null || cfg.tlsKeyFile == null) then
''
tls_disable = "true"
''
else
''
tls_cert_file = "${cfg.tlsCertFile}"
tls_key_file = "${cfg.tlsKeyFile}"
''
}
${cfg.listenerExtraConfig}
}
''}
storage "${cfg.storageBackend}" {
${lib.optionalString (cfg.storagePath != null) ''path = "${cfg.storagePath}"''}
${lib.optionalString (cfg.storageConfig != null) cfg.storageConfig}
}
${lib.optionalString (cfg.telemetryConfig != "") ''
telemetry {
${cfg.telemetryConfig}
}
''}
${cfg.extraConfig}
'';
allConfigPaths = [ configFile ] ++ cfg.extraSettingsPaths;
configOptions = lib.escapeShellArgs (
lib.optional cfg.dev "-dev"
++ lib.optional (cfg.dev && cfg.devRootTokenID != null) "-dev-root-token-id=${cfg.devRootTokenID}"
++ (lib.concatMap (p: [
"-config"
p
]) allConfigPaths)
);
in
{
options = {
services.vault = {
enable = lib.mkEnableOption "Vault daemon";
package = lib.mkPackageOption pkgs "vault" { };
dev = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
In this mode, Vault runs in-memory and starts unsealed. This option is not meant production but for development and testing i.e. for nixos tests.
'';
};
devRootTokenID = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Initial root token. This only applies when {option}`services.vault.dev` is true
'';
};
address = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1:8200";
description = "The name of the ip interface to listen to";
};
tlsCertFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/path/to/your/cert.pem";
description = "TLS certificate file. TLS will be disabled unless this option is set";
};
tlsKeyFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/path/to/your/key.pem";
description = "TLS private key file. TLS will be disabled unless this option is set";
};
listenerExtraConfig = lib.mkOption {
type = lib.types.lines;
default = ''
tls_min_version = "tls12"
'';
description = "Extra text appended to the listener section.";
};
storageBackend = lib.mkOption {
type = lib.types.enum [
"inmem"
"file"
"consul"
"zookeeper"
"s3"
"azure"
"dynamodb"
"etcd"
"mssql"
"mysql"
"postgresql"
"swift"
"gcs"
"raft"
];
default = "inmem";
description = "The name of the type of storage backend";
};
storagePath = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default =
if cfg.storageBackend == "file" || cfg.storageBackend == "raft" then "/var/lib/vault" else null;
defaultText = lib.literalExpression ''
if config.${opt.storageBackend} == "file" || cfg.storageBackend == "raft"
then "/var/lib/vault"
else null
'';
description = "Data directory for file backend";
};
storageConfig = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
description = ''
HCL configuration to insert in the storageBackend section.
Confidential values should not be specified here because this option's
value is written to the Nix store, which is publicly readable.
Provide credentials and such in a separate file using
[](#opt-services.vault.extraSettingsPaths).
'';
};
telemetryConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Telemetry configuration";
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Extra text appended to {file}`vault.hcl`.";
};
extraSettingsPaths = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ ];
description = ''
Configuration files to load besides the immutable one defined by the NixOS module.
This can be used to avoid putting credentials in the Nix store, which can be read by any user.
Each path can point to a JSON- or HCL-formatted file, or a directory
to be scanned for files with `.hcl` or
`.json` extensions.
To upload the confidential file with NixOps, use for example:
```
# https://releases.nixos.org/nixops/latest/manual/manual.html#opt-deployment.keys
deployment.keys."vault.hcl" = let db = import ./db-credentials.nix; in {
text = ${"''"}
storage "postgresql" {
connection_url = "postgres://''${db.username}:''${db.password}@host.example.com/exampledb?sslmode=verify-ca"
}
${"''"};
user = "vault";
};
services.vault.extraSettingsPaths = ["/run/keys/vault.hcl"];
services.vault.storageBackend = "postgresql";
users.users.vault.extraGroups = ["keys"];
```
'';
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.storageBackend == "inmem" -> (cfg.storagePath == null && cfg.storageConfig == null);
message = ''The "inmem" storage expects no services.vault.storagePath nor services.vault.storageConfig'';
}
{
assertion = (
(cfg.storageBackend == "file" -> (cfg.storagePath != null && cfg.storageConfig == null))
&& (cfg.storagePath != null -> (cfg.storageBackend == "file" || cfg.storageBackend == "raft"))
);
message = ''You must set services.vault.storagePath only when using the "file" or "raft" backend'';
}
];
users.users.vault = {
name = "vault";
group = "vault";
uid = config.ids.uids.vault;
description = "Vault daemon user";
};
users.groups.vault.gid = config.ids.gids.vault;
systemd.tmpfiles.rules = lib.optional (
cfg.storagePath != null
) "d '${cfg.storagePath}' 0700 vault vault - -";
systemd.services.vault = {
description = "Vault server daemon";
wantedBy = [ "multi-user.target" ];
after = [
"network.target"
]
++ lib.optional (config.services.consul.enable && cfg.storageBackend == "consul") "consul.service";
restartIfChanged = false; # do not restart on "nixos-rebuild switch". It would seal the storage and disrupt the clients.
startLimitIntervalSec = 60;
startLimitBurst = 3;
serviceConfig = {
User = "vault";
Group = "vault";
ExecStart = "${cfg.package}/bin/vault server ${configOptions}";
ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
StateDirectory = "vault";
# In `dev` mode vault will put its token here
Environment = lib.optional (cfg.dev) "HOME=/var/lib/vault";
PrivateDevices = true;
PrivateTmp = true;
ProtectSystem = "full";
ProtectHome = "read-only";
AmbientCapabilities = "cap_ipc_lock";
NoNewPrivileges = true;
LimitCORE = 0;
KillSignal = "SIGINT";
TimeoutStopSec = "30s";
Restart = "on-failure";
};
unitConfig.RequiresMountsFor = lib.optional (cfg.storagePath != null) cfg.storagePath;
};
};
}

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Allow use of !() when copying to not copy certain files
shopt -s extglob
# Based on: https://github.com/dani-garcia/vaultwarden/wiki/Backing-up-your-vault
if [ ! -d "$BACKUP_FOLDER" ]; then
echo "Backup folder '$BACKUP_FOLDER' does not exist" >&2
exit 1
fi
if [[ -f "$DATA_FOLDER"/db.sqlite3 ]]; then
sqlite3 "$DATA_FOLDER"/db.sqlite3 ".backup '$BACKUP_FOLDER/db.sqlite3'"
fi
if [ ! -d "$DATA_FOLDER" ]; then
echo "No data folder (yet). This will happen on first launch if backup is triggered before vaultwarden has started."
exit 0
fi
cp -r "$DATA_FOLDER"/!(db.*) "$BACKUP_FOLDER"/

View File

@@ -0,0 +1,319 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.vaultwarden;
user = config.users.users.vaultwarden.name;
group = config.users.groups.vaultwarden.name;
StateDirectory =
if lib.versionOlder config.system.stateVersion "24.11" then "bitwarden_rs" else "vaultwarden";
dataDir = "/var/lib/${StateDirectory}";
# Convert name from camel case (e.g. disable2FARemember) to upper case snake case (e.g. DISABLE_2FA_REMEMBER).
nameToEnvVar =
name:
let
parts = builtins.split "([A-Z0-9]+)" name;
partsToEnvVar =
parts:
lib.foldl' (
key: x:
let
last = lib.stringLength key - 1;
in
if lib.isList x then
key + lib.optionalString (key != "" && lib.substring last 1 key != "_") "_" + lib.head x
else if key != "" && lib.elem (lib.substring 0 1 x) lib.lowerChars then # to handle e.g. [ "disable" [ "2FAR" ] "emember" ]
lib.substring 0 last key
+ lib.optionalString (lib.substring (last - 1) 1 key != "_") "_"
+ lib.substring last 1 key
+ lib.toUpper x
else
key + lib.toUpper x
) "" parts;
in
if builtins.match "[A-Z0-9_]+" name != null then name else partsToEnvVar parts;
# Due to the different naming schemes allowed for config keys,
# we can only check for values consistently after converting them to their corresponding environment variable name.
configEnv =
let
configEnv = lib.concatMapAttrs (
name: value:
lib.optionalAttrs (value != null) {
${nameToEnvVar name} = if lib.isBool value then lib.boolToString value else toString value;
}
) cfg.config;
in
{
DATA_FOLDER = dataDir;
}
// lib.optionalAttrs (!(configEnv ? WEB_VAULT_ENABLED) || configEnv.WEB_VAULT_ENABLED == "true") {
WEB_VAULT_FOLDER = "${cfg.webVaultPackage}/share/vaultwarden/vault";
}
// configEnv;
configFile = pkgs.writeText "vaultwarden.env" (
lib.concatStrings (lib.mapAttrsToList (name: value: "${name}=${value}\n") configEnv)
);
vaultwarden = cfg.package.override { inherit (cfg) dbBackend; };
useSendmail = configEnv.USE_SENDMAIL or null == "true";
in
{
imports = [
(lib.mkRenamedOptionModule [ "services" "bitwarden_rs" ] [ "services" "vaultwarden" ])
];
options.services.vaultwarden = {
enable = lib.mkEnableOption "vaultwarden";
dbBackend = lib.mkOption {
type = lib.types.enum [
"sqlite"
"mysql"
"postgresql"
];
default = "sqlite";
description = ''
Which database backend vaultwarden will be using.
'';
};
backupDir = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = ''
The directory under which vaultwarden will backup its persistent data.
'';
example = "/var/backup/vaultwarden";
};
config = lib.mkOption {
type =
with lib.types;
attrsOf (
nullOr (oneOf [
bool
int
str
])
);
default = {
ROCKET_ADDRESS = "::1"; # default to localhost
ROCKET_PORT = 8222;
};
example = lib.literalExpression ''
{
DOMAIN = "https://bitwarden.example.com";
SIGNUPS_ALLOWED = false;
# Vaultwarden currently recommends running behind a reverse proxy
# (nginx or similar) for TLS termination, see
# https://github.com/dani-garcia/vaultwarden/wiki/Hardening-Guide#reverse-proxying
# > you should avoid enabling HTTPS via vaultwarden's built-in Rocket TLS support,
# > especially if your instance is publicly accessible.
#
# A suitable NixOS nginx reverse proxy example config might be:
#
# services.nginx.virtualHosts."bitwarden.example.com" = {
# enableACME = true;
# forceSSL = true;
# locations."/" = {
# proxyPass = "http://127.0.0.1:''${toString config.services.vaultwarden.config.ROCKET_PORT}";
# };
# };
ROCKET_ADDRESS = "127.0.0.1";
ROCKET_PORT = 8222;
ROCKET_LOG = "critical";
# This example assumes a mailserver running on localhost,
# thus without transport encryption.
# If you use an external mail server, follow:
# https://github.com/dani-garcia/vaultwarden/wiki/SMTP-configuration
SMTP_HOST = "127.0.0.1";
SMTP_PORT = 25;
SMTP_SSL = false;
SMTP_FROM = "admin@bitwarden.example.com";
SMTP_FROM_NAME = "example.com Bitwarden server";
}
'';
description = ''
The configuration of vaultwarden is done through environment variables,
therefore it is recommended to use upper snake case (e.g. {env}`DISABLE_2FA_REMEMBER`).
However, camel case (e.g. `disable2FARemember`) is also supported:
The NixOS module will convert it automatically to
upper case snake case (e.g. {env}`DISABLE_2FA_REMEMBER`).
In this conversion digits (0-9) are handled just like upper case characters,
so `foo2` would be converted to {env}`FOO_2`.
Names already in this format remain unchanged, so `FOO2` remains `FOO2` if passed as such,
even though `foo2` would have been converted to {env}`FOO_2`.
This allows working around any potential future conflicting naming conventions.
Based on the attributes passed to this config option an environment file will be generated
that is passed to vaultwarden's systemd service.
The available configuration options can be found in
[the environment template file](https://github.com/dani-garcia/vaultwarden/blob/${vaultwarden.version}/.env.template).
See [](#opt-services.vaultwarden.environmentFile) for how
to set up access to the Admin UI to invite initial users.
'';
};
environmentFile = lib.mkOption {
type = with lib.types; coercedTo path lib.singleton (listOf path);
default = [ ];
example = "/var/lib/vaultwarden.env";
description = ''
Additional environment file or files as defined in {manpage}`systemd.exec(5)`.
Secrets like {env}`ADMIN_TOKEN` and {env}`SMTP_PASSWORD`
should be passed to the service without adding them to the world-readable Nix store.
Note that this file needs to be available on the host on which `vaultwarden` is running.
As a concrete example, to make the Admin UI available (from which new users can be invited initially),
the secret {env}`ADMIN_TOKEN` needs to be defined as described
[here](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page):
```
# Admin secret token, see
# https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page
ADMIN_TOKEN=...copy-paste a unique generated secret token here...
```
'';
};
package = lib.mkPackageOption pkgs "vaultwarden" { };
webVaultPackage = lib.mkPackageOption pkgs [ "vaultwarden" "webvault" ] { };
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.backupDir != null -> cfg.dbBackend == "sqlite";
message = "Backups for database backends other than sqlite will need customization";
}
{
assertion = cfg.backupDir != null -> !(lib.hasPrefix dataDir cfg.backupDir);
message = "Backup directory can not be in ${dataDir}";
}
];
users.users.vaultwarden = {
inherit group;
isSystemUser = true;
};
users.groups.vaultwarden = { };
systemd.services.vaultwarden = {
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
path = with pkgs; [ openssl ];
serviceConfig = {
User = user;
Group = group;
EnvironmentFile = [ configFile ] ++ cfg.environmentFile;
ExecStart = lib.getExe vaultwarden;
LimitNOFILE = "1048576";
CapabilityBoundingSet = [ "" ];
DeviceAllow = [ "" ];
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = !useSendmail;
PrivateDevices = !useSendmail;
PrivateTmp = true;
PrivateUsers = !useSendmail;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "noaccess";
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
inherit StateDirectory;
StateDirectoryMode = "0700";
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
]
++ lib.optionals (!useSendmail) [
"~@privileged"
];
Restart = "always";
UMask = "0077";
};
wantedBy = [ "multi-user.target" ];
};
systemd.services.backup-vaultwarden = lib.mkIf (cfg.backupDir != null) {
description = "Backup vaultwarden";
environment = {
DATA_FOLDER = dataDir;
BACKUP_FOLDER = cfg.backupDir;
};
path = with pkgs; [ sqlite ];
# if both services are started at the same time, vaultwarden fails with "database is locked"
before = [ "vaultwarden.service" ];
serviceConfig = {
SyslogIdentifier = "backup-vaultwarden";
Type = "oneshot";
User = lib.mkDefault user;
Group = lib.mkDefault group;
ExecStart = "${pkgs.bash}/bin/bash ${./backup.sh}";
};
wantedBy = [ "multi-user.target" ];
};
systemd.timers.backup-vaultwarden = lib.mkIf (cfg.backupDir != null) {
description = "Backup vaultwarden on time";
timerConfig = {
OnCalendar = lib.mkDefault "23:00";
Persistent = "true";
Unit = "backup-vaultwarden.service";
};
wantedBy = [ "multi-user.target" ];
};
systemd.tmpfiles.settings = lib.mkIf (cfg.backupDir != null) {
"10-vaultwarden".${cfg.backupDir}.d = {
inherit user group;
mode = "0770";
};
};
};
meta = {
# uses attributes of the linked package
buildDocsInSandbox = false;
maintainers = with lib.maintainers; [
dotlambda
SuperSandro2000
];
};
}

View File

@@ -0,0 +1,60 @@
# Global configuration for yubikey-agent.
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.yubikey-agent;
in
{
###### interface
meta.maintainers = with lib.maintainers; [
philandstuff
rawkode
];
options = {
services.yubikey-agent = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to start yubikey-agent when you log in. Also sets
SSH_AUTH_SOCK to point at yubikey-agent.
Note that yubikey-agent will use whatever pinentry is
specified in programs.gnupg.agent.pinentryPackage.
'';
};
package = lib.mkPackageOption pkgs "yubikey-agent" { };
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
systemd.packages = [ cfg.package ];
# This overrides the systemd user unit shipped with the
# yubikey-agent package
systemd.user.services.yubikey-agent =
lib.mkIf (config.programs.gnupg.agent.pinentryPackage != null)
{
path = [ config.programs.gnupg.agent.pinentryPackage ];
wantedBy = [ "default.target" ];
};
# Yubikey-agent expects pcsd to be running in order to function.
services.pcscd.enable = true;
environment.extraInit = ''
if [ -z "$SSH_AUTH_SOCK" -a -n "$XDG_RUNTIME_DIR" ]; then
export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/yubikey-agent/yubikey-agent.sock"
fi
'';
};
}