diff --git a/deployments/transfer-sh/default.nix b/deployments/transfer-sh/default.nix new file mode 100644 index 0000000..562aadf --- /dev/null +++ b/deployments/transfer-sh/default.nix @@ -0,0 +1,23 @@ +{ lib, fetchFromGitHub, buildGoModule }: + +buildGoModule rec { + pname = "transfer.sh"; + version = "1.6.1"; + + src = fetchFromGitHub { + owner = "dutchcoders"; + repo = "transfer.sh"; + rev = "v${version}"; + hash = "sha256-V8E6RwzxKB6KeGPer5074e7y6XHn3ZD24PQMwTxw5lQ="; + }; + + vendorHash = "sha256-C8ZfUIGT9HiQQiJ2hk18uwGaQzNCIKp/Jiz6ePZkgDQ="; + + meta = with lib; { + description = "Easy and fast file sharing and pastebin server with access from the command-line"; + homepage = "https://github.com/dutchcoders/transfer.sh"; + changelog = "https://github.com/dutchcoders/transfer.sh/releases"; + license = licenses.mit; + maintainers = with maintainers; [ ecchibitionist ]; + }; +} diff --git a/deployments/transfer-sh/module.nix b/deployments/transfer-sh/module.nix new file mode 100644 index 0000000..021e5d3 --- /dev/null +++ b/deployments/transfer-sh/module.nix @@ -0,0 +1,423 @@ +{ config, lib, pkgs, ... }: +with lib; + +let + cfg = config.services.transfer-sh; + package = pkgs.callPackage ../transfer-sh { }; +in +{ + options = { + services.transfer-sh = { + enable = mkEnableOption "transfer-sh setup"; + + # package = mkPackageOption pkgs "transfer-sh" { }; + + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + description = lib.mdDoc "Environment file as defined in {manpage}`systemd.exec(5)`."; + }; + + user = mkOption { + description = "user to run as"; + default = "transfersh"; + type = types.str; + }; + + group = mkOption { + description = "group to run as"; + default = "transfersh"; + type = types.str; + }; + + provider = mkOption { + description = "which storage provider to use (s3, storj, gdrive or local)"; + default = "local"; + type = types.enum [ "s3" "storj" "gdrive" "local" ]; + }; + + address = mkOption { + description = "address to listen on"; + default = "127.0.0.1"; + type = types.str; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Open the firewall port"; + }; + + LISTENER = mkOption { + description = "port to use for http"; + default = 6080; + type = types.port; + }; + + PROFILE_LISTENER = mkOption { + description = "port to use for profiler"; + default = 6060; + type = types.nullOr types.port; + }; + + FORCE_HTTPS = mkOption { + description = "redirect to https"; + default = false; + type = types.nullOr types.bool; + }; + + TLS_LISTENER = mkOption { + description = "port to use for https"; + default = null; + type = types.nullOr types.port; + }; + + TLS_LISTENER_ONLY = mkOption { + description = "flag to enable tls listener only"; + default = false; + type = types.nullOr types.bool; + }; + + TLS_CERT_FILE = mkOption { + description = "path to tls certificate"; + default = null; + type = types.nullOr types.path; + }; + + TLS_PRIVATE_KEY = mkOption { + description = "path to tls private key"; + default = null; + type = types.nullOr types.path; + }; + + HTTP_AUTH_USER = mkOption { + description = "user for basic http auth on upload"; + default = null; + type = types.nullOr types.str; + }; + + HTTP_AUTH_PASS = mkOption { + description = "pass for basic http auth on upload"; + default = null; + type = types.nullOr types.str; + }; + + HTTP_AUTH_HTPASSWD = mkOption { + description = "htpasswd file path for basic http auth on upload"; + default = null; + type = types.nullOr types.path; + }; + + HTTP_AUTH_IP_WHITELIST = mkOption { + description = "comma separated list of ips allowed to upload without being challenged an http auth"; + default = [ ]; + type = with types; listOf str; + }; + + IP_WHITELIST = mkOption { + description = "comma separated list of ips allowed to connect to the service"; + default = [ ]; + type = with types; listOf str; + }; + + IP_BLACKLIST = mkOption { + description = "comma separated list of ips not allowed to connect to the service"; + default = [ ]; + type = with types; listOf str; + }; + + TEMP_PATH = mkOption { + description = "path to temp folder"; + default = null; + type = types.nullOr types.str; + }; + + WEB_PATH = mkOption { + description = "path to static web files (for development or custom front end)"; + default = null; + type = types.nullOr types.str; + }; + + PROXY_PATH = mkOption { + description = "path prefix when service is run behind a proxy"; + default = null; + type = types.nullOr types.str; + }; + + PROXY_PORT = mkOption { + description = "port of the proxy when the service is run behind a proxy"; + default = null; + type = types.nullOr types.port; + }; + + EMAIL_CONTACT = mkOption { + description = "email contact for the front end"; + default = null; + type = types.nullOr types.str; + }; + + GA_KEY = mkOption { + description = "google analytics key for the front end"; + default = null; + type = types.nullOr types.str; + }; + + USERVOICE_KEY = mkOption { + description = "user voice key for the front end"; + default = null; + type = types.nullOr types.str; + }; + + AWS_ACCESS_KEY = mkOption { + description = "aws access key"; + default = null; + type = types.nullOr types.str; + }; + + AWS_SECRET_KEY = mkOption { + description = "aws access key"; + default = null; + type = types.nullOr types.str; + }; + + BUCKET = mkOption { + description = "aws bucket"; + default = null; + type = types.nullOr types.str; + }; + + S3_ENDPOINT = mkOption { + description = "Custom S3 endpoint."; + default = null; + type = types.nullOr types.str; + }; + + S3_REGION = mkOption { + description = "region of the s3 bucket"; + default = "eu-west-1"; + type = types.nullOr types.str; + }; + + S3_NO_MULTIPART = mkOption { + description = "disables s3 multipart upload"; + default = false; + type = types.nullOr types.bool; + }; + + S3_PATH_STYLE = mkOption { + description = "Forces path style URLs, required for Minio."; + default = false; + type = types.nullOr types.bool; + }; + + STORJ_ACCESS = mkOption { + description = "Access for the project"; + default = null; + type = types.nullOr types.str; + }; + + STORJ_BUCKET = mkOption { + description = "Bucket to use within the project"; + default = null; + type = types.nullOr types.str; + }; + + BASEDIR = mkOption { + description = "path storage for local/gdrive provider"; + default = "${cfg.stateDir}/store"; + type = types.nullOr types.str; + }; + + GDRIVE_CLIENT_JSON_FILEPATH = mkOption { + description = "path to oauth client json config for gdrive provider"; + default = null; + type = types.nullOr types.str; + }; + + GDRIVE_LOCAL_CONFIG_PATH = mkOption { + description = "path to store local transfer.sh config cache for gdrive provider"; + default = null; + type = types.nullOr types.str; + }; + + GDRIVE_CHUNK_SIZE = mkOption { + description = "chunk size for gdrive upload in megabytes, must be lower than available memory (8 MB)"; + default = null; + type = types.nullOr types.str; + }; + + HOSTS = mkOption { + description = "hosts to use for lets encrypt certificates (comma seperated)"; + default = null; + type = types.nullOr types.str; + }; + + LOG = mkOption { + description = "path to log file"; + default = "${cfg.stateDir}/transfer-sh.log"; + type = types.nullOr types.str; + }; + + CORS_DOMAINS = mkOption { + description = "comma separated list of domains for CORS, setting it enable CORS"; + default = null; + type = types.nullOr types.str; + }; + + CLAMAV_HOST = mkOption { + description = "host for clamav feature"; + default = null; + type = types.nullOr types.str; + }; + + PERFORM_CLAMAV_PRESCAN = mkOption { + description = "prescan every upload through clamav feature (clamav-host must be a local clamd unix socket)"; + default = false; + type = types.nullOr types.bool; + }; + + RATE_LIMIT = mkOption { + description = "request per minute"; + default = null; + type = types.nullOr types.str; + }; + + MAX_UPLOAD_SIZE = mkOption { + description = "max upload size in kilobytes"; + default = null; + type = types.nullOr types.str; + }; + + PURGE_DAYS = mkOption { + description = "number of days after the uploads are purged automatically"; + default = "7"; + type = types.nullOr types.str; + }; + + PURGE_INTERVAL = mkOption { + description = "interval in hours to run the automatic purge for (not applicable to S3 and Storj)"; + default = 1; + type = types.nullOr types.int; + }; + + RANDOM_TOKEN_LENGTH = mkOption { + description = "length of the random token for the upload path (double the size for delete path)"; + default = "6"; + type = types.nullOr types.str; + }; + + stateDir = mkOption { + type = types.path; + description = "Variable state directory"; + default = "/var/lib/transfer.sh"; + }; + }; + }; + + config = mkIf cfg.enable + { + users.users = mkIf (cfg.user == "transfersh") { + transfersh = { + description = "transfer-sh service user"; + home = cfg.stateDir; + group = cfg.group; + isSystemUser = true; + }; + }; + + users.groups = mkIf (cfg.group == "transfersh") { transfersh = { }; }; + + systemd.tmpfiles.rules = [ + "d ${cfg.stateDir} 0750 ${cfg.user} ${cfg.group} - -" + "d ${cfg.BASEDIR} 0750 ${cfg.user} ${cfg.group} - -" + ]; + + systemd.services.transfer-sh = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + User = cfg.user; + Group = cfg.group; + ExecStart = "${lib.getExe package} --provider=${cfg.provider}"; + EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile; + }; + + environment = + { + LISTENER = "${cfg.address}:${toString cfg.LISTENER}"; + PROFILE_LISTENER = toString cfg.PROFILE_LISTENER; + HTTP_AUTH_USER = cfg.HTTP_AUTH_USER; + HTTP_AUTH_PASS = cfg.HTTP_AUTH_PASS; + HTTP_AUTH_HTPASSWD = cfg.HTTP_AUTH_HTPASSWD; + HTTP_AUTH_IP_WHITELIST = concatStringsSep "," cfg.HTTP_AUTH_IP_WHITELIST; + IP_WHITELIST = concatStringsSep "," cfg.IP_WHITELIST; + IP_BLACKLIST = concatStringsSep "," cfg.IP_BLACKLIST; + TEMP_PATH = cfg.TEMP_PATH; + WEB_PATH = cfg.WEB_PATH; + PROXY_PATH = cfg.PROXY_PATH; + PROXY_PORT = toString cfg.PROXY_PORT; + EMAIL_CONTACT = cfg.EMAIL_CONTACT; + GA_KEY = cfg.GA_KEY; + USERVOICE_KEY = cfg.USERVOICE_KEY; + HOSTS = cfg.HOSTS; + LOG = cfg.LOG; + CORS_DOMAINS = cfg.CORS_DOMAINS; + CLAMAV_HOST = cfg.CLAMAV_HOST; + PERFORM_CLAMAV_PRESCAN = lib.boolToString cfg.PERFORM_CLAMAV_PRESCAN; + RATE_LIMIT = cfg.RATE_LIMIT; + MAX_UPLOAD_SIZE = cfg.MAX_UPLOAD_SIZE; + PURGE_DAYS = cfg.PURGE_DAYS; + RANDOM_TOKEN_LENGTH = cfg.RANDOM_TOKEN_LENGTH; + BASEDIR = cfg.BASEDIR; + PURGE_INTERVAL = toString cfg.PURGE_INTERVAL; + } // lib.optionalAttrs (cfg.provider == "s3") { + # Options specific to s3 backend + AWS_ACCESS_KEY = cfg.AWS_ACCESS_KEY; + AWS_SECRET_KEY = cfg.AWS_SECRET_KEY; + BUCKET = cfg.BUCKET; + S3_REGION = cfg.S3_REGION; + S3_ENDPOINT = cfg.S3_ENDPOINT; + S3_NO_MULTIPART = lib.boolToString cfg.S3_NO_MULTIPART; + S3_PATH_STYLE = lib.boolToString cfg.S3_PATH_STYLE; + } // lib.optionalAttrs (cfg.provider == "storj") { + # Options specific to storj backend + STORJ_ACCESS = cfg.STORJ_ACCESS; + STORJ_BUCKET = cfg.STORJ_BUCKET; + } // lib.optionalAttrs (cfg.provider == "gdrive") { + # Options specific to google drive backend + GDRIVE_CLIENT_JSON_FILEPATH = cfg.GDRIVE_CLIENT_JSON_FILEPATH; + GDRIVE_LOCAL_CONFIG_PATH = cfg.GDRIVE_LOCAL_CONFIG_PATH; + GDRIVE_CHUNK_SIZE = cfg.GDRIVE_CHUNK_SIZE; + } // lib.optionalAttrs (cfg.TLS_LISTENER != null) { + # TLS specific options + TLS_LISTENER = "${cfg.address}:${toString cfg.TLS_LISTENER}"; + TLS_LISTENER_ONLY = lib.boolToString cfg.TLS_LISTENER_ONLY; + TLS_CERT_FILE = cfg.TLS_CERT_FILE; + TLS_PRIVATE_KEY = cfg.TLS_PRIVATE_KEY; + FORCE_HTTPS = lib.boolToString cfg.FORCE_HTTPS; + }; + }; + + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall + ([ cfg.LISTENER cfg.PROFILE_LISTENER ] ++ optionals (cfg.TLS_LISTENER != null) [ cfg.TLS_LISTENER ]); + + warnings = + let + sensitiveVars = [ + "GA_KEY" + "HTTP_AUTH_PASS" + "USERVOICE_KEY" + "AWS_SECRET_KEY" + "STORJ_ACCESS" + ]; + in + + lib.lists.forEach (filter (i: cfg."${i}" != null) sensitiveVars) (x: + '' + config.services.transfer-sh.${x} will be stored as plaintext in the Nix store. + Use services.transfer-sh.environmentFile instead to prevent this. + '' + ); + }; + meta.maintainers = with lib.maintainers; [ pinpox ]; +} diff --git a/hosts/kinda.sus.lol/secrets.nix b/hosts/kinda.sus.lol/secrets.nix index 6fde052..d864460 100644 --- a/hosts/kinda.sus.lol/secrets.nix +++ b/hosts/kinda.sus.lol/secrets.nix @@ -19,6 +19,11 @@ owner = config.users.users.nginx.name; group = config.users.users.nginx.group; }; + sops.secrets."services/nginx/transfersh.htpasswd" = { + mode = "0400"; + owner = config.users.users.transfersh.name; + group = config.users.users.transfersh.group; + }; # HedgeDoc sops.secrets."services/hedgedoc/.env" = { diff --git a/hosts/kinda.sus.lol/secrets/nginx.yaml b/hosts/kinda.sus.lol/secrets/nginx.yaml index 117b11f..a0f9247 100644 --- a/hosts/kinda.sus.lol/secrets/nginx.yaml +++ b/hosts/kinda.sus.lol/secrets/nginx.yaml @@ -3,6 +3,7 @@ services: admin.htpasswd: ENC[AES256_GCM,data:SYy91gzsVPwca7QHsAFnDV7e9hLoqS1+xeFyLNTa7WwFwT6sbvboMEnZUQ==,iv:RX8+6Ivx0ibZvoMlaxIGzJ1/OzMgOHu94J/lsvF5UqY=,tag:LtBBAlmRI0jskINGR7Gw/Q==,type:str] ecchi.htpasswd: ENC[AES256_GCM,data:w6VYz0uQun4QiSmpqjwVLDRseVND0pHNzFxlD9F/0j7YqeHTo8gl1AI2cQ==,iv:7KKyUyoVtvIiZuQTmtKzWjZwr7heVX2K2C/WRSOPh0A=,tag:iOdURKQGTh+wt4PcEXCGUg==,type:str] music.htpasswd: ENC[AES256_GCM,data:kYY/QtHZAjC3d8nn41R5NkVj529oGZdnMcqH0S4GW26HUzQ/yYlKELzCxoHRXq4nqoU+gGdjDsRGnzIiKTn629/MzfwpLwD+objiPFzpnvasD6eEHRKE2w==,iv:TKD8Rbv8XcNJFdrQ9YlruuKGvdXyHOenkAW0B7eytKQ=,tag:CmhQR+u7uvZV1go/YOKR4A==,type:str] + transfersh.htpasswd: ENC[AES256_GCM,data:tC4o0/0u2z5vs9FVRBuZrPKujjKXBp/6Ra9g1rnRTvBtM7GUWCUcRItE7Q==,iv:/CLfX+WWahfCCZhHdxIvTUsnTyCymM8pbzkjnVliU/8=,tag:BXHjddJATTeXbnG79du8SA==,type:str] sops: kms: [] gcp_kms: [] @@ -27,8 +28,8 @@ sops: a1d3ekVWMDV4dUxrSGNod2JvYmtHMmMKnBaqvtBd53Jz9CtkOeEJ93YBeGA8pmof VlSrnXcJmZ3tG1GwVOu8Q9Xr5gXrvaG4HGvETLsGBafxVtMTU4v8KQ== -----END AGE ENCRYPTED FILE----- - lastmodified: "2024-01-05T20:09:42Z" - mac: ENC[AES256_GCM,data:lcZQtLr1TtpoETO13T8n62DZlwhjoZteq9BeoF5LjcjS/ol8YdWgJ/a9XxeG8/403wsgpB2P1tjIsNJ14oB7+ehj3G6x51j0saz/qk0Dv0XZVYHsg5GdubBGMd1dgYYHnSQx3Yu5nHXAW+Dx1uN/vLAjZAe1KtYG6MghyiyeOwE=,iv:W2hXqXzbMlZvkPPpMPEscrAlNTwKvilanWEz7EK4df8=,tag:/7HuZHUdM2II/iFBuOr6hQ==,type:str] + lastmodified: "2024-01-24T20:03:25Z" + mac: ENC[AES256_GCM,data:KoNpLDn791EKRgZ1l6TbBLhHfXTPV0j3Wy+knk/Mc6oW9dTQaN9OsqHCSb4HbXJk4E0Vt2C+Ngwgip5+9xvYuWc1q5z8F91MgY/euhbG1raEAHxLp3c9c+J805dYeim2NqTjWbufLQ12ittn3Rv2lArurFsWoJayfvrTUjXImkU=,iv:RpFUctEZ/yxKLeYMTyPEMShufL1A6BxakBefL4v+3uc=,tag:dZNEvnX4mk8mWYTVyJBPAg==,type:str] pgp: [] unencrypted_suffix: _unencrypted version: 3.8.1 diff --git a/hosts/kinda.sus.lol/services/transfer-sh.nix b/hosts/kinda.sus.lol/services/transfer-sh.nix new file mode 100644 index 0000000..e85d03a --- /dev/null +++ b/hosts/kinda.sus.lol/services/transfer-sh.nix @@ -0,0 +1,17 @@ +{ config, pkgs, ... }: +{ + imports = [ + ../../../deployments/transfer-sh/module.nix + ]; + + services.transfer-sh = { + enable = true; + openFirewall = true; + address = "192.168.99.201"; + #HTTP_AUTH_HTPASSWD = "/run/secrets/services/nginx/transfersh.htpasswd"; + TEMP_PATH = "/mnt/data/transfer-sh/temp"; + BASEDIR = "/mnt/data/transfer-sh/store"; + EMAIL_CONTACT = "abuse@lewd.wtf"; + PURGE_DAYS = "90"; + }; +}