From fd4153b332e077a5bbfc5edd3d36329da229958e Mon Sep 17 00:00:00 2001 From: Chris Mann Date: Sun, 19 Apr 2026 11:33:53 +0200 Subject: [PATCH] Added the starts of a new Nixos Package --- tools/nix/README.md | 3 + tools/nix/default.nix | 59 ++++++ tools/nix/flake.nix | 28 +++ tools/nix/module.nix | 406 +++++++++++++++++++++++++++++++++++++++++ tools/nix/oxicloud.env | 2 + tools/nix/package.nix | 85 +++++++++ 6 files changed, 583 insertions(+) create mode 100644 tools/nix/README.md create mode 100644 tools/nix/default.nix create mode 100644 tools/nix/flake.nix create mode 100644 tools/nix/module.nix create mode 100644 tools/nix/oxicloud.env create mode 100644 tools/nix/package.nix diff --git a/tools/nix/README.md b/tools/nix/README.md new file mode 100644 index 00000000..fb21295a --- /dev/null +++ b/tools/nix/README.md @@ -0,0 +1,3 @@ +## Nixos Implementation of OxiCloud + +This folder sets up a Nixos module of OxiCloud. \ No newline at end of file diff --git a/tools/nix/default.nix b/tools/nix/default.nix new file mode 100644 index 00000000..6b6441dd --- /dev/null +++ b/tools/nix/default.nix @@ -0,0 +1,59 @@ +{ + system ? builtins.currentSystem, + pkgs ? import {}, + withManuals ? false, # building the manuals is expensive +}: let + lib = pkgs.lib; + oxicloud = pkgs.callPackage ./package.nix {}; +in { + shell = pkgs.mkShell { + packages = [ + oxicloud + # Alpine Rust + pkgs.postgresql + pkgs.cargo + pkgs.rustup + pkgs.openssl + pkgs.binutils + pkgs.zlib-ng + pkgs.zstd + pkgs.mpfr + pkgs.cryptopp + pkgs.cacert + pkgs.mkcert + pkgs.certstrap + pkgs.musl + pkgs.isl + pkgs.gcc + pkgs.libgcc + pkgs.jansson + pkgs.libatomic_ops + pkgs.pax-utils + pkgs.gomp + pkgs.libressl + pkgs.mpc + pkgs.pkg-config + pkgs.libpq + pkgs.libpqxx + pkgs.perl + pkgs.gnumake + pkgs.su-exec + # Postgres + pkgs.tzdata + pkgs.keyutils + pkgs.gsasl + pkgs.xz + pkgs.libedit + pkgs.libuuid + pkgs.libxslt + # WOPI + # collabora/code:latest + # Extra (I think) + pkgs.ffmpeg + pkgs.libavif + pkgs.librtprocess + pkgs.pdftk + pkgs.imagemagick + ]; + }; +} diff --git a/tools/nix/flake.nix b/tools/nix/flake.nix new file mode 100644 index 00000000..08d0bb2c --- /dev/null +++ b/tools/nix/flake.nix @@ -0,0 +1,28 @@ +{ + description = "OxiCloud Nix flake with module"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + }; + + outputs = { + self, + nixpkgs, + }: let + systems = ["x86_64-linux"]; + forAllSystems = f: + nixpkgs.lib.genAttrs systems ( + system: + f (import nixpkgs {inherit system;}) + ); + in { + packages = forAllSystems (pkgs: { + oxicloud = pkgs.callPackage ./package.nix {}; + }); + + nixosModules.oxicloud = import ./module.nix; + + # optional default + defaultPackage.x86_64-linux = self.packages.x86_64-linux.oxicloud; + }; +} diff --git a/tools/nix/module.nix b/tools/nix/module.nix new file mode 100644 index 00000000..78fb26d1 --- /dev/null +++ b/tools/nix/module.nix @@ -0,0 +1,406 @@ +{ + config, + lib, + pkgs, + oxicloud ? callPackage ./package.nix {}, + ... +}: let + cfg = config.services.oxicloud; + + generatedEnv = pkgs.writeText "oxicloud-generated.env" '' + OXICLOUD_STORAGE_PATH=${cfg.storagePath} + OXICLOUD_STATIC_PATH=${cfg.staticPath} + OXICLOUD_SERVER_PORT=${builtins.toString cfg.port} + OXICLOUD_SERVER_HOST=${cfg.host} + OXICLOUD_BASE_URL=${cfg.baseUrl} + + OXICLOUD_DB_CONNECTION_STRING=postgres://${cfg.database.user}:${builtins.readFile cfg.database.password.file}@${cfg.database.host}:${builtins.toString cfg.database.port}/${cfg.database.name} + OXICLOUD_DB_MAX_CONNECTIONS=${builtins.toString cfg.database.maxConnections} + OXICLOUD_DB_MIN_CONNECTIONS=${builtins.toString cfg.database.minConnections} + + OXICLOUD_ENABLE_AUTH=${lib.boolToString cfg.auth.enable} + OXICLOUD_JWT_SECRET=${builtins.readFile cfg.auth.jwtSecret.file} + + OXICLOUD_ACCESS_TOKEN_EXPIRY_SECS=${builtins.toString cfg.auth.accessTokenExpirySecs} + OXICLOUD_REFRESH_TOKEN_EXPIRY_SECS=${builtins.toString cfg.auth.refreshTokenExpirySecs} + + OXICLOUD_ENABLE_USER_STORAGE_QUOTAS=${lib.boolToString cfg.features.userStorageQuotas} + OXICLOUD_ENABLE_FILE_SHARING=${lib.boolToString cfg.features.fileSharing} + OXICLOUD_ENABLE_TRASH=${lib.boolToString cfg.features.trash} + OXICLOUD_ENABLE_SEARCH=${lib.boolToString cfg.features.search} + + OXICLOUD_OIDC_ENABLED=${lib.boolToString cfg.sso.enable} + OXICLOUD_OIDC_ISSUER_URL=${cfg.sso.issuerUrl} + OXICLOUD_OIDC_CLIENT_ID=${cfg.sso.clientId} + OXICLOUD_OIDC_CLIENT_SECRET=${builtins.readFile cfg.sso.clientSecret.file} + OXICLOUD_OIDC_REDIRECT_URI=${cfg.sso.redirectUri} + OXICLOUD_OIDC_SCOPES=${cfg.sso.scopes} + OXICLOUD_OIDC_FRONTEND_URL=${cfg.sso.frontendUrl} + + OXICLOUD_WOPI_ENABLED=${lib.boolToString cfg.wopi.enable} + OXICLOUD_WOPI_DISCOVERY_URL=${cfg.wopi.discoveryUrl} + OXICLOUD_WOPI_SECRET=${builtins.readFile cfg.wopi.secret.file} + ''; +in { + options.services.oxicloud = { + enable = lib.mkEnableOption "OxiCloud"; + + envFile = lib.mkOption { + type = lib.types.path; + default = "/etc/oxicloud/oxicloud.env"; + description = "Environment file containing secrets (DATABASE_URL, etc.)"; + }; + + dataDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/oxicloud"; + description = "Directory for OxiCloud data"; + }; + + storagePath = lib.mkOption { + type = lib.types.path; + default = "${cfg.dataDir}/storage"; + description = "Path for storage file storage"; + }; + + staticPath = lib.mkOption { + type = lib.types.path; + default = "${cfg.dataDir}/static"; + description = "Path for static file storage"; + }; + + port = lib.mkOption { + type = lib.types.int; + default = 8086; + description = "Server port"; + }; + + host = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = "Server bind address"; + }; + + baseUrl = lib.mkOption { + type = lib.types.str; + default = "http://${cfg.host}:${builtins.toString cfg.port}"; + description = "Public base URL for share links"; + }; + + database = { + url = lib.mkOption { + type = lib.types.str; + default = ""; + description = "PostgreSQL connection string: postgres://postgres:postgres@localhost:5432/oxicloud"; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "oxicloud"; + description = "Name of the Postgresql database (if not database url)"; + }; + + password.file = lib.mkOption { + type = lib.types.str; + default = "oxicloud"; + description = "Path to the database password (if not database url)"; + }; + + host = lib.mkOption { + type = lib.types.str; + default = "localhost"; + description = "Host to the postgresql database (if not database url)"; + }; + + port = lib.mkOption { + type = lib.types.int; + default = 5432; + description = "Port to the postgresql database (if not database url)"; + }; + + maxConnections = lib.mkOption { + type = lib.types.int; + default = 20; + description = "Max pool connections"; + }; + + minConnections = lib.mkOption { + type = lib.types.int; + default = 5; + description = "Min pool connections"; + }; + }; + + auth = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable authentication"; + }; + + jwtSecret.file = lib.mkOption { + type = lib.types.str; + default = "/etc/oxicloud/jwt.secret"; + description = "JWT signing secret"; + }; + + accessTokenExpirySecs = lib.mkOption { + type = lib.types.int; + default = 3600; + description = "Access token lifetime (seconds)"; + }; + + refreshTokenExpirySecs = lib.mkOption { + type = lib.types.int; + default = 2592000; + description = "Refresh token lifetime (seconds)"; + }; + }; + + features = { + userStorageQuotas = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Per-user storage quotas"; + }; + + fileSharing = lib.mkOption { + type = lib.types.bool; + default = true; + description = "File/folder sharing"; + }; + + trash = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Trash / recycle bin"; + }; + + search = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Search"; + }; + }; + + oauth2 = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Enable OIDC"; + }; + + issuerUrl = lib.mkOption { + type = lib.types.str; + default = ""; + description = "OIDC issuer URL"; + }; + + clientId = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Client ID"; + }; + + clientSecret.file = lib.mkOption { + type = lib.types.str; + default = "/etc/oxicloud/oidc.secret"; + description = "Client secret"; + }; + + redirectUri = lib.mkOption { + type = lib.types.str; + default = "http://localhost:8086/api/auth/oidc/callback"; + description = "Callback URL"; + }; + + scopes = lib.mkOption { + type = lib.types.str; + default = "openid"; + description = "profile email Requested scopes"; + }; + + frontendUrl = lib.mkOption { + type = lib.types.str; + default = "http://${cfg.host}:${builtins.toString cfg.port}"; + description = "Frontend URL"; + }; + + autoProvision = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Auto-create users on first SSO login"; + }; + + adminGroups = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Groups that grant admin role"; + }; + + disablePasswordLogin = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Hide password form when OIDC enabled"; + }; + + providerName = lib.mkOption { + type = lib.types.str; + default = "SSO"; + description = "Display name for the provider"; + }; + }; + wopi = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Enable WOPI"; + }; + + discoveryUrl = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Collabora/OnlyOffice discovery URL"; + }; + + secret.file = lib.mkOption { + type = lib.types.str; + default = "/etc/oxicloud/wopi.secret"; + description = "(JWT secret) WOPI token signing key"; + }; + + tokenTtlSecs = lib.mkOption { + type = lib.types.int; + default = 86400; + description = "Token lifetime"; + }; + + lockTtlSecs = lib.mkOption { + type = lib.types.int; + default = 1800; + description = "Lock expiration"; + }; + }; + + package = lib.mkOption { + type = lib.types.package; + default = oxicloud; + description = "oxicloud nix package"; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "oxicloud"; + description = "User to run OxiCloud service"; + }; + + group = lib.mkOption { + type = lib.types.str; + default = "services"; + description = "Group to run OxiCloud service"; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.oxicloud-env = { + description = "Prepare OxiCloud environment file"; + + wantedBy = ["multi-user.target"]; + before = ["oxicloud.service"]; + + serviceConfig.Type = "oneshot"; + + script = '' + install -d -m 0755 ${cfg.dataDir} + + cp ${generatedEnv} ${cfg.dataDir}/.env + + if [ -f ${cfg.envFile} ]; then + # append overrides (last wins in dotenv parsing) + cat ${cfg.envFile} >> ${cfg.dataDir}/.env + fi + + chown ${cfg.user}:${cfg.group} ${cfg.dataDir}/.env + chmod 640 ${cfg.dataDir}/.env + ''; + }; + + systemd.tmpfiles.rules = [ + "d ${cfg.dataDir} 0700 ${cfg.user} ${cfg.group} -" + "d ${cfg.storagePath} 0700 ${cfg.user} ${cfg.group} -" + "d ${cfg.staticPath} 0750 ${cfg.user} ${cfg.group} -" + "d /etc/oxicloud 0750 root root -" + "f ${cfg.auth.jwtSecret.file} 0600 oxicloud services -" + "f ${cfg.envFile} 0600 oxicloud services -" + "f ${cfg.oauth2.clientSecret.file} 0600 oxicloud services -" + "f ${cfg.wopi.secret.file} 0600 oxicloud services -" + ]; + + # Create user/group only if using defaults + users.users = lib.mkIf (cfg.user == "oxicloud") { + oxicloud = { + isSystemUser = true; + group = cfg.group; + description = "OxiCloud service user"; + }; + }; + + users.groups = lib.mkIf (cfg.group == "oxicloud") { + oxicloud = {}; + }; + + systemd.services.oxicloud = { + description = "OxiCloud server"; + after = ["network.target" "postgresql.service" "oxicloud-env.service"]; + wantedBy = ["multi-user.target"]; + + serviceConfig = { + User = cfg.user; + Group = cfg.group; + EnvironmentFile = [ + "-${cfg.dataDir}/.env" + "-${generatedEnv}" + "-${cfg.envFile}" + ]; + ExecStart = "${cfg.package}/bin/oxicloud"; + Restart = "always"; + WorkingDirectory = cfg.dataDir; + ReadWritePaths = [cfg.dataDir]; + + # 🔒 hardening + # ProtectSystem = "strict"; + # ProtectHome = true; + + AmbientCapabilities = lib.mkIf (cfg.port < 1024) ["CAP_NET_BIND_SERVICE"]; + CapabilityBoundingSet = + if (cfg.port < 1024) + then ["CAP_NET_BIND_SERVICE"] + else [""]; + DeviceAllow = [""]; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "full"; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + }; + }; + }; +} diff --git a/tools/nix/oxicloud.env b/tools/nix/oxicloud.env new file mode 100644 index 00000000..e832f93c --- /dev/null +++ b/tools/nix/oxicloud.env @@ -0,0 +1,2 @@ +DATABASE_URL=postgres://oxicloud:password@localhost/oxicloud +RUST_LOG=info \ No newline at end of file diff --git a/tools/nix/package.nix b/tools/nix/package.nix new file mode 100644 index 00000000..ed105e28 --- /dev/null +++ b/tools/nix/package.nix @@ -0,0 +1,85 @@ +{pkgs ? import {}}: +pkgs.rustPlatform.buildRustPackage rec { + pname = "oxicloud"; + version = "v.0.5.5"; + + src = pkgs.fetchFromGitHub { + owner = "DioCrafts"; + repo = "OxiCloud"; + rev = "v0.5.5"; + hash = "sha256-Nn8qgLdiw7w4PZIMCiI+UHZGNW64fjWZ5mErTJifRZU="; + }; + + cargoHash = "sha256-4KfrKL2AKkTt3cOXdl9Xr2qed+qy8WSWuqYfN8WJ0bQ="; + + buildInputs = [ + # Alpine Rust + pkgs.postgresql + pkgs.cargo + pkgs.rustup + pkgs.openssl + pkgs.binutils + pkgs.zlib-ng + pkgs.zstd + pkgs.mpfr + pkgs.cryptopp + pkgs.cacert + pkgs.mkcert + pkgs.certstrap + pkgs.musl + pkgs.isl + pkgs.gcc + pkgs.libgcc + pkgs.jansson + pkgs.libatomic_ops + pkgs.pax-utils + pkgs.gomp + pkgs.libressl + pkgs.mpc + pkgs.pkg-config + pkgs.libpq + pkgs.libpqxx + pkgs.perl + pkgs.gnumake + pkgs.su-exec + # Postgres + pkgs.tzdata + pkgs.keyutils + pkgs.gsasl + pkgs.xz + pkgs.libedit + pkgs.libuuid + pkgs.libxslt + # WOPI + # collabora/code:latest + # Extra (I think) + pkgs.ffmpeg + pkgs.libavif + pkgs.librtprocess + pkgs.pdftk + pkgs.imagemagick + ]; + + # If tests fail due to DB, disable: + # doCheck = false; + + postInstall = '' + mkdir -p $out/bin + + # main server + if [ -f target/release/ ]; then + cp -a target/release/oxicloud $out/bin/oxicloud + cp -a target/release/generate-openapi $out/bin/generate-openapi + fi + + cp -a static $out/static + cp -a static-dist $out/static-dist + ''; + + meta = with pkgs.lib; { + description = "Lightweight Rust-powered self-hosted cloud (Nextcloud alternative)"; + homepage = "https://github.com/DioCrafts/OxiCloud"; + license = licenses.mit; + platforms = platforms.linux; + }; +}