Files
Morgan Jones 2a28ddd27e nixos/immichframe: add more systemd hardening
Based on Jellyfin but a little stricter because it doesn't do as much.
2026-07-14 20:32:51 -07:00

177 lines
5.9 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.immichframe;
format = pkgs.formats.json { };
inherit (lib)
types
mkIf
mkOption
mkEnableOption
;
in
{
options.services.immichframe = {
enable = mkEnableOption "ImmichFrame";
package = lib.mkPackageOption pkgs "immichframe" { };
port = mkOption {
type = types.port;
default = 3000;
description = "The port that ImmichFrame will listen on.";
};
settings = mkOption {
type = types.submodule {
freeformType = format.type;
options = {
Accounts = mkOption {
type = types.listOf (
types.submodule {
freeformType = format.type;
options = {
ImmichServerUrl = mkOption {
type = types.str;
example = "http://photos.example.com";
description = "The URL of your Immich server.";
};
ApiKey = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
API key to talk to the Immich server.
Warning: it will be world-readable in /nix/store.
Consider using {option}`ApiKeyFile` instead.
See
<https://immichframe.online/docs/getting-started/configuration#api-key-permissions>
for details on what permissions this key needs.
'';
};
ApiKeyFile = mkOption {
type = types.nullOr types.externalPath;
default = null;
description = ''
File containing an API key to talk to the Immich server.
See
<https://immichframe.online/docs/getting-started/configuration#api-key-permissions>
for details on what permissions this key needs.
'';
};
};
}
);
description = ''
Accounts configuration, multiple are permitted. See
<https://immichframe.online/docs/getting-started/configuration>.
'';
};
};
};
default = { };
description = ''
Configuration for ImmichFrame. See
<https://immichframe.online/docs/getting-started/configuration> for
options and defaults.
'';
};
};
config = mkIf cfg.enable {
assertions = lib.imap0 (i: account: {
assertion = lib.xor (account.ApiKey == null) (account.ApiKeyFile == null);
message = "Exactly one of {option}`services.immichframe.settings.Accounts[${toString i}].ApiKey` and {option}`services.immichframe.settings.Accounts[${toString i}].ApiKeyFile` must be specified";
}) cfg.settings.Accounts;
systemd.services.immichframe =
let
accountsWithApiKeyFiles = lib.filter (account: account.ApiKeyFile != null) cfg.settings.Accounts;
apiKeyFileToId = lib.listToAttrs (
lib.imap0 (
index: account: lib.nameValuePair account.ApiKeyFile "api-key-${toString index}"
) accountsWithApiKeyFiles
);
settingsPatchWithCredentialPaths = {
Accounts = map (
account:
account
// (
if account.ApiKeyFile != null then
{
ApiKeyFile = "/run/credentials/${config.systemd.services.immichframe.name}/${
apiKeyFileToId.${account.ApiKeyFile}
}";
}
else
{ }
)
) cfg.settings.Accounts;
};
settingsWithFixedSecretPaths = lib.recursiveUpdate cfg.settings settingsPatchWithCredentialPaths;
in
{
description = "Display your photos from Immich as a digital photo frame";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
IMMICHFRAME_CONFIG_PATH = pkgs.runCommand "Config" { } ''
mkdir $out
ln -s ${format.generate "Settings.json" settingsWithFixedSecretPaths} $out/Settings.json
'';
};
serviceConfig = {
ExecStart = "${lib.getExe cfg.package} --urls=http://localhost:${toString cfg.port}";
LoadCredential = lib.concatMapAttrsStringSep ":" (
apiKeyFile: id: "${id}:${apiKeyFile}"
) apiKeyFileToId;
DynamicUser = true;
Type = "simple";
Restart = "on-failure";
RestartSec = 3;
# systemd hardening, based on jellyfin (also .NET) but stricter
CapabilityBoundingSet = [ "" ];
DevicePolicy = "closed";
LockPersonality = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = !config.boot.isContainer;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = !config.boot.isContainer;
ProtectKernelTunables = !config.boot.isContainer;
ProtectProc = "invisible";
ProtectSystem = "strict";
# no AF_NETLINK here since there's no network connection monitoring
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = !config.boot.isContainer;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
UMask = "0077";
};
};
};
meta.maintainers = with lib.maintainers; [ jfly ];
}