This commit is contained in:
Shelvacu
2025-07-11 10:39:26 -07:00
committed by Shelvacu on prophecy
parent 9800d6860f
commit 9b4fe4d75d
4 changed files with 116 additions and 7 deletions

View File

@@ -1,3 +1,5 @@
# TODO: what is the right way to make sure dirs are created automatically? What's the right way to initialize client id and client key without interactive?
# see: https://ehwiki.org/wiki/Hentai@Home
{
lib,
config,
@@ -29,6 +31,17 @@ let
cfg.downloadDir
cfg.logDir
];
capabilities = []
++ lib.optional cfg.allowPrivilegedPort "CAP_NET_BIND_SERVICE"
;
credentialsType = types.submodule ({ ... }: {
options.clientId = mkOption {
type = types.ints.unsigned;
};
options.clientKeyPath = mkOption {
type = types.path;
};
});
in
{
options.vacu.hath = {
@@ -38,12 +51,12 @@ in
default = pkgs.hentai-at-home;
};
user = mkOption {
type = types.str;
type = types.passwdEntry types.str;
default = "hath";
readOnly = true;
};
group = mkOption {
type = types.str;
type = types.passwdEntry types.str;
default = "hath";
readOnly = true;
};
@@ -51,7 +64,15 @@ in
type = types.bool;
default = false;
};
allowPrivilegedPort = mkOption {
type = types.bool;
default = if cfg.port == null then true else cfg.port < 1024;
};
credentials = mkOption {
type = types.nullOr credentialsType;
default = null;
description = "The credentials for this client. If null, credentials must be provided to the H@H client manually.";
};
bandwidthMonitor = mkOption {
type = types.bool;
@@ -114,6 +135,13 @@ in
type = types.path;
default = "${cfg.baseDir}/log";
};
clientLoginPath = mkOption {
type = types.path;
default = "${cfg.dataDir}/client_login";
readOnly = true;
description = "File containing the credentials, in the format {client_id}`-`{client_key}";
};
};
config = lib.mkIf cfg.enable {
@@ -126,8 +154,20 @@ in
wantedBy = lib.mkIf cfg.autoStart [ "multi-user.target" ];
description = "Hentai@Home client";
preStart = ''
mkdir -p -- ${lib.escapeShellArgs dirs}
chown ${cfg.user}:${cfg.group} -- ${lib.escapeShellArgs dirs}
set -xeu
all_dirs=(${lib.escapeShellArgs dirs})
for d in "''${all_dirs[@]}"; do
containing_dir="$(dirname -- "$d")"
mkdir -p -- "$containing_dir"
if ! [[ -d "$d" ]]; then
install --owner=${lib.escapeShellArg cfg.user} --group=${lib.escapeShellArg cfg.group} --mode=rwxr-x--- -d -- "$d"
fi
done
${lib.optionalString (cfg.credentials != null) ''
client_id="${toString cfg.clientId}"
client_key="$(cat ${lib.escapeShellArg cfg.clientKeyPath})"
printf '%s-%s' "$client_id" "$client_key" > ${lib.escapeShellArg cfg.clientLoginPath}
''}
'';
script = "exec ${lib.escapeShellArgs fullCommand}";
serviceConfig = {
@@ -142,8 +182,8 @@ in
"-/etc/localtime"
];
BindPaths = dirs;
CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
CapabilityBoundingSet = capabilities;
AmbientCapabilities = capabilities;
DeviceAllow = "";
ProtectSystem = "strict";
@@ -175,6 +215,7 @@ in
"@system-service"
"~pkey_alloc:ENOSPC"
];
UMask = "0027"; # this makes the default permissions u::rwx,g::r-x,o::---
};
};
};

View File

@@ -1,5 +1,6 @@
{
vacuModules,
config,
...
}:
let
@@ -7,11 +8,20 @@ let
in
{
imports = [ vacuModules.hath ];
sops.secrets.hath-client-key = {
owner = config.vacu.hath.user;
restartUnits = [ "hath.service" ];
};
vacu.hath = {
enable = true;
autoStart = true;
flushLogs = true;
allowPrivilegedPort = false;
cacheDir = "/propdata/hath-cache";
credentials = {
clientId = 50751;
clientKeyPath = config.sops.secrets.hath-client-key.path;
};
};
networking.firewall.allowedTCPPorts = [ port ];
}

View File

@@ -0,0 +1,4 @@
{
writers,
}:
writers.writeBashBin "ensure-secrets" { } (builtins.readFile ./ensure-secrets.bash)

View File

@@ -0,0 +1,54 @@
source shellvaculib.bash
exact_args $# 0
# nixosConfigurations.* where sops is used
configs=(
prophecy
fw
liam
triple-dezert
)
declare -i exitCode=0
for configName in "${configs[@]}"; do
nixPath="nixosConfigurations.\"${configName}\".config.sops.secrets"
secretsInfo="$(nix eval ".#.$nixPath" --json)"
declare -a secretsNames
mapfile -d '' secretsNames < <(jq '.|keys[]' --raw-output0 <<<"$secretsInfo")
for name in "${secretsNames[@]}"; do
thisSecretNixPath="${nixPath}.\"${name}\""
jqPath=".\"${name}\""
thisSecretInfo="$(jq "$jqPath" <<<"$secretsInfo")"
format="$(jq '.format' -r <<<"$thisSecretInfo")"
case "$format" in
yaml|json)
# we know how to deal with this
:
;;
*)
svl_throw "dunno what to do with format $format for $thisSecretNixPath"
;;
esac
sopsFile="$(jq '.sopsFile' -r <<<"$thisSecretInfo")"
key="$(jq '.key' -r <<<"$thisSecretInfo")"
sopsCmd=(
nix run .#sops --
decrypt
--input-type "$format"
)
# make sure we can decrypt the file at all
"${sopsCmd[@]}" -- "$sopsFile" >/dev/null
# now try to read the specific key we're interested in
if "${sopsCmd[@]}" --extract "[\"$key\"]" -- "$sopsFile" >/dev/null; then
# success! all is well
:
else
svl_err "${thisSecretNixPath}: sops file $sopsFile does not contain key $key"
exitCode=1
fi
done
done
exit $exitCode