refactor
This commit is contained in:
parent
66c05f9093
commit
45d6f4b0f3
205 changed files with 9040 additions and 342 deletions
97
modules/nixos/services/attic/default.nix
Normal file
97
modules/nixos/services/attic/default.nix
Normal file
|
@ -0,0 +1,97 @@
|
|||
{ lib, config, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
with lib.plusultra;
|
||||
let
|
||||
cfg = config.plusultra.services.attic;
|
||||
|
||||
toml-format = pkgs.formats.toml { };
|
||||
|
||||
raw-server-toml = toml-format.generate "server.toml" cfg.settings;
|
||||
|
||||
server-toml = pkgs.runCommand "checked-server.toml" { config = raw-server-toml; } ''
|
||||
cat $config
|
||||
|
||||
export ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="dGVzdCBzZWNyZXQ="
|
||||
export ATTIC_SERVER_DATABASE_URL="sqlite://:memory:"
|
||||
|
||||
${cfg.package}/bin/atticd --mode check-config -f "$config"
|
||||
|
||||
cat < $config > $out
|
||||
'';
|
||||
|
||||
is-local-postgres =
|
||||
let
|
||||
url = cfg.settings.database.url or "";
|
||||
local-db-strings = [ "localhost" "127.0.0.1" "/run/postgresql" ];
|
||||
is-local-db-url = any (flip hasInfix url) local-db-strings;
|
||||
in
|
||||
config.services.postgresql.enable
|
||||
&& hasPrefix "postgresql://" url
|
||||
&& is-local-db-url;
|
||||
in
|
||||
{
|
||||
options.plusultra.services.attic = {
|
||||
enable = mkEnableOption "Attic";
|
||||
|
||||
package = mkOpt types.package pkgs.attic-server "The attic-server package to use.";
|
||||
|
||||
credentials = mkOpt (types.nullOr types.path) null "The path to an optional EnvironmentFile for the atticd service to use.";
|
||||
|
||||
user = mkOpt types.str "atticd" "The user under which attic runs.";
|
||||
group = mkOpt types.str "atticd" "The group under which attic runs.";
|
||||
|
||||
settings = mkOpt toml-format.type { } "Settings for the atticd config file.";
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = !isStorePath cfg.credentials;
|
||||
message = "plusultra.services.attic.credentials CANNOT be in the Nix Store.";
|
||||
}
|
||||
];
|
||||
|
||||
users = {
|
||||
users = optionalAttrs (cfg.user == "atticd") {
|
||||
atticd = {
|
||||
group = cfg.group;
|
||||
isSystemUser = true;
|
||||
};
|
||||
};
|
||||
|
||||
groups = optionalAttrs (cfg.group == "atticd") {
|
||||
atticd = { };
|
||||
};
|
||||
};
|
||||
|
||||
plusultra = {
|
||||
tools.attic = enabled;
|
||||
|
||||
services.attic.settings = {
|
||||
database.url = mkDefault "sqlite:///var/lib/atticd/server.db?mode=rwc";
|
||||
|
||||
storage = mkDefault {
|
||||
type = "local";
|
||||
path = "/var/lib/atticd/storage";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.atticd = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" ]
|
||||
++ optionals is-local-postgres [ "postgresql.service" "nss-lookup.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = "${cfg.package}/bin/atticd -f ${server-toml}";
|
||||
StateDirectory = "atticd";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
DynamicUser = true;
|
||||
} // optionalAttrs (cfg.credentials != null) {
|
||||
EnvironmentFile = mkDefault cfg.credentials;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
41
modules/nixos/services/avahi/default.nix
Normal file
41
modules/nixos/services/avahi/default.nix
Normal file
|
@ -0,0 +1,41 @@
|
|||
{ lib, config, options, ... }:
|
||||
|
||||
let
|
||||
cfg = config.plusultra.services.avahi;
|
||||
|
||||
inherit (lib) types mkEnableOption mkIf;
|
||||
in
|
||||
{
|
||||
options.plusultra.services.avahi = with types; {
|
||||
enable = mkEnableOption "Avahi";
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.avahi = {
|
||||
enable = true;
|
||||
nssmdns = true;
|
||||
publish = {
|
||||
enable = true;
|
||||
addresses = true;
|
||||
domain = true;
|
||||
hinfo = true;
|
||||
userServices = true;
|
||||
workstation = true;
|
||||
};
|
||||
|
||||
extraServiceFiles = {
|
||||
smb = ''
|
||||
<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
|
||||
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
|
||||
<service-group>
|
||||
<name replace-wildcards="yes">%h</name>
|
||||
<service>
|
||||
<type>_smb._tcp</type>
|
||||
<port>445</port>
|
||||
</service>
|
||||
</service-group>
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
66
modules/nixos/services/cowsay-mastodon-poster/default.nix
Normal file
66
modules/nixos/services/cowsay-mastodon-poster/default.nix
Normal file
|
@ -0,0 +1,66 @@
|
|||
{ lib, pkgs, config, ... }:
|
||||
|
||||
let
|
||||
inherit (lib) types mkIf;
|
||||
inherit (lib.plusultra) mkBoolOpt mkOpt;
|
||||
inherit (pkgs) fortune toot;
|
||||
inherit (pkgs.snowfallorg) cow2img;
|
||||
|
||||
cfg = config.plusultra.services.cowsay-mastodon-poster;
|
||||
|
||||
script = ''
|
||||
if [ ! -f ~/.config/toot/config.json ]; then
|
||||
echo "File ~/.config/toot/config.json does not exist. Run 'toot login_cli' first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmp_dir=$(mktemp -d)
|
||||
|
||||
pushd $tmp_dir > /dev/null
|
||||
${cow2img}/bin/cow2img --no-spinner ${if cfg.short then "--message \"$(${fortune}/bin/fortune -s)\"" else ""}
|
||||
|
||||
cow_name=$(cat ./cow/name)
|
||||
cow_message=$(cat ./cow/message)
|
||||
|
||||
post="$cow_name saying:"$'\n\n'"$cow_message"
|
||||
|
||||
${toot}/bin/toot post --media ./cow/image.png --description "$post" "#hachybots"
|
||||
popd > /dev/null
|
||||
|
||||
rm -rf $tmp_dir
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.plusultra.services.cowsay-mastodon-poster = with types; {
|
||||
enable = mkBoolOpt false "Whether or not to enable cowsay posts.";
|
||||
short = mkBoolOpt false "Use short fortunes only.";
|
||||
user = mkOpt str config.plusultra.user.name "The user to run as.";
|
||||
group = mkOpt str "users" "The group to run as.";
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd = {
|
||||
timers.cowsay-mastodon-poster = {
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
# Run once a day at 10am.
|
||||
OnCalendar = "*-*-* 10:00:00";
|
||||
Unit = "cowsay-mastodon-poster.service";
|
||||
};
|
||||
};
|
||||
|
||||
services.cowsay-mastodon-poster = {
|
||||
after = [ "network-online.target" ];
|
||||
description = "Post a cowsay image to Mastodon.";
|
||||
|
||||
inherit script;
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
198
modules/nixos/services/dex/default.nix
Normal file
198
modules/nixos/services/dex/default.nix
Normal file
|
@ -0,0 +1,198 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
inherit (builtins) map removeAttrs;
|
||||
inherit (lib) mapAttrs flatten concatMap concatMapStringsSep;
|
||||
|
||||
cfg = config.plusultra.services.dex;
|
||||
|
||||
process-client-settings = client:
|
||||
if client ? secretFile then
|
||||
(removeAttrs client [ "secretFile" ])
|
||||
// { secret = client.secretFile; }
|
||||
else
|
||||
client;
|
||||
|
||||
settings =
|
||||
mapAttrs
|
||||
(name: value:
|
||||
if name == "staticClients" then
|
||||
map process-client-settings value
|
||||
else
|
||||
value
|
||||
)
|
||||
(cfg.settings // {
|
||||
storage = (cfg.settings.storage or { }) // {
|
||||
type = cfg.settings.storage.type or "sqlite3";
|
||||
config = cfg.settings.storage.config or {
|
||||
file = "${cfg.stateDir}/dex.db";
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
secret-files = concatMap
|
||||
(client:
|
||||
if client ? secretFile then
|
||||
[ client.secretFile ]
|
||||
else
|
||||
[ ]
|
||||
)
|
||||
(settings.staticClients or [ ]);
|
||||
|
||||
format = pkgs.formats.yaml { };
|
||||
|
||||
configYaml = format.generate "config.yaml" settings;
|
||||
|
||||
replace-config-secrets = pkgs.writeShellScript "replace-config-secrets"
|
||||
(concatMapStringsSep "\n"
|
||||
(file: "${pkgs.replace-secret}/bin/replace-secret '${file}' '${file}' ${cfg.stateDir}/config.yaml")
|
||||
secret-files
|
||||
);
|
||||
|
||||
in
|
||||
{
|
||||
options.plusultra.services.dex = {
|
||||
enable = lib.mkEnableOption "Dex, the OpenID Connect and OAuth 2 identity provider";
|
||||
|
||||
stateDir = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
default = "/var/lib/dex";
|
||||
description = "The state directory where config and data are stored.";
|
||||
};
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "dex";
|
||||
description = "The user to run Dex as.";
|
||||
};
|
||||
|
||||
group = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "dex";
|
||||
description = "The group to run Dex as.";
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
type = format.type;
|
||||
default = { };
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
# External url
|
||||
issuer = "http://127.0.0.1:5556/dex";
|
||||
storage = {
|
||||
type = "postgres";
|
||||
config.host = "/var/run/postgres";
|
||||
};
|
||||
web = {
|
||||
http = "127.0.0.1:5556";
|
||||
};
|
||||
enablePasswordDB = true;
|
||||
staticClients = [
|
||||
{
|
||||
id = "oidcclient";
|
||||
name = "Client";
|
||||
redirectURIs = [ "https://example.com/callback" ];
|
||||
|
||||
# The content of `secretFile` will be written into to the config as `secret`.
|
||||
secretFile = "/etc/dex/oidcclient";
|
||||
}
|
||||
];
|
||||
}
|
||||
'';
|
||||
description = lib.mdDoc ''
|
||||
The available options can be found in
|
||||
[the example configuration](https://github.com/dexidp/dex/blob/v${pkgs.dex.version}/config.yaml.dist).
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
users = {
|
||||
users = lib.optionalAttrs (cfg.user == "dex") {
|
||||
dex = {
|
||||
group = cfg.group;
|
||||
home = cfg.stateDir;
|
||||
isSystemUser = true;
|
||||
};
|
||||
};
|
||||
|
||||
groups = lib.optionalAttrs (cfg.group == "dex") {
|
||||
dex = { };
|
||||
};
|
||||
};
|
||||
|
||||
systemd = {
|
||||
tmpfiles.rules = [
|
||||
"d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group}"
|
||||
];
|
||||
|
||||
services = {
|
||||
dex = {
|
||||
description = "dex identity provider";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "networking.target" ];
|
||||
|
||||
preStart = ''
|
||||
cp --remove-destination ${configYaml} ${cfg.stateDir}/config.yaml
|
||||
|
||||
chmod 600 ${cfg.stateDir}/config.yaml
|
||||
|
||||
${replace-config-secrets}
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = "${pkgs.dex-oidc}/bin/dex serve ${cfg.stateDir}/config.yaml";
|
||||
WorkingDirectory = cfg.stateDir;
|
||||
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
|
||||
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
||||
BindReadOnlyPaths = [
|
||||
"/nix/store"
|
||||
"-/etc/resolv.conf"
|
||||
"-/etc/nsswitch.conf"
|
||||
"-/etc/hosts"
|
||||
"-/etc/localtime"
|
||||
"-/etc/dex"
|
||||
];
|
||||
BindPaths = [ cfg.stateDir ] ++ lib.optional (settings.storage.type == "postgres") "/var/run/postgresql";
|
||||
CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
|
||||
## ProtectClock= adds DeviceAllow=char-rtc r
|
||||
#DeviceAllow = "";
|
||||
#DynamicUser = true;
|
||||
#LockPersonality = true;
|
||||
#MemoryDenyWriteExecute = true;
|
||||
#NoNewPrivileges = true;
|
||||
#PrivateDevices = true;
|
||||
#PrivateMounts = true;
|
||||
## Port needs to be exposed to the host network
|
||||
##PrivateNetwork = true;
|
||||
#PrivateTmp = true;
|
||||
#PrivateUsers = true;
|
||||
#ProcSubset = "pid";
|
||||
#ProtectClock = true;
|
||||
#ProtectHome = true;
|
||||
#ProtectHostname = true;
|
||||
## Would re-mount paths ignored by temporary root
|
||||
##ProtectSystem = "strict";
|
||||
#ProtectControlGroups = true;
|
||||
#ProtectKernelLogs = true;
|
||||
#ProtectKernelModules = true;
|
||||
#ProtectKernelTunables = true;
|
||||
#ProtectProc = "invisible";
|
||||
#RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
|
||||
#RestrictNamespaces = true;
|
||||
#RestrictRealtime = true;
|
||||
#RestrictSUIDSGID = true;
|
||||
#SystemCallArchitectures = "native";
|
||||
#SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
|
||||
#TemporaryFileSystem = "/:ro";
|
||||
# Does not work well with the temporary root
|
||||
#UMask = "0066";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
121
modules/nixos/services/dex/default.orig.nix
Normal file
121
modules/nixos/services/dex/default.orig.nix
Normal file
|
@ -0,0 +1,121 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.dex;
|
||||
fixClient = client: if client ? secretFile then ((builtins.removeAttrs client [ "secretFile" ]) // { secret = client.secretFile; }) else client;
|
||||
filteredSettings = mapAttrs (n: v: if n == "staticClients" then (builtins.map fixClient v) else v) cfg.settings;
|
||||
secretFiles = flatten (builtins.map (c: if c ? secretFile then [ c.secretFile ] else [ ]) (cfg.settings.staticClients or [ ]));
|
||||
|
||||
settingsFormat = pkgs.formats.yaml { };
|
||||
configFile = settingsFormat.generate "config.yaml" filteredSettings;
|
||||
|
||||
startPreScript = pkgs.writeShellScript "dex-start-pre" (''
|
||||
'' + (concatStringsSep "\n" (builtins.map
|
||||
(file: ''
|
||||
${pkgs.replace-secret}/bin/replace-secret '${file}' '${file}' /run/dex/config.yaml
|
||||
'')
|
||||
secretFiles)));
|
||||
in
|
||||
{
|
||||
options.services.dex = {
|
||||
enable = mkEnableOption "the OpenID Connect and OAuth2 identity provider";
|
||||
|
||||
settings = mkOption {
|
||||
type = settingsFormat.type;
|
||||
default = { };
|
||||
example = literalExpression ''
|
||||
{
|
||||
# External url
|
||||
issuer = "http://127.0.0.1:5556/dex";
|
||||
storage = {
|
||||
type = "postgres";
|
||||
config.host = "/var/run/postgres";
|
||||
};
|
||||
web = {
|
||||
http = "127.0.0.1:5556";
|
||||
};
|
||||
enablePasswordDB = true;
|
||||
staticClients = [
|
||||
{
|
||||
id = "oidcclient";
|
||||
name = "Client";
|
||||
redirectURIs = [ "https://example.com/callback" ];
|
||||
secretFile = "/etc/dex/oidcclient"; # The content of `secretFile` will be written into to the config as `secret`.
|
||||
}
|
||||
];
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
The available options can be found in
|
||||
<link xlink:href="https://github.com/dexidp/dex/blob/v${pkgs.dex.version}/config.yaml.dist">the example configuration</link>.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.dex = {
|
||||
description = "dex identity provider";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "networking.target" ] ++ (optional (cfg.settings.storage.type == "postgres") "postgresql.service");
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = "${pkgs.dex-oidc}/bin/dex serve /run/dex/config.yaml";
|
||||
ExecStartPre = [
|
||||
"${pkgs.coreutils}/bin/install -m 600 ${configFile} /run/dex/config.yaml"
|
||||
"+${startPreScript}"
|
||||
];
|
||||
RuntimeDirectory = "dex";
|
||||
|
||||
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
||||
BindReadOnlyPaths = [
|
||||
"/nix/store"
|
||||
"-/etc/resolv.conf"
|
||||
"-/etc/nsswitch.conf"
|
||||
"-/etc/hosts"
|
||||
"-/etc/localtime"
|
||||
"-/etc/dex"
|
||||
];
|
||||
BindPaths = optional (cfg.settings.storage.type == "postgres") "/var/run/postgresql";
|
||||
CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
|
||||
# ProtectClock= adds DeviceAllow=char-rtc r
|
||||
DeviceAllow = "";
|
||||
DynamicUser = true;
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateDevices = true;
|
||||
PrivateMounts = true;
|
||||
# Port needs to be exposed to the host network
|
||||
#PrivateNetwork = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = true;
|
||||
ProcSubset = "pid";
|
||||
ProtectClock = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
# Would re-mount paths ignored by temporary root
|
||||
#ProtectSystem = "strict";
|
||||
ProtectControlGroups = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
|
||||
TemporaryFileSystem = "/:ro";
|
||||
# Does not work well with the temporary root
|
||||
#UMask = "0066";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# uses attributes of the linked package
|
||||
meta.buildDocsInSandbox = false;
|
||||
}
|
||||
|
79
modules/nixos/services/homer/default.nix
Normal file
79
modules/nixos/services/homer/default.nix
Normal file
|
@ -0,0 +1,79 @@
|
|||
{ lib, config, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
with lib.plusultra;
|
||||
let
|
||||
cfg = config.plusultra.services.homer;
|
||||
|
||||
yaml-format = pkgs.formats.yaml { };
|
||||
settings-yaml = yaml-format.generate "config.yml" cfg.settings;
|
||||
|
||||
settings-path =
|
||||
if cfg.settings-path != null then
|
||||
cfg.settings-path
|
||||
else
|
||||
builtins.toString settings-yaml;
|
||||
in
|
||||
{
|
||||
options.plusultra.services.homer = {
|
||||
enable = mkEnableOption "Homer";
|
||||
|
||||
package = mkOpt types.package pkgs.plusultra.homer "The package of Homer assets to use.";
|
||||
|
||||
settings = mkOpt yaml-format.type { } "Configuration for Homer's config.yml file.";
|
||||
settings-path = mkOpt (types.nullOr types.path) null "A replacement for the generated config.yml file.";
|
||||
|
||||
host = mkOpt (types.nullOr types.str) null "The host to serve Homer on.";
|
||||
|
||||
nginx = {
|
||||
forceSSL = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether or not to force the use of SSL.";
|
||||
};
|
||||
};
|
||||
|
||||
acme = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description =
|
||||
"Whether or not to automatically fetch and configure SSL certs.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.host != null;
|
||||
message = "plusultra.services.homer.host must be set.";
|
||||
}
|
||||
{
|
||||
assertion = cfg.settings-path != null -> cfg.settings == { };
|
||||
message = "plusultra.services.homer.settings and plusultra.services.homer.settings-path are mutually exclusive.";
|
||||
}
|
||||
{
|
||||
assertion = cfg.nginx.forceSSL -> cfg.acme.enable;
|
||||
message = "plusultra.services.homer.nginx.forceSSL requires setting plusultra.services.homer.acme.enable to true.";
|
||||
}
|
||||
];
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
|
||||
virtualHosts."${cfg.host}" = {
|
||||
enableACME = cfg.acme.enable;
|
||||
forceSSL = cfg.nginx.forceSSL;
|
||||
|
||||
locations."/" = {
|
||||
root = "${cfg.package}/share/homer";
|
||||
};
|
||||
|
||||
locations."= /assets/config.yml" = {
|
||||
alias = settings-path;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
158
modules/nixos/services/infrared/default.nix
Normal file
158
modules/nixos/services/infrared/default.nix
Normal file
|
@ -0,0 +1,158 @@
|
|||
{ config, options, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
inherit (builtins) toString;
|
||||
inherit (lib) types;
|
||||
|
||||
cfg = config.plusultra.services.infrared;
|
||||
|
||||
format = pkgs.formats.json { };
|
||||
|
||||
serversType = (types.submodule ({ config, ... }: {
|
||||
options = {
|
||||
domain = lib.mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = ''
|
||||
The domain to proxy. Should be fully qualified domain name.
|
||||
Note: Every string is accepted. So localhost is also valid.
|
||||
'';
|
||||
example = "minecraft.example.com";
|
||||
};
|
||||
|
||||
host = lib.mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "The host where the Minecraft server is running. Defaults to local host.";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = types.port;
|
||||
default = 25566;
|
||||
description = "The port where the Minecraft server is running.";
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
default = { };
|
||||
description = ''
|
||||
Infrared configuration (<filename>config.json</filename>). Refer to
|
||||
<link xlink:href="https://github.com/haveachin/infrared#proxy-config" />
|
||||
for details.
|
||||
'';
|
||||
|
||||
type = types.submodule {
|
||||
freeformType = format.type;
|
||||
|
||||
options = {
|
||||
domainName = lib.mkOption {
|
||||
type = types.str;
|
||||
default = config.domain;
|
||||
defaultText = lib.literalExpression ''
|
||||
""
|
||||
'';
|
||||
description = "The domain to proxy.";
|
||||
};
|
||||
|
||||
proxyTo = lib.mkOption {
|
||||
type = types.str;
|
||||
default = "${config.host}:${toString config.port}";
|
||||
defaultText = ":25565";
|
||||
description = "The address that the proxy should send incoming connections to.";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}));
|
||||
in
|
||||
{
|
||||
options.plusultra.services.infrared = {
|
||||
enable = lib.mkEnableOption "Infrared";
|
||||
|
||||
stateDir = lib.mkOption {
|
||||
type = types.path;
|
||||
default = "/var/lib/infrared";
|
||||
description = "The state directory where configurations are stored.";
|
||||
};
|
||||
|
||||
user = lib.mkOption {
|
||||
type = types.str;
|
||||
default = "infrared";
|
||||
description = "User under which Infrared is ran.";
|
||||
};
|
||||
|
||||
group = lib.mkOption {
|
||||
type = types.str;
|
||||
default = "infrared";
|
||||
description = "Group under which Infrared is ran.";
|
||||
};
|
||||
|
||||
openFirewall = lib.mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to open the firewall for ports specified by each server's listenTo address.";
|
||||
};
|
||||
|
||||
servers = lib.mkOption {
|
||||
type = types.listOf serversType;
|
||||
default = [ ];
|
||||
description = "The servers to proxy.";
|
||||
example = lib.literalExpression ''
|
||||
[
|
||||
{
|
||||
domain = "minecraft.example.com";
|
||||
port = 25567;
|
||||
}
|
||||
]
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
];
|
||||
|
||||
networking.firewall = lib.mkIf cfg.openFirewall {
|
||||
allowedUDPPorts = builtins.map (server: server.port) cfg.servers;
|
||||
allowedTCPPorts = builtins.map (server: server.port) cfg.servers;
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules =
|
||||
[ "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" ] ++
|
||||
builtins.map
|
||||
(server:
|
||||
let
|
||||
config = format.generate "${server.domain}.json" server.settings;
|
||||
in
|
||||
"L+ '${cfg.stateDir}/${server.domain}.json' - - - - ${config}"
|
||||
)
|
||||
cfg.servers;
|
||||
|
||||
systemd.services.infrared = {
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
WorkingDirectory = cfg.stateDir;
|
||||
ExecStart = "${pkgs.plusultra.infrared}/bin/infrared -config-path ${cfg.stateDir}";
|
||||
};
|
||||
};
|
||||
|
||||
users = {
|
||||
users = lib.optionalAttrs (cfg.user == "infrared") {
|
||||
infrared = {
|
||||
group = cfg.group;
|
||||
home = "/var/lib/infrared";
|
||||
isSystemUser = true;
|
||||
};
|
||||
};
|
||||
|
||||
groups = lib.optionalAttrs (cfg.group == "infrared") {
|
||||
infrared = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
195
modules/nixos/services/minecraft/default.nix
Normal file
195
modules/nixos/services/minecraft/default.nix
Normal file
|
@ -0,0 +1,195 @@
|
|||
{ config, options, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
inherit (lib) types;
|
||||
|
||||
cfg = config.plusultra.services.minecraft;
|
||||
in
|
||||
{
|
||||
options.plusultra.services.minecraft = {
|
||||
enable = lib.mkEnableOption "Minecraft server";
|
||||
|
||||
eula = lib.mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = lib.mdDoc ''
|
||||
Whether you agree to
|
||||
[Mojang's EULA](https://account.mojang.com/documents/minecraft_eula).
|
||||
This option must be set to `true` to run Minecraft server.
|
||||
'';
|
||||
};
|
||||
|
||||
infrared = {
|
||||
enable = lib.mkEnableOption "Infrared";
|
||||
};
|
||||
|
||||
servers = lib.mkOption {
|
||||
default = { };
|
||||
description = "The Minecraft servers to run.";
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
# A default vanilla server.
|
||||
vanilla-1 = {};
|
||||
|
||||
# A vanilla server with a custom port.
|
||||
vanilla-2 = {
|
||||
port = 4000;
|
||||
};
|
||||
|
||||
# A vanilla server proxied by Infrared (when enabled).
|
||||
vanilla-3 = {
|
||||
domain = "minecraft.example.com";
|
||||
};
|
||||
|
||||
# A Forge server.
|
||||
forge-1 = {
|
||||
type = "forge";
|
||||
};
|
||||
|
||||
# Use a custom Minecraft server version.
|
||||
custom-vanilla = {
|
||||
package = pkgs.minecraft-server_1_12_2;
|
||||
};
|
||||
|
||||
# Use a custom Forge server version.
|
||||
custom-forge = {
|
||||
type = "forge";
|
||||
package = pkgs.minecraft-forge_1_19_2-43_1_25;
|
||||
};
|
||||
}
|
||||
'';
|
||||
|
||||
type = types.attrsOf (types.submodule ({ config, name, ... }: {
|
||||
options = {
|
||||
type = lib.mkOption {
|
||||
type = types.enum [ "vanilla" "forge" ];
|
||||
default = "vanilla";
|
||||
description = "The kind of Minecraft server to create.";
|
||||
};
|
||||
|
||||
package = lib.mkOption {
|
||||
type = types.package;
|
||||
default =
|
||||
if config.type == "vanilla" then
|
||||
pkgs.minecraft-server
|
||||
else
|
||||
pkgs.plusultra.minecraft-forge;
|
||||
defaultText = lib.literalExpression ''
|
||||
pkgs.minecraft-server
|
||||
'';
|
||||
};
|
||||
|
||||
dataDir = lib.mkOption {
|
||||
type = types.path;
|
||||
default = "/var/lib/minecraft/${name}";
|
||||
defaultText = "/var/lib/minecraft/<name>";
|
||||
description = "The datrectory where data for the server is stored.";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = types.port;
|
||||
default = 25565;
|
||||
description = "The port for the server to listen on.";
|
||||
};
|
||||
|
||||
domain = lib.mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "The domain to pass to Infrared (if enabled).";
|
||||
};
|
||||
|
||||
jvmOpts = lib.mkOption {
|
||||
type = types.separatedString " ";
|
||||
default = "-Xmx2048M -Xms2048M";
|
||||
# Example options from https://minecraft.gamepedia.com/Tutorials/Server_startup_script
|
||||
example = "-Xms4092M -Xmx4092M -XX:+UseG1GC -XX:+CMSIncrementalPacing "
|
||||
+ "-XX:+CMSClassUnloadingEnabled -XX:ParallelGCThreads=2 "
|
||||
+ "-XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10";
|
||||
description = lib.mdDoc "JVM options for the Minecraft server.";
|
||||
};
|
||||
|
||||
serverProperties = lib.mkOption {
|
||||
type = types.attrsOf (types.oneOf [ types.bool types.int types.str ]);
|
||||
default = { };
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
server-port = 43000;
|
||||
difficulty = 3;
|
||||
gamemode = 1;
|
||||
max-players = 5;
|
||||
motd = "NixOS Minecraft server!";
|
||||
white-list = true;
|
||||
enable-rcon = true;
|
||||
"rcon.password" = "hunter2";
|
||||
}
|
||||
'';
|
||||
description = lib.mdDoc ''
|
||||
Minecraft server properties for the server.properties file. Only has
|
||||
an effect when {option}`services.minecraft-server.declarative`
|
||||
is set to `true`. See
|
||||
<https://minecraft.gamepedia.com/Server.properties#Java_Edition_3>
|
||||
for documentation on these values.
|
||||
'';
|
||||
};
|
||||
|
||||
whitelist = lib.mkOption {
|
||||
type =
|
||||
let
|
||||
minecraftUUID = lib.types.strMatching
|
||||
"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" // {
|
||||
description = "Minecraft UUID";
|
||||
};
|
||||
in
|
||||
lib.types.attrsOf minecraftUUID;
|
||||
default = { };
|
||||
description = lib.mdDoc ''
|
||||
Whitelisted players, only has an effect when
|
||||
{option}`services.minecraft-server.declarative` is
|
||||
`true` and the whitelist is enabled
|
||||
via {option}`services.minecraft-server.serverProperties` by
|
||||
setting `white-list` to `true`.
|
||||
This is a mapping from Minecraft usernames to UUIDs.
|
||||
You can use <https://mcuuid.net/> to get a
|
||||
Minecraft UUID for a username.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
username1 = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
|
||||
username2 = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy";
|
||||
};
|
||||
'';
|
||||
};
|
||||
|
||||
openFirewall = lib.mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = lib.mdDoc ''
|
||||
Whether to open ports in the firewall for the server.
|
||||
'';
|
||||
};
|
||||
|
||||
declarative = lib.mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = lib.mdDoc ''
|
||||
Whether to use a declarative Minecraft server configuration.
|
||||
Only if set to `true`, the options
|
||||
{option}`plusultra.services.minecraft.servers.<name>.whitelist` and
|
||||
{option}`plusultra.services.minecraft.servers.<name>.serverProperties` will be
|
||||
applied.
|
||||
'';
|
||||
};
|
||||
|
||||
extraInfraredOptions = lib.mkOption {
|
||||
type = types.attrs;
|
||||
default = { };
|
||||
|
||||
description = lib.mdDoc ''
|
||||
Extra options passed to Infrared (if enabled) when configuring this server.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}));
|
||||
};
|
||||
};
|
||||
}
|
109
modules/nixos/services/openssh/default.nix
Normal file
109
modules/nixos/services/openssh/default.nix
Normal file
|
@ -0,0 +1,109 @@
|
|||
{ options
|
||||
, config
|
||||
, pkgs
|
||||
, lib
|
||||
, host ? ""
|
||||
, format ? ""
|
||||
, inputs ? { }
|
||||
, ...
|
||||
}:
|
||||
with lib;
|
||||
with lib.plusultra; let
|
||||
cfg = config.plusultra.services.openssh;
|
||||
|
||||
user = config.users.users.${config.plusultra.user.name};
|
||||
user-id = builtins.toString user.uid;
|
||||
|
||||
# TODO: This is a hold-over from an earlier Snowfall Lib version which used
|
||||
# the specialArg `name` to provide the host name.
|
||||
name = host;
|
||||
|
||||
default-key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCwaaCUq3Ooq1BaHbg5IwVxWj/xmNJY2dDthHKPZefrHXv/ksM/IREgm38J0CdoMpVS0Zp1C/vFrwGfaYZ2lCF5hBVdV3gf+mvj8Yb8Xpm6aM4L5ig+oBMp/3cz1+g/I4aLMJfCKCtdD6Q2o4vtkTpid6X+kL3UGZbX0HFn3pxoDinzOXQnVGSGw+pQhLASvQeVXWTJjVfIWhj9L2NRJau42cBRRlAH9kE3HUbcgLgyPUZ28aGXLLmiQ6CUjiIlce5ee16WNLHQHOzVfPJfF1e1F0HwGMMBe39ey3IEQz6ab1YqlIzjRx9fQ9hQK6Du+Duupby8JmBlbUAxhh8KJFCJB2cXW/K5Et4R8GHMS6MyIoKQwFUXGyrszVfiuNTGZIkPAYx9zlCq9M/J+x1xUZLHymL85WLPyxhlhN4ysM9ILYiyiJ3gYrPIn5FIZrW7MCQX4h8k0bEjWUwH5kF3dZpEvIT2ssyIu12fGzXkYaNQcJEb5D9gT1mNyi2dxQ62NPZ5orfYyIZ7fn22d1P/jegG+7LQeXPiy5NLE6b7MP5Rq2dL8Y9Oi8pOBtoY9BpLh7saSBbNFXTBtH/8OfAQacxDsZD/zTFtCzZjtTK6yiAaXCZTvMIOuoYGZvEk6zWXrjVsU8FlqF+4JOTfePqr/SSUXNJyKnrvQJ1BfHQiYsrckw==";
|
||||
|
||||
other-hosts =
|
||||
lib.filterAttrs
|
||||
(key: host:
|
||||
key != name && (host.config.plusultra.user.name or null) != null)
|
||||
((inputs.self.nixosConfigurations or { }) // (inputs.self.darwinConfigurations or { }));
|
||||
|
||||
other-hosts-config =
|
||||
lib.concatMapStringsSep
|
||||
"\n"
|
||||
(
|
||||
name:
|
||||
let
|
||||
remote = other-hosts.${name};
|
||||
remote-user-name = remote.config.plusultra.user.name;
|
||||
remote-user-id = builtins.toString remote.config.users.users.${remote-user-name}.uid;
|
||||
|
||||
forward-gpg =
|
||||
optionalString (config.programs.gnupg.agent.enable && remote.config.programs.gnupg.agent.enable)
|
||||
''
|
||||
RemoteForward /run/user/${remote-user-id}/gnupg/S.gpg-agent /run/user/${user-id}/gnupg/S.gpg-agent.extra
|
||||
RemoteForward /run/user/${remote-user-id}/gnupg/S.gpg-agent.ssh /run/user/${user-id}/gnupg/S.gpg-agent.ssh
|
||||
'';
|
||||
in
|
||||
''
|
||||
Host ${name}
|
||||
User ${remote-user-name}
|
||||
ForwardAgent yes
|
||||
Port ${builtins.toString cfg.port}
|
||||
${forward-gpg}
|
||||
''
|
||||
)
|
||||
(builtins.attrNames other-hosts);
|
||||
in
|
||||
{
|
||||
options.plusultra.services.openssh = with types; {
|
||||
enable = mkBoolOpt false "Whether or not to configure OpenSSH support.";
|
||||
authorizedKeys =
|
||||
mkOpt (listOf str) [ default-key ] "The public keys to apply.";
|
||||
port = mkOpt port 2222 "The port to listen on (in addition to 22).";
|
||||
manage-other-hosts = mkOpt bool true "Whether or not to add other host configurations to SSH config.";
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
|
||||
settings = {
|
||||
PermitRootLogin =
|
||||
if format == "install-iso"
|
||||
then "yes"
|
||||
else "no";
|
||||
PasswordAuthentication = false;
|
||||
};
|
||||
|
||||
extraConfig = ''
|
||||
StreamLocalBindUnlink yes
|
||||
'';
|
||||
|
||||
ports = [
|
||||
22
|
||||
cfg.port
|
||||
];
|
||||
};
|
||||
|
||||
programs.ssh.extraConfig = ''
|
||||
Host *
|
||||
HostKeyAlgorithms +ssh-rsa
|
||||
|
||||
${optionalString cfg.manage-other-hosts other-hosts-config}
|
||||
'';
|
||||
|
||||
plusultra.user.extraOptions.openssh.authorizedKeys.keys =
|
||||
cfg.authorizedKeys;
|
||||
|
||||
plusultra.home.extraOptions = {
|
||||
programs.zsh.shellAliases =
|
||||
foldl
|
||||
(aliases: system:
|
||||
aliases
|
||||
// {
|
||||
"ssh-${system}" = "ssh ${system} -t tmux a";
|
||||
})
|
||||
{ }
|
||||
(builtins.attrNames other-hosts);
|
||||
};
|
||||
};
|
||||
}
|
13
modules/nixos/services/printing/default.nix
Normal file
13
modules/nixos/services/printing/default.nix
Normal file
|
@ -0,0 +1,13 @@
|
|||
{ options, config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
with lib.plusultra;
|
||||
let cfg = config.plusultra.services.printing;
|
||||
in
|
||||
{
|
||||
options.plusultra.services.printing = with types; {
|
||||
enable = mkBoolOpt false "Whether or not to configure printing support.";
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable { services.printing.enable = true; };
|
||||
}
|
76
modules/nixos/services/samba/default.nix
Normal file
76
modules/nixos/services/samba/default.nix
Normal file
|
@ -0,0 +1,76 @@
|
|||
{ lib, config, ... }:
|
||||
|
||||
let
|
||||
cfg = config.plusultra.services.samba;
|
||||
|
||||
inherit (lib)
|
||||
types
|
||||
mkEnableOption
|
||||
mkIf
|
||||
mapAttrs
|
||||
optionalAttrs;
|
||||
|
||||
inherit (lib.plusultra)
|
||||
mkOpt
|
||||
mkBoolOpt;
|
||||
|
||||
bool-to-yes-no = value: if value then "yes" else "no";
|
||||
|
||||
shares-submodule = with types; submodule ({ name, ... }: {
|
||||
options = {
|
||||
path = mkOpt str null "The path to serve.";
|
||||
public = mkBoolOpt false "Whether the share is public.";
|
||||
browseable = mkBoolOpt true "Whether the share is browseable.";
|
||||
comment = mkOpt str name "An optional comment.";
|
||||
read-only = mkBoolOpt false "Whether the share should be read only.";
|
||||
only-owner-editable = mkBoolOpt false "Whether the share is only writable by the system owner (plusultra.user.name).";
|
||||
|
||||
extra-config = mkOpt attrs { } "Extra configuration options for the share.";
|
||||
};
|
||||
});
|
||||
in
|
||||
{
|
||||
options.plusultra.services.samba = with types; {
|
||||
enable = mkEnableOption "Samba";
|
||||
workgroup = mkOpt str "WORKGROUP" "The workgroup to use.";
|
||||
browseable = mkBoolOpt true "Whether the shares are browseable.";
|
||||
|
||||
shares = mkOpt (attrsOf shares-submodule) { } "The shares to serve.";
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
networking.firewall = {
|
||||
allowedTCPPorts = [ 5357 ];
|
||||
allowedUDPPorts = [ 3702 ];
|
||||
};
|
||||
|
||||
services.samba-wsdd = {
|
||||
enable = true;
|
||||
discovery = true;
|
||||
workgroup = "WORKGROUP";
|
||||
};
|
||||
|
||||
services.samba = {
|
||||
enable = true;
|
||||
openFirewall = true;
|
||||
|
||||
extraConfig = ''
|
||||
browseable = ${bool-to-yes-no cfg.browseable}
|
||||
'';
|
||||
|
||||
shares = mapAttrs
|
||||
(name: value: {
|
||||
inherit (value) path comment;
|
||||
|
||||
public = bool-to-yes-no value.public;
|
||||
browseable = bool-to-yes-no value.browseable;
|
||||
"read only" = bool-to-yes-no value.read-only;
|
||||
} // (optionalAttrs value.only-owner-editable {
|
||||
"write list" = config.plusultra.user.name;
|
||||
"read list" = "guest, nobody";
|
||||
"create mask" = "0755";
|
||||
}) // value.extra-config)
|
||||
cfg.shares;
|
||||
};
|
||||
};
|
||||
}
|
69
modules/nixos/services/tailscale/default.nix
Normal file
69
modules/nixos/services/tailscale/default.nix
Normal file
|
@ -0,0 +1,69 @@
|
|||
{ lib, pkgs, config, ... }:
|
||||
|
||||
with lib;
|
||||
with lib.plusultra;
|
||||
let cfg = config.plusultra.services.tailscale;
|
||||
in
|
||||
{
|
||||
options.plusultra.services.tailscale = with types; {
|
||||
enable = mkBoolOpt false "Whether or not to configure Tailscale";
|
||||
autoconnect = {
|
||||
enable = mkBoolOpt false "Whether or not to enable automatic connection to Tailscale";
|
||||
key = mkOpt str "" "The authentication key to use";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.autoconnect.enable -> cfg.autoconnect.key != "";
|
||||
message = "plusultra.services.tailscale.autoconnect.key must be set";
|
||||
}
|
||||
];
|
||||
|
||||
environment.systemPackages = with pkgs; [ tailscale ];
|
||||
|
||||
services.tailscale = enabled;
|
||||
|
||||
networking = {
|
||||
firewall = {
|
||||
trustedInterfaces = [ config.services.tailscale.interfaceName ];
|
||||
|
||||
allowedUDPPorts = [ config.services.tailscale.port ];
|
||||
|
||||
# Strict reverse path filtering breaks Tailscale exit node use and some subnet routing setups.
|
||||
checkReversePath = "loose";
|
||||
};
|
||||
|
||||
networkmanager.unmanaged = [ "tailscale0" ];
|
||||
};
|
||||
|
||||
systemd.services.tailscale-autoconnect = mkIf cfg.autoconnect.enable {
|
||||
description = "Automatic connection to Tailscale";
|
||||
|
||||
# Make sure tailscale is running before trying to connect to tailscale
|
||||
after = [ "network-pre.target" "tailscale.service" ];
|
||||
wants = [ "network-pre.target" "tailscale.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
# Set this service as a oneshot job
|
||||
serviceConfig.Type = "oneshot";
|
||||
|
||||
# Have the job run this shell script
|
||||
script = with pkgs; ''
|
||||
# Wait for tailscaled to settle
|
||||
sleep 2
|
||||
|
||||
# Check if we are already authenticated to tailscale
|
||||
status="$(${tailscale}/bin/tailscale status -json | ${jq}/bin/jq -r .BackendState)"
|
||||
if [ $status = "Running" ]; then # if so, then do nothing
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Otherwise authenticate with tailscale
|
||||
${tailscale}/bin/tailscale up -authkey "${cfg.autoconnect.key}"
|
||||
'';
|
||||
|
||||
};
|
||||
};
|
||||
}
|
174
modules/nixos/services/vault-agent/default.nix
Normal file
174
modules/nixos/services/vault-agent/default.nix
Normal file
|
@ -0,0 +1,174 @@
|
|||
{ lib, config, pkgs, inputs, ... }:
|
||||
|
||||
with lib;
|
||||
with lib.plusultra;
|
||||
let
|
||||
cfg = config.plusultra.services.vault-agent;
|
||||
|
||||
# nixos-vault-service places generated files here:
|
||||
# https://github.com/DeterminateSystems/nixos-vault-service/blob/45e65627dff5dc4bb40d0f2595916f37e78959c1/module/helpers.nix#L4
|
||||
secret-files-root = "/tmp/detsys-vault";
|
||||
environment-files-root = "/run/keys/environment";
|
||||
|
||||
create-environment-files-submodule = service-name: types.submodule ({ name, ... }: {
|
||||
options = {
|
||||
text = mkOpt (types.nullOr types.str) null "An inline template for Vault to template.";
|
||||
source = mkOpt (types.nullOr types.path) null "The file with environment variables for Vault to template.";
|
||||
path = mkOption {
|
||||
readOnly = true;
|
||||
type = types.str;
|
||||
description = "The path to the environment file.";
|
||||
default = "${environment-files-root}/${service-name}/${name}.EnvFile";
|
||||
defaultText = "${environment-files-root}/<service-name>/<template-name>.EnvFile";
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
secret-files-submodule = types.submodule ({ name, ... }: {
|
||||
options = {
|
||||
text = mkOpt (types.nullOr types.str) null "An inline template for Vault to template.";
|
||||
source = mkOpt (types.nullOr types.path) null "The file for Vault to template.";
|
||||
permissions = mkOpt types.str "0400" "The octal mode of this file.";
|
||||
change-action = mkOpt (types.nullOr (types.enum [ "restart" "stop" "none" ])) null "The action to take when secrets change.";
|
||||
path = mkOption {
|
||||
readOnly = true;
|
||||
type = types.str;
|
||||
description = "The path to the secret file.";
|
||||
default = "${secret-files-root}/${name}";
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
services-submodule =
|
||||
types.submodule
|
||||
({ name, config, ... }: {
|
||||
options = {
|
||||
enable = mkBoolOpt true "Whether to enable Vault Agent for this service.";
|
||||
settings = mkOpt types.attrs { } "Vault Agent configuration.";
|
||||
secrets = {
|
||||
environment = {
|
||||
force = mkOpt types.bool false "Whether or not to force the use of Vault Agent's environment files.";
|
||||
change-action = mkOpt (types.enum [ "restart" "stop" "none" ]) "restart" "The action to take when secrets change.";
|
||||
templates = mkOpt (types.attrsOf (create-environment-files-submodule name)) { } "Environment variable files for Vault to template.";
|
||||
template = mkOpt (types.nullOr (types.either types.path types.str)) null "An environment variable template.";
|
||||
paths = mkOption {
|
||||
readOnly = true;
|
||||
type = types.listOf types.str;
|
||||
description = "Paths to all of the environment files";
|
||||
default =
|
||||
if config.secrets.environment.template != null then
|
||||
[ "${environment-files-root}/${name}/EnvFile" ]
|
||||
else
|
||||
(
|
||||
mapAttrsToList
|
||||
(template-name: value: value.path)
|
||||
config.secrets.environment.templates
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
file = {
|
||||
change-action = mkOpt (types.enum [ "restart" "stop" "none" ]) "restart" "The action to take when secrets change.";
|
||||
files = mkOption {
|
||||
description = "Secret files to template.";
|
||||
default = { };
|
||||
type = types.attrsOf secret-files-submodule;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
});
|
||||
in
|
||||
{
|
||||
# imports = [
|
||||
# inputs.vault-service.nixosModules.nixos-vault-service
|
||||
# ];
|
||||
|
||||
options.plusultra.services.vault-agent = {
|
||||
enable = mkEnableOption "Vault Agent";
|
||||
|
||||
settings = mkOpt types.attrs { } "Default Vault Agent configuration.";
|
||||
|
||||
services = mkOption {
|
||||
description = "Services to install Vault Agent into.";
|
||||
default = { };
|
||||
type = types.attrsOf services-submodule;
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
assertions = flatten (mapAttrsToList
|
||||
(service-name: service:
|
||||
(mapAttrsToList
|
||||
(template-name: template:
|
||||
{
|
||||
assertion = (template.source != null && template.text == null) || (template.source == null && template.text != null);
|
||||
message = "plusultra.services.vault-agent.services.${service-name}.secrets.environment.templates.${template-name} must set either `source` or `text`.";
|
||||
}
|
||||
)
|
||||
service.secrets.environment.templates)
|
||||
++
|
||||
(mapAttrsToList
|
||||
(file-name: file:
|
||||
{
|
||||
assertion = (file.source != null && file.text == null) || (file.source == null && file.text != null);
|
||||
message = "plusultra.services.vault-agent.services.${service-name}.secrets.file.files.${file-name} must set either `source` or `text`.";
|
||||
}
|
||||
)
|
||||
service.secrets.file.files)
|
||||
)
|
||||
cfg.services);
|
||||
|
||||
systemd.services = mapAttrs
|
||||
(service-name: value: mkIf value.secrets.environment.force {
|
||||
serviceConfig.EnvironmentFile = mkForce value.secrets.environment.paths;
|
||||
})
|
||||
cfg.services;
|
||||
|
||||
detsys.vaultAgent = {
|
||||
defaultAgentConfig = cfg.settings;
|
||||
|
||||
systemd.services = mapAttrs
|
||||
(service-name: value: {
|
||||
inherit (value) enable;
|
||||
|
||||
agentConfig = value.settings;
|
||||
|
||||
environment = {
|
||||
changeAction = value.secrets.environment.change-action;
|
||||
|
||||
templateFiles = mapAttrs
|
||||
(template-name: value: {
|
||||
file =
|
||||
if value.source != null then
|
||||
value.source
|
||||
else
|
||||
pkgs.writeText "${service-name}-${template-name}-env-template" value.text;
|
||||
})
|
||||
value.secrets.environment.templates;
|
||||
|
||||
template =
|
||||
if (builtins.isPath value.secrets.environment.template) || (builtins.isNull value.secrets.environment.template) then
|
||||
value.secrets.environment.template
|
||||
else
|
||||
pkgs.writeText "${service-name}-env-template" value.secrets.environment.template;
|
||||
};
|
||||
|
||||
secretFiles = {
|
||||
defaultChangeAction = value.secrets.file.change-action;
|
||||
|
||||
files = mapAttrs
|
||||
(file-name: value: {
|
||||
changeAction = value.change-action;
|
||||
template = value.text;
|
||||
templateFile = value.source;
|
||||
perms = value.permissions;
|
||||
})
|
||||
value.secrets.file.files;
|
||||
};
|
||||
})
|
||||
cfg.services;
|
||||
};
|
||||
};
|
||||
}
|
202
modules/nixos/services/vault/default.nix
Normal file
202
modules/nixos/services/vault/default.nix
Normal file
|
@ -0,0 +1,202 @@
|
|||
{ lib, config, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
with lib.plusultra;
|
||||
let
|
||||
cfg = config.plusultra.services.vault;
|
||||
|
||||
package = if cfg.ui then pkgs.vault-bin else pkgs.vault;
|
||||
|
||||
has-policies = (builtins.length (builtins.attrNames cfg.policies)) != 0;
|
||||
|
||||
format-policy = name: file: pkgs.runCommandNoCC
|
||||
"formatted-vault-policy"
|
||||
{
|
||||
inherit file;
|
||||
buildInputs = [ package ];
|
||||
}
|
||||
''
|
||||
name="$(basename "$file")"
|
||||
|
||||
cp "$file" "./$name"
|
||||
|
||||
# Ensure that vault can overwrite the file.
|
||||
chmod +w "./$name"
|
||||
|
||||
# Create this variable here to avoid swallowing vault's exit code.
|
||||
vault_output=
|
||||
|
||||
set +e
|
||||
vault_output=$(vault policy fmt "./$name" 2>&1)
|
||||
vault_status=$?
|
||||
set -e
|
||||
|
||||
if [ "$vault_status" != 0 ]; then
|
||||
echo 'Error formatting policy "${name}"'
|
||||
echo "This is normally caused by a syntax error in the policy file."
|
||||
echo "$file"
|
||||
echo ""
|
||||
echo "Vault Output:"
|
||||
echo "$vault_output"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mv "./$name" $out
|
||||
'';
|
||||
|
||||
policies = mapAttrs
|
||||
(name: value:
|
||||
if builtins.isPath value then
|
||||
format-policy name value
|
||||
else
|
||||
format-policy name (pkgs.writeText "${name}.hcl" value)
|
||||
)
|
||||
cfg.policies;
|
||||
in
|
||||
{
|
||||
options.plusultra.services.vault = {
|
||||
enable = mkEnableOption "Vault";
|
||||
|
||||
ui = mkBoolOpt true "Whether the UI should be enabled.";
|
||||
|
||||
storage = {
|
||||
backend = mkOpt types.str "file" "The storage backend for Vault.";
|
||||
};
|
||||
|
||||
settings = mkOpt types.str "" "Configuration for Vault's config file.";
|
||||
|
||||
mutable-policies = mkBoolOpt false "Whether policies not specified in Nix should be removed.";
|
||||
|
||||
policies = mkOpt (types.attrsOf (types.either types.str types.path)) { } "Policies to install when Vault runs.";
|
||||
|
||||
policy-agent = {
|
||||
user = mkOpt types.str "vault" "The user to run the Vault Agent as.";
|
||||
group = mkOpt types.str "vault" "The group to run the Vault Agent as.";
|
||||
|
||||
auth = {
|
||||
roleIdFilePath = mkOpt types.str "/var/lib/vault/role-id" "The file to read the role-id from.";
|
||||
secretIdFilePath = mkOpt types.str "/var/lib/vault/secret-id" "The file to read the secret-id from.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.vault = {
|
||||
enable = true;
|
||||
inherit package;
|
||||
|
||||
extraConfig = ''
|
||||
ui = ${if cfg.ui then "true" else "false"}
|
||||
|
||||
${cfg.settings}
|
||||
'';
|
||||
|
||||
};
|
||||
|
||||
systemd.services.vault = { };
|
||||
|
||||
systemd.services.vault-policies = mkIf (has-policies || !cfg.mutable-policies) {
|
||||
wantedBy = [ "vault.service" ];
|
||||
after = [ "vault.service" ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = cfg.policy-agent.user;
|
||||
Group = cfg.policy-agent.group;
|
||||
Restart = "on-failure";
|
||||
RestartSec = 30;
|
||||
RemainAfterExit = "yes";
|
||||
};
|
||||
|
||||
restartTriggers = (mapAttrsToList (name: value: "${name}=${value}") policies);
|
||||
|
||||
path = [
|
||||
package
|
||||
pkgs.curl
|
||||
pkgs.jq
|
||||
];
|
||||
|
||||
environment = {
|
||||
VAULT_ADDR = "http://${config.services.vault.address}";
|
||||
};
|
||||
|
||||
script =
|
||||
let
|
||||
write-policies-commands = mapAttrsToList
|
||||
(name: policy:
|
||||
''
|
||||
echo Writing policy '${name}': '${policy}'
|
||||
vault policy write '${name}' '${policy}'
|
||||
''
|
||||
)
|
||||
policies;
|
||||
write-policies = concatStringsSep "\n" write-policies-commands;
|
||||
|
||||
known-policies = mapAttrsToList (name: value: name) policies;
|
||||
|
||||
remove-unknown-policies = ''
|
||||
current_policies=$(vault policy list -format=json | jq -r '.[]')
|
||||
known_policies=(${concatStringsSep " " (builtins.map (policy: "\"${policy}\"") known-policies)})
|
||||
|
||||
while read current_policy; do
|
||||
is_known=false
|
||||
|
||||
for known_policy in "''${known_policies[@]}"; do
|
||||
if [ "$known_policy" = "$current_policy" ]; then
|
||||
is_known=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$is_known" = "false" ] && [ "$current_policy" != "default" ] && [ "$current_policy" != "root" ]; then
|
||||
echo "Removing policy: $current_policy"
|
||||
vault policy delete "$current_policy"
|
||||
else
|
||||
echo "Keeping policy: $current_policy"
|
||||
fi
|
||||
done <<< "$current_policies"
|
||||
'';
|
||||
in
|
||||
''
|
||||
if ! [ -f '${cfg.policy-agent.auth.roleIdFilePath}' ]; then
|
||||
echo 'role-id file not found: ${cfg.policy-agent.auth.roleIdFilePath}'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [ -f '${cfg.policy-agent.auth.secretIdFilePath}' ]; then
|
||||
echo 'secret-id file not found: ${cfg.policy-agent.auth.secretIdFilePath}'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
role_id="$(cat '${cfg.policy-agent.auth.roleIdFilePath}')"
|
||||
secret_id="$(cat '${cfg.policy-agent.auth.secretIdFilePath}')"
|
||||
|
||||
seal_status=$(curl -s "$VAULT_ADDR/v1/sys/seal-status" | jq ".sealed")
|
||||
|
||||
echo "Seal Status: $seal_status"
|
||||
|
||||
if [ seal_status = "true" ]; then
|
||||
echo "Vault is currently sealed, cannot install policies."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Getting token..."
|
||||
|
||||
token=$(vault write -field=token auth/approle/login \
|
||||
role_id="$role_id" \
|
||||
secret_id="$secret_id" \
|
||||
)
|
||||
|
||||
echo "Logging in..."
|
||||
|
||||
export VAULT_TOKEN="$(vault login -method=token -token-only token="$token")"
|
||||
|
||||
echo "Writing policies..."
|
||||
|
||||
${write-policies}
|
||||
|
||||
${optionalString (!cfg.mutable-policies) remove-unknown-policies}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
493
modules/nixos/services/writefreely/default.nix
Normal file
493
modules/nixos/services/writefreely/default.nix
Normal file
|
@ -0,0 +1,493 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
inherit (builtins) toString;
|
||||
inherit (lib) types mkIf mkOption mkDefault;
|
||||
inherit (lib) optional optionals optionalAttrs optionalString;
|
||||
|
||||
inherit (pkgs) sqlite;
|
||||
|
||||
format = pkgs.formats.ini {
|
||||
mkKeyValue = key: value:
|
||||
let
|
||||
value' =
|
||||
if builtins.isNull value then
|
||||
""
|
||||
else if builtins.isBool value then
|
||||
if value == true then "true" else "false"
|
||||
else
|
||||
toString value;
|
||||
in
|
||||
"${key} = ${value'}";
|
||||
};
|
||||
|
||||
cfg = config.plusultra.services.writefreely;
|
||||
|
||||
isSqlite = cfg.database.type == "sqlite3";
|
||||
isMysql = cfg.database.type == "mysql";
|
||||
isMysqlLocal = isMysql && cfg.database.createLocally == true;
|
||||
|
||||
hostProtocol = if cfg.acme.enable then "https" else "http";
|
||||
|
||||
settings = cfg.settings // {
|
||||
app = cfg.settings.app or { } // {
|
||||
host = cfg.settings.app.host or "${hostProtocol}://${cfg.host}";
|
||||
};
|
||||
|
||||
database =
|
||||
if cfg.database.type == "sqlite3" then {
|
||||
type = "sqlite3";
|
||||
filename = cfg.settings.database.filename or "writefreely.db";
|
||||
database = cfg.database.name;
|
||||
} else {
|
||||
type = "mysql";
|
||||
username = cfg.database.user;
|
||||
password = "#dbpass#";
|
||||
database = cfg.database.name;
|
||||
host = cfg.database.host;
|
||||
port = cfg.database.port;
|
||||
tls = cfg.database.tls;
|
||||
};
|
||||
|
||||
server = cfg.settings.server or { } // {
|
||||
bind = cfg.settings.server.bind or "localhost";
|
||||
gopher_port = cfg.settings.server.gopher_port or 0;
|
||||
autocert = !cfg.nginx.enable && cfg.acme.enable;
|
||||
templates_parent_dir =
|
||||
cfg.settings.server.templates_parent_dir or cfg.package.src;
|
||||
static_parent_dir = cfg.settings.server.static_parent_dir or assets;
|
||||
pages_parent_dir =
|
||||
cfg.settings.server.pages_parent_dir or cfg.package.src;
|
||||
keys_parent_dir = cfg.settings.server.keys_parent_dir or cfg.stateDir;
|
||||
};
|
||||
};
|
||||
|
||||
configFile = format.generate "config.ini" settings;
|
||||
|
||||
assets = pkgs.stdenvNoCC.mkDerivation {
|
||||
pname = "writefreely-assets";
|
||||
|
||||
inherit (cfg.package) version src;
|
||||
|
||||
nativeBuildInputs = with pkgs.nodePackages; [ less ];
|
||||
|
||||
buildPhase = ''
|
||||
mkdir -p $out
|
||||
|
||||
cp -r static $out/
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
less_dir=$src/less
|
||||
css_dir=$out/static/css
|
||||
|
||||
lessc $less_dir/app.less $css_dir/write.css
|
||||
lessc $less_dir/fonts.less $css_dir/fonts.css
|
||||
lessc $less_dir/icons.less $css_dir/icons.css
|
||||
lessc $less_dir/prose.less $css_dir/prose.css
|
||||
'';
|
||||
};
|
||||
|
||||
withConfigFile = text: ''
|
||||
db_pass=${
|
||||
optionalString (cfg.database.passwordFile != null)
|
||||
"$(head -n1 ${cfg.database.passwordFile})"
|
||||
}
|
||||
|
||||
cp -f ${configFile} '${cfg.stateDir}/config.ini'
|
||||
sed -e "s,#dbpass#,$db_pass,g" -i '${cfg.stateDir}/config.ini'
|
||||
chmod 440 '${cfg.stateDir}/config.ini'
|
||||
|
||||
${text}
|
||||
'';
|
||||
|
||||
withMysql = text:
|
||||
withConfigFile ''
|
||||
query () {
|
||||
local result=$(${config.services.mysql.package}/bin/mysql \
|
||||
--user=${cfg.database.user} \
|
||||
--password=$db_pass \
|
||||
--database=${cfg.database.name} \
|
||||
--silent \
|
||||
--raw \
|
||||
--skip-column-names \
|
||||
--execute "$1" \
|
||||
)
|
||||
|
||||
echo $result
|
||||
}
|
||||
|
||||
${text}
|
||||
'';
|
||||
|
||||
withSqlite = text:
|
||||
withConfigFile ''
|
||||
query () {
|
||||
local result=$(${sqlite}/bin/sqlite3 \
|
||||
'${cfg.stateDir}/${settings.database.filename}'
|
||||
"$1" \
|
||||
)
|
||||
|
||||
echo $result
|
||||
}
|
||||
|
||||
${text}
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.plusultra.services.writefreely = {
|
||||
enable =
|
||||
lib.mkEnableOption (lib.mdDoc "Writefreely, build a digital writing community");
|
||||
|
||||
package = lib.mkOption {
|
||||
type = lib.types.package;
|
||||
default = pkgs.writefreely;
|
||||
defaultText = lib.literalExpression "pkgs.writefreely";
|
||||
description = lib.mdDoc "Writefreely package to use.";
|
||||
};
|
||||
|
||||
stateDir = mkOption {
|
||||
type = types.path;
|
||||
default = "/var/lib/writefreely";
|
||||
description = lib.mdDoc "The state directory where keys and data are stored.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "writefreely";
|
||||
description = lib.mdDoc "User under which Writefreely is ran.";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "writefreely";
|
||||
description = lib.mdDoc "Group under which Writefreely is ran.";
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = lib.mdDoc "The public host name to serve.";
|
||||
example = "example.com";
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
default = { };
|
||||
description = lib.mdDoc ''
|
||||
Writefreely configuration (`config.ini`). Refer to
|
||||
[writefreely.org/docs/latest/admin/config](https://writefreely.org/docs/latest/admin/config)
|
||||
for details.
|
||||
'';
|
||||
|
||||
type = types.submodule {
|
||||
freeformType = format.type;
|
||||
|
||||
options = {
|
||||
app = {
|
||||
theme = mkOption {
|
||||
type = types.str;
|
||||
default = "write";
|
||||
description = "The theme to apply.";
|
||||
};
|
||||
};
|
||||
|
||||
server = {
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = if cfg.nginx.enable then 18080 else 80;
|
||||
defaultText = "80";
|
||||
description = "The port WriteFreely should listen on.";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
database = {
|
||||
type = mkOption {
|
||||
type = types.enum [ "sqlite3" "mysql" ];
|
||||
default = "sqlite3";
|
||||
description = "The database provider to use.";
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "writefreely";
|
||||
description = "The name of the database to store data in.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = if cfg.database.type == "mysql" then "writefreely" else null;
|
||||
defaultText = "writefreely";
|
||||
description = lib.mdDoc "The database user to connect as.";
|
||||
};
|
||||
|
||||
passwordFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = lib.mdDoc "The file to load the database password from.";
|
||||
};
|
||||
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = lib.mdDoc "The database host to connect to.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 3306;
|
||||
description = lib.mdDoc "The port used when connecting to the database host.";
|
||||
};
|
||||
|
||||
tls = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description =
|
||||
lib.mdDoc "Whether or not TLS should be used for the database connection.";
|
||||
};
|
||||
|
||||
migrate = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description =
|
||||
lib.mdDoc "Whether or not to automatically run migrations on startup.";
|
||||
};
|
||||
|
||||
createLocally = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = lib.mdDoc ''
|
||||
When `plusultra.services.writefreely.database.type` is set to
|
||||
`"mysql"`, this option will enable the MySQL service locally.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
admin = {
|
||||
name = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
description = "The name of the first admin user.";
|
||||
default = null;
|
||||
};
|
||||
|
||||
initialPasswordFile = mkOption {
|
||||
type = types.path;
|
||||
description = ''
|
||||
Path to a file containing the initial password for the admin user.
|
||||
If not provided, the default password will be set to <code>nixos</code>.
|
||||
'';
|
||||
default = pkgs.writeText "default-admin-pass" "nixos";
|
||||
defaultText = "/nix/store/xxx-default-admin-pass";
|
||||
};
|
||||
};
|
||||
|
||||
nginx = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description =
|
||||
"Whether or not to enable and configure nginx as a proxy for WriteFreely.";
|
||||
};
|
||||
|
||||
forceSSL = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether or not to force the use of SSL.";
|
||||
};
|
||||
};
|
||||
|
||||
acme = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description =
|
||||
"Whether or not to automatically fetch and configure SSL certs.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.host != "";
|
||||
message = "plusultra.services.writefreely.host must be set";
|
||||
}
|
||||
{
|
||||
assertion = isMysqlLocal -> cfg.database.passwordFile != null;
|
||||
message =
|
||||
"plusultra.services.writefreely.database.passwordFile must be set if plusultra.services.writefreely.database.createLocally is set to true";
|
||||
}
|
||||
{
|
||||
assertion = isSqlite -> !cfg.database.createLocally;
|
||||
message =
|
||||
"plusultra.services.writefreely.database.createLocally has no use when plusultra.services.writefreely.database.type is set to sqlite3";
|
||||
}
|
||||
];
|
||||
|
||||
users = {
|
||||
users = optionalAttrs (cfg.user == "writefreely") {
|
||||
writefreely = {
|
||||
group = cfg.group;
|
||||
home = cfg.stateDir;
|
||||
isSystemUser = true;
|
||||
};
|
||||
};
|
||||
|
||||
groups =
|
||||
optionalAttrs (cfg.group == "writefreely") { writefreely = { }; };
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules =
|
||||
[ "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" ];
|
||||
|
||||
systemd.services.writefreely = {
|
||||
after = [ "network.target" ]
|
||||
++ optional isSqlite "writefreely-sqlite-init.service"
|
||||
++ optional isMysql "writefreely-mysql-init.service"
|
||||
++ optional isMysqlLocal "mysql.service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
WorkingDirectory = cfg.stateDir;
|
||||
Restart = "always";
|
||||
RestartSec = 20;
|
||||
ExecStart =
|
||||
"${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' serve";
|
||||
AmbientCapabilities =
|
||||
optionalString (settings.server.port < 1024) "cap_net_bind_service";
|
||||
};
|
||||
|
||||
preStart = ''
|
||||
if ! test -d "${cfg.stateDir}/keys"; then
|
||||
mkdir -p ${cfg.stateDir}/keys
|
||||
|
||||
# Key files end up with the wrong permissions by default.
|
||||
# We need to correct them so that Writefreely can read them.
|
||||
chmod -R 750 "${cfg.stateDir}/keys"
|
||||
|
||||
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' keys generate
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.writefreely-sqlite-init = mkIf isSqlite {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
WorkingDirectory = cfg.stateDir;
|
||||
ReadOnlyPaths = optional (cfg.admin.initialPasswordFile != null)
|
||||
cfg.admin.initialPasswordFile;
|
||||
};
|
||||
|
||||
script =
|
||||
let
|
||||
migrateDatabase = optionalString cfg.database.migrate ''
|
||||
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db migrate
|
||||
'';
|
||||
|
||||
createAdmin = optionalString (cfg.admin.name != null) ''
|
||||
if [[ $(query "SELECT COUNT(*) FROM users") == 0 ]]; then
|
||||
admin_pass=$(head -n1 ${cfg.admin.initialPasswordFile})
|
||||
|
||||
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' --create-admin ${cfg.admin.name}:$admin_pass
|
||||
fi
|
||||
'';
|
||||
in
|
||||
withSqlite ''
|
||||
if ! test -f '${settings.database.filename}'; then
|
||||
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db init
|
||||
fi
|
||||
|
||||
${migrateDatabase}
|
||||
|
||||
${createAdmin}
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.writefreely-mysql-init = mkIf isMysql {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = optional isMysqlLocal "mysql.service";
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
WorkingDirectory = cfg.stateDir;
|
||||
ReadOnlyPaths = optional isMysqlLocal cfg.database.passwordFile
|
||||
++ optional (cfg.admin.initialPasswordFile != null)
|
||||
cfg.admin.initialPasswordFile;
|
||||
};
|
||||
|
||||
script =
|
||||
let
|
||||
updateUser = optionalString isMysqlLocal ''
|
||||
# WriteFreely currently *requires* a password for authentication, so we
|
||||
# need to update the user in MySQL accordingly. By default MySQL users
|
||||
# authenticate with auth_socket or unix_socket.
|
||||
# See: https://github.com/writefreely/writefreely/issues/568
|
||||
${config.services.mysql.package}/bin/mysql --skip-column-names --execute "ALTER USER '${cfg.database.user}'@'localhost' IDENTIFIED VIA unix_socket OR mysql_native_password USING PASSWORD('$db_pass'); FLUSH PRIVILEGES;"
|
||||
'';
|
||||
|
||||
migrateDatabase = optionalString cfg.database.migrate ''
|
||||
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db migrate
|
||||
'';
|
||||
|
||||
createAdmin = optionalString (cfg.admin.name != null) ''
|
||||
if [[ $(query 'SELECT COUNT(*) FROM users') == 0 ]]; then
|
||||
admin_pass=$(head -n1 ${cfg.admin.initialPasswordFile})
|
||||
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' --create-admin ${cfg.admin.name}:$admin_pass
|
||||
fi
|
||||
'';
|
||||
in
|
||||
withMysql ''
|
||||
${updateUser}
|
||||
|
||||
if [[ $(query "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '${cfg.database.name}'") == 0 ]]; then
|
||||
${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db init
|
||||
fi
|
||||
|
||||
${migrateDatabase}
|
||||
|
||||
${createAdmin}
|
||||
'';
|
||||
};
|
||||
|
||||
services.mysql = mkIf isMysqlLocal {
|
||||
enable = true;
|
||||
package = mkDefault pkgs.mariadb;
|
||||
ensureDatabases = [ cfg.database.name ];
|
||||
ensureUsers = [{
|
||||
name = cfg.database.user;
|
||||
ensurePermissions = {
|
||||
"${cfg.database.name}.*" = "ALL PRIVILEGES";
|
||||
# WriteFreely requires the use of passwords, so we need permissions
|
||||
# to `ALTER` the user to add password support and also to reload
|
||||
# permissions so they can be used.
|
||||
"*.*" = "CREATE USER, RELOAD";
|
||||
};
|
||||
}];
|
||||
};
|
||||
|
||||
services.nginx = lib.mkIf cfg.nginx.enable {
|
||||
enable = true;
|
||||
recommendedProxySettings = true;
|
||||
|
||||
virtualHosts."${cfg.host}" = {
|
||||
enableACME = cfg.acme.enable;
|
||||
forceSSL = cfg.nginx.forceSSL;
|
||||
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:${toString settings.server.port}";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue