feat: added sftpgo to puffer

This commit is contained in:
alina 🌸 2024-01-09 08:34:03 +03:00
parent 519ab56711
commit 0a343d7825
7 changed files with 422 additions and 44 deletions

View file

@ -1,16 +1,23 @@
{ abs, lib, pkgs, ... }@inputs:
{ abs, lib, config, pkgs, ... }@inputs:
let
containers = (import (abs "lib/containers.nix") inputs);
avahi = (import (abs "lib/avahi.nix") inputs);
systemd = (import (abs "lib/systemd.nix") inputs);
in
containers.mkNixosContainer {
name = "puffer";
ip = "10.42.0.5";
private = false;
containers = import (abs "lib/containers.nix") inputs;
avahi = import (abs "lib/avahi.nix") inputs;
systemd = import (abs "lib/systemd.nix") inputs;
sftpgo = import (abs "services/sftpgo.nix") inputs;
secrets = import (abs "lib/secrets.nix");
sftpKey = secrets.mount config "sftpgo-ed25519";
sambaConfig = {
imports = [
(systemd.mkOneshot {
name = "smb-guest-setup";
# for whatever reason smbd refuses to write unless we set the password
script = "${pkgs.samba}/bin/smbpasswd -a smb-guest -n";
})
];
config = { ... }: {
services.samba = {
enable = true;
openFirewall = true;
@ -81,49 +88,90 @@ containers.mkNixosContainer {
};
};
};
};
imports = [
(systemd.mkOneshot {
name = "smb-guest-setup";
# for whatever reason smbd refuses to write unless we set the password
script = "${pkgs.samba}/bin/smbpasswd -a smb-guest -n";
})
(avahi.setup {
name = "puffer";
services = [
{ type = "_smb._tcp"; port = 445; }
# cancer stuff for macs to see this disk as a time machine-compatible disk
[
{ type = "_adisk._tcp"; port = 9; }
{ txt-record = "sys=waMa=0,adVF=0x100"; }
{ txt-record = "dk0=adVN=Puffer TimeMachine,adVF=0x82"; }
]
{ type = "_device-info._tcp"; port = 0; txt-record = "model=TimeCapsule8,119"; }
avahiConfig = avahi.setup {
name = "puffer";
services = [
{ type = "_smb._tcp"; port = 445; }
# cancer stuff for macs to see this disk as a time machine-compatible disk
[
{ type = "_adisk._tcp"; port = 9; }
{ txt-record = "sys=waMa=0,adVF=0x100"; }
{ txt-record = "dk0=adVN=Puffer TimeMachine,adVF=0x82"; }
]
{ type = "_device-info._tcp"; port = 0; txt-record = "model=TimeCapsule8,119"; }
];
};
sftpgoConfig = sftpgo.setup {
package = pkgs.callPackage (abs "packages/sftpgo.nix") {
tags = [ "nogcs" "nos3" "noazblob" "nobolt" "nomysql" "nopgsql" "nometrics" "bundle" ];
};
config = {
sftpd = {
bindings = [
{ port = 22; }
];
})
];
host_keys = [ "id_ed25519" ];
};
};
keys.ed25519 = sftpKey.path;
users.groups.puffer = { };
users.users.smb-guest = {
isNormalUser = true;
description = "Guest account for Samba";
extraGroups = [ "puffer" ];
createHome = false;
shell = pkgs.shadow;
users.guest = {
# argon-hashed 0
password = "$2a$10$IcGdNtx10ycmPRD6lA4c0uNfRXTEchFRzCZEDkngTjzForn6pd0Wa";
};
systemd.tmpfiles.rules = [
"d /mnt/puffer/Public 0755 smb-guest puffer - -"
"d /mnt/puffer/Backups 0755 smb-guest puffer - -"
folders.Public.path = "/mnt/puffer/Public";
usersFolders = [
{ username = "guest"; folder = "Public"; }
];
};
mounts = {
"/mnt/puffer" = {
hostPath = "/mnt/puffer";
isReadOnly = false;
container = containers.mkNixosContainer {
name = "puffer";
ip = "10.42.0.5";
private = false;
config = { ... }: {
imports = [
sambaConfig
avahiConfig
sftpgoConfig
];
users.groups.puffer = { };
users.users.smb-guest = {
isNormalUser = true;
description = "Guest account for Samba";
extraGroups = [ "puffer" ];
createHome = false;
shell = pkgs.shadow;
};
systemd.tmpfiles.rules = [
"d /mnt/puffer/Public 0755 smb-guest puffer - -"
"d /mnt/puffer/Backups 0755 smb-guest puffer - -"
];
networking.firewall.allowedTCPPorts = [ 22 ];
};
mounts = {
"/mnt/puffer" = {
hostPath = "/mnt/puffer";
isReadOnly = false;
};
} // (sftpKey.mounts);
};
in
{
imports = [
(secrets.declare [ "sftpgo-ed25519" ])
container
];
}

View file

@ -15,4 +15,20 @@
defs
);
};
file = config: name: config.age.secrets.${name}.path;
mount = config: name:
let
path = config.age.secrets.${name}.path;
in
{
path = path;
mounts = {
${path} = {
hostPath = path;
isReadOnly = true;
};
};
};
}

54
lib/sqlite.nix Normal file
View file

@ -0,0 +1,54 @@
{ lib, ... }@inputs:
rec {
mkValue = value:
if builtins.isInt value then builtins.toString value
else if builtins.isString value then "'" + (builtins.replaceStrings [ "'" ] [ "''" ] value) + "'"
else if builtins.isAttrs value then
if builtins.hasAttr "_raw" value then "(" + value._raw + ")"
else mkValue (builtins.toJSON value)
else if value == null then "null"
else throw "invalid type: ${builtins.typeOf value} (value ${builtins.toString value})";
mkIdent = value: "\"" + (builtins.replaceStrings [ "\"" ] [ "\"\"" ] value) + "\"";
mkTableName = table: if builtins.isList table then builtins.concatStringsSep "." table else table;
sql = sql: { _raw = sql; };
insert = { into, value, orReplace ? false }:
let
columns = builtins.attrNames value;
values = builtins.map (name: value.${name}) columns;
cmd = if orReplace then "insert or replace" else "insert";
in
"${cmd} into ${mkTableName into} " +
"(${builtins.concatStringsSep ", " (builtins.map mkIdent columns)}) " +
"values (${builtins.concatStringsSep ", " (builtins.map mkValue values)})";
mkWhere = where:
if builtins.isAttrs where then
builtins.concatStringsSep " and " (builtins.map (name: "${name} = ${mkValue where.${name}}") (builtins.attrNames where))
else where;
select =
{ from
, where
, orderBy ? null
, limit ? null
, offset ? null
, columns ? null
}:
let
whereClause = if where != null then " where ${mkWhere where}" else "";
orderByClause = if orderBy != null then " order by ${orderBy}" else "";
limitClause = if limit != null then " limit ${limit}" else "";
offsetClause = if offset != null then " offset ${offset}" else "";
columnsClause = if columns != null then builtins.concatStringsSep ", " (builtins.map mkIdent columns) else "*";
in
"select ${columnsClause} from ${mkTableName from}${whereClause}${orderByClause}${limitClause}${offsetClause}";
script = statements:
builtins.concatStringsSep "\n" (
builtins.map (statement: statement + ";") (lib.lists.flatten statements)
);
}

60
packages/sftpgo.nix Normal file
View file

@ -0,0 +1,60 @@
# based on https://github.com/NixOS/nixpkgs/blob/63c4175cb0fb03cab301d7ba058e4937bec464e2/pkgs/servers/sftpgo/default.nix
# modified to support bundled usage of static files
{ lib
, buildGoModule
, fetchFromGitHub
, installShellFiles
, nixosTests
, tags ? []
}:
buildGoModule rec {
pname = "sftpgo";
version = "2.5.6";
src = fetchFromGitHub {
owner = "drakkan";
repo = "sftpgo";
rev = "refs/tags/v${version}";
hash = "sha256-ea4DbPwi2tcRgmbNsZKKUOVkp6vjRbr679yAP7znNUc=";
};
vendorHash = "sha256-8TBDaDBLy+82BwsaLncDknVIrauF0eop9e2ZhwcLmIs=";
inherit tags;
ldflags = [
"-s"
"-w"
"-X github.com/drakkan/sftpgo/v2/internal/version.commit=${src.rev}"
"-X github.com/drakkan/sftpgo/v2/internal/version.date=1970-01-01T00:00:00Z"
];
nativeBuildInputs = [ installShellFiles ];
doCheck = false;
subPackages = [ "." ];
preBuild = if builtins.any (x: x == "bundle") tags then ''
cp -rf openapi internal/bundle/openapi
cp -rf static internal/bundle/static
cp -rf templates internal/bundle/templates
'' else null;
postInstall = ''
$out/bin/sftpgo gen man
installManPage man/*.1
installShellCompletion --cmd sftpgo \
--bash <($out/bin/sftpgo gen completion bash) \
--zsh <($out/bin/sftpgo gen completion zsh) \
--fish <($out/bin/sftpgo gen completion fish)
shareDirectory="$out/share/sftpgo"
mkdir -p "$shareDirectory"
cp -r ./{openapi,static,templates} "$shareDirectory"
'';
passthru.tests = nixosTests.sftpgo;
}

View file

@ -0,0 +1,5 @@
age-encryption.org/v1
-> ssh-ed25519 sj88Xw 2YLa2A0IPI5OYU1pACLEZmIXse0w3rwthY/0IhUfITE
W3fHzNoxCouKZXeEZpfZTAfIAKIF07rCDvvnNHgF23Y
--- dIuhxlxaabb+kOpULhF3wcj6CaDpjnLCdog9dTnsWyo
%Å0ÕÛE~G öZ^UFR”ji©w/ÖÄCM~þv©«AŠdá>[h´ëA ÁÊÆãÊ¿œV>D>[m?€y£CZ©Ù^yĹ˜TP§“Ó<ÖDL¡Î£âFÞ©³Ld<4C>éÏ j1(¸(êõ7ë-î§ <0A>}“°JQ4¦EÝÍ ›î%'ìªí/

194
services/sftpgo.nix Normal file
View file

@ -0,0 +1,194 @@
{ lib, abs, pkgs, ... }@inputs:
let
sqlite = import (abs "lib/sqlite.nix") inputs;
defaultFilesystem = {
provider = 0;
osconfig = { };
s3config = { };
gcsconfig = { };
azblobconfig = { };
cryptconfig = { };
sftpconfig = { };
httpconfig = { };
};
insertFolder = name: folder: sqlite.insert {
into = "folders";
value = {
name = name;
description = folder.description or "";
path = folder.path;
used_quota_size = "0";
used_quota_files = "0";
last_quota_update = "0";
filesystem = defaultFilesystem;
};
};
insertUser = username: user: sqlite.insert {
into = "users";
orReplace = true;
value = {
username = username;
status = "1";
expiration_date = "0";
description = user.description or "";
password = user.password or "";
public_keys = builtins.toJSON (user.publicKeys or [ ]);
home_dir = user.home or "/tmp/${username}";
uid = "0";
gid = "0";
max_sessions = "0";
quota_size = "0";
quota_files = "0";
last_login = "0";
permissions."/" = [ "*" ];
used_quota_size = "0";
used_quota_files = "0";
last_quota_update = "0";
upload_bandwidth = "0";
download_bandwidth = "0";
filters = {
hooks = {
external_auth_disabled = false;
pre_login_disabled = false;
check_password_disabled = false;
};
totp_config = {
secret = { };
};
};
filesystem = defaultFilesystem;
additional_info = "";
created_at = "0";
updated_at = "0";
email = "";
upload_data_transfer = "0";
download_data_transfer = "0";
total_data_transfer = "0";
used_upload_data_transfer = "0";
used_download_data_transfer = "0";
deleted_at = "0";
first_download = "0";
first_upload = "0";
role_id = null;
last_password_change = "0";
};
};
insertUserFolder = userFolder: sqlite.insert {
into = "users_folders_mapping";
orReplace = true;
value = {
user_id = sqlite.sql (
sqlite.select {
from = "users";
columns = [ "id" ];
where.username = userFolder.username;
}
);
folder_id = sqlite.sql (
sqlite.select {
from = "folders";
columns = [ "id" ];
where.name = userFolder.folder;
}
);
virtual_path = userFolder.path or "/${userFolder.folder}";
quota_size = -1;
quota_files = -1;
};
};
insertAdmin = username: admin: sqlite.insert {
into = "admins";
value = {
username = username;
description = admin.description or "";
password = admin.password;
email = admin.email or "";
status = "1";
permissions = "[\"*\"]";
filters = {
totp_config = { secret = { }; };
preferences = { };
};
additional_info = "";
last_login = "0";
created_at = "0";
updated_at = "0";
role_id = null;
};
};
in
{
setup =
{
# paths to private keys for sftp server
# all required with default config, but can be omitted if you change the config to not require them
# { ecdsa?: string, ed25519?: string, rsa?: string }
keys ? { }
# https://github.com/drakkan/sftpgo/blob/main/docs/full-configuration.md
, config ? null
# username => {
# home?: string, # defaults to /tmp/username
# description?: string,
# password?: string, # argon2 hashed
# publicKeys?: string[]
# permisisons?: Record<string, string>, e.g,
# }
, users ? { }
# name => { description?: string, path: string }
, folders ? { }
# username => {
# password: string, # argon2 hashed
# description?: string,
# email?: string,
# }
, admins ? {
# argon2 hash of password "admin"
admin.password = "$2a$10$7QZqmQNWwfbgIwc5Jskkgea7s8dffkbwPUW30MEShpDpZWxMVrFaa";
}
# array of {
# username: string,
# folder: string, # name of folder
# path?: string # virtual path, e.g. /Folder (defaults to /${folder})
# }
, usersFolders ? null
, package ? pkgs.sftpgo
, sqlitePackage ? pkgs.sqlite
, serviceName ? "sftpgo"
}:
let
configFile = if config != null then pkgs.writeText "sftpgo.json" (builtins.toJSON config) else null;
sqliteStatements = [ ] ++
lib.optionals (users != null) (lib.attrsets.mapAttrsToList insertUser users) ++
lib.optionals (folders != null) (lib.attrsets.mapAttrsToList insertFolder folders) ++
lib.optionals (admins != null) (lib.attrsets.mapAttrsToList insertAdmin admins) ++
lib.optionals (usersFolders != null) (builtins.map insertUserFolder usersFolders);
sqliteScript = pkgs.writeText "sftpgo-init.sql" (sqlite.script sqliteStatements);
in
{
systemd.services.${serviceName} = {
description = "${serviceName} Daemon";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ package sqlitePackage ];
serviceConfig.PrivateTmp = true;
script = ''
sftpgo initprovider
sqlite3 sftpgo.db < ${sqliteScript}
${lib.optionalString (keys ? ecdsa) "cp ${keys.ecdsa} id_ecdsa"}
${lib.optionalString (keys ? ed25519) "cp ${keys.ed25519} id_ed25519"}
${lib.optionalString (keys ? rsa) "cp ${keys.rsa} id_rsa"}
${lib.optionalString (configFile != null) "cp ${configFile} sftpgo.json"}
sftpgo serve
'';
};
};
}

View file

@ -24,6 +24,7 @@
tree
nixpkgs-fmt
htop
jq
inputs.nil.packages.${system}.default
inputs.agenix.packages.${system}.default
];