diff --git a/README.md b/README.md deleted file mode 100644 index 09bba40..0000000 --- a/README.md +++ /dev/null @@ -1,231 +0,0 @@ -# Bitpoll Nix Package - -This repository contains a Nix flake for packaging [Bitpoll](https://github.com/fsinfuhh/Bitpoll), a web application for scheduling meetings and general polling. - -## Features - -- **Complete Nix Package**: Bitpoll packaged as a Nix derivation with all Python dependencies -- **NixOS Service Module**: Ready-to-use systemd service with PostgreSQL integration -- **Security Hardened**: Runs with minimal privileges and security restrictions -- **Configurable**: All major settings exposed as NixOS options -- **Production Ready**: Uses uWSGI with proper process management - -## Quick Start - -### 1. Add to your NixOS configuration - -```nix -{ - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; - bitpoll.url = "github:your-username/bitpoll-nix"; - }; - - outputs = { self, nixpkgs, bitpoll }: { - nixosConfigurations.your-host = nixpkgs.lib.nixosSystem { - system = "x86_64-linux"; - modules = [ - bitpoll.nixosModules.default - { - services.bitpoll = { - enable = true; - secretKey = "your-secret-key-here"; - encryptionKey = "your-encryption-key-here"; - allowedHosts = [ "your-domain.com" ]; - }; - } - ]; - }; - }; -} -``` - -### 2. Generate required keys - -```bash -# Generate Django secret key -python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())" - -# Generate field encryption key (32 bytes, base64 encoded) -python -c "import base64, os; print(base64.b64encode(os.urandom(32)).decode())" -``` - -### 3. Deploy - -```bash -sudo nixos-rebuild switch --flake .#your-host -``` - -## Configuration Options - -### Basic Configuration - -```nix -services.bitpoll = { - enable = true; - - # Required security keys - secretKey = "your-django-secret-key"; - encryptionKey = "your-field-encryption-key"; - - # Network settings - listenAddress = "127.0.0.1"; - port = 3008; # uWSGI socket - httpPort = 3009; # HTTP port (null to disable) - - # Django settings - debug = false; - allowedHosts = [ "your-domain.com" ]; - language = "en-us"; - timezone = "Europe/Berlin"; -}; -``` - -### Database Configuration - -```nix -services.bitpoll = { - # PostgreSQL is enabled by default - enablePostgreSQL = true; - - database = { - name = "bitpoll"; - user = "bitpoll"; - password = ""; # Leave empty for peer authentication - host = "localhost"; - port = 5432; - }; -}; -``` - -### Performance Tuning - -```nix -services.bitpoll = { - # uWSGI process management - processes = 8; # Max processes - threads = 4; # Threads per process - cheaperProcesses = 2; # Min processes - - # Additional uWSGI configuration - extraUwsgiConfig = '' - max-requests = 1000 - reload-on-rss = 512 - ''; -}; -``` - -### Advanced Settings - -```nix -services.bitpoll = { - # Additional Django settings - extraSettings = { - PIPELINE_LOCAL = { - JS_COMPRESSOR = "pipeline.compressors.uglifyjs.UglifyJSCompressor"; - CSS_COMPRESSOR = "pipeline.compressors.cssmin.CSSMinCompressor"; - }; - CSP_ADDITIONAL_SCRIPT_SRC = [ "your-analytics-domain.com" ]; - }; -}; -``` - -## Reverse Proxy Setup - -### Nginx Example - -```nix -services.nginx = { - enable = true; - virtualHosts."your-domain.com" = { - enableACME = true; - forceSSL = true; - locations = { - "/" = { - proxyPass = "http://127.0.0.1:3009"; - proxyWebsockets = true; - extraConfig = '' - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - ''; - }; - "/static/" = { - alias = "/var/lib/bitpoll/static/"; - extraConfig = '' - expires 1y; - add_header Cache-Control "public, immutable"; - ''; - }; - }; - }; -}; -``` - -## Data Storage - -All persistent data is stored in `/var/lib/bitpoll/`: -- `media/` - User uploaded files -- `static/` - Collected static files -- Database data (if using PostgreSQL, stored in PostgreSQL data directory) - -## Security - -The service runs with extensive security hardening: -- Dedicated user account (`bitpoll`) -- Restricted filesystem access -- No network access except required ports -- Memory execution protection -- System call filtering - -## Development - -### Building the package - -```bash -nix build .#bitpoll -``` - -### Development shell - -```bash -nix develop -``` - -### Testing the module - -```bash -nixos-rebuild build-vm --flake .#test-vm -``` - -## Troubleshooting - -### Check service status - -```bash -systemctl status bitpoll -journalctl -u bitpoll -f -``` - -### Database issues - -```bash -# Check PostgreSQL status -systemctl status postgresql - -# Connect to database -sudo -u postgres psql bitpoll -``` - -### Permission issues - -```bash -# Fix data directory permissions -sudo chown -R bitpoll:bitpoll /var/lib/bitpoll -sudo chmod -R u=rwX,g=rX,o= /var/lib/bitpoll -``` - -## License - -This packaging is released under the same license as Bitpoll (GPL-3.0). diff --git a/example-configuration.nix b/example-configuration.nix deleted file mode 100644 index a8b0e51..0000000 --- a/example-configuration.nix +++ /dev/null @@ -1,160 +0,0 @@ -# Example NixOS configuration for Bitpoll -{ config, pkgs, ... }: - -{ - imports = [ - # Import the Bitpoll module - ./module.nix - ]; - - # Enable Bitpoll service - services.bitpoll = { - enable = true; - - # Required security keys (generate these!) - secretKey = "CHANGE-ME-django-secret-key-here"; - encryptionKey = "CHANGE-ME-field-encryption-key-here"; - - # Network configuration - listenAddress = "127.0.0.1"; - port = 3008; # uWSGI socket port - httpPort = 3009; # HTTP port for direct access - - # Django settings - debug = false; - allowedHosts = [ "localhost" "bitpoll.example.com" ]; - language = "en-us"; - timezone = "Europe/Berlin"; - - # Database configuration (PostgreSQL is auto-configured) - database = { - name = "bitpoll"; - user = "bitpoll"; - password = ""; # Empty for peer authentication - host = "localhost"; - port = 5432; - }; - - # Performance settings - processes = 4; # Adjust based on your server - threads = 2; - cheaperProcesses = 1; - - # Additional Django settings - extraSettings = { - # Pipeline configuration for asset compression - PIPELINE_LOCAL = { - JS_COMPRESSOR = "pipeline.compressors.uglifyjs.UglifyJSCompressor"; - CSS_COMPRESSOR = "pipeline.compressors.cssmin.CSSMinCompressor"; - }; - - # Content Security Policy - CSP_ADDITIONAL_SCRIPT_SRC = [ ]; - - # Additional installed apps (if needed) - INSTALLED_APPS_LOCAL = [ ]; - }; - - # Additional uWSGI configuration - extraUwsgiConfig = '' - # Reload workers after 1000 requests to prevent memory leaks - max-requests = 1000 - - # Reload if memory usage exceeds 512MB - reload-on-rss = 512 - - # Enable stats server (optional, for monitoring) - # stats = 127.0.0.1:9191 - ''; - }; - - # Nginx reverse proxy configuration - services.nginx = { - enable = true; - - virtualHosts."bitpoll.example.com" = { - # Enable HTTPS with Let's Encrypt - enableACME = true; - forceSSL = true; - - locations = { - # Proxy all requests to Bitpoll - "/" = { - proxyPass = "http://127.0.0.1:3009"; - proxyWebsockets = true; - extraConfig = '' - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # Increase timeouts for long-running requests - proxy_connect_timeout 60s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - ''; - }; - - # Serve static files directly from Nginx for better performance - "/static/" = { - alias = "/var/lib/bitpoll/static/"; - extraConfig = '' - expires 1y; - add_header Cache-Control "public, immutable"; - gzip on; - gzip_types text/css application/javascript application/json; - ''; - }; - - # Serve media files (user uploads) - "/media/" = { - alias = "/var/lib/bitpoll/media/"; - extraConfig = '' - expires 1d; - add_header Cache-Control "public"; - ''; - }; - }; - }; - }; - - # ACME configuration for Let's Encrypt - security.acme = { - acceptTerms = true; - defaults.email = "admin@example.com"; - }; - - # Firewall configuration - networking.firewall = { - enable = true; - allowedTCPPorts = [ 80 443 ]; - }; - - # Optional: Backup configuration - services.restic.backups.bitpoll = { - initialize = true; - repository = "/backup/bitpoll"; - passwordFile = "/etc/nixos/secrets/restic-password"; - paths = [ "/var/lib/bitpoll" ]; - timerConfig = { - OnCalendar = "daily"; - Persistent = true; - }; - }; - - # Optional: Log rotation - services.logrotate = { - enable = true; - settings = { - "/var/log/bitpoll/*.log" = { - frequency = "daily"; - rotate = 30; - compress = true; - delaycompress = true; - missingok = true; - notifempty = true; - create = "644 bitpoll bitpoll"; - }; - }; - }; -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index f7c6533..0000000 --- a/flake.nix +++ /dev/null @@ -1,42 +0,0 @@ -{ - description = "Bitpoll - A web application for scheduling meetings and general polling"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; - flake-utils.url = "github:numtide/flake-utils"; - }; - - outputs = { self, nixpkgs, flake-utils }: - flake-utils.lib.eachDefaultSystem (system: - let - pkgs = nixpkgs.legacyPackages.${system}; - bitpoll = pkgs.callPackage ./package.nix { }; - in - { - packages = { - default = bitpoll; - bitpoll = bitpoll; - }; - - devShells.default = pkgs.mkShell { - buildInputs = with pkgs; [ - python3 - python3Packages.pip - python3Packages.virtualenv - postgresql - uwsgi - ]; - shellHook = '' - echo "Bitpoll development environment" - echo "Run 'nix build' to build the package" - echo "Run 'nixos-rebuild switch --flake .#' to deploy the service" - ''; - }; - } - ) // { - nixosModules = { - default = import ./module.nix; - bitpoll = import ./module.nix; - }; - }; -} diff --git a/module.nix b/module.nix deleted file mode 100644 index cb84ea8..0000000 --- a/module.nix +++ /dev/null @@ -1,349 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; - -let - cfg = config.services.bitpoll; - - # Persistent data directory - dataDir = "/var/lib/bitpoll"; - - # Generate Django settings file - settingsFile = pkgs.writeText "bitpoll-settings.py" '' - # Bitpoll NixOS Configuration - import os - from bitpoll.settings.production import * - - # Security settings - SECRET_KEY = '${cfg.secretKey}' - FIELD_ENCRYPTION_KEY = '${cfg.encryptionKey}' - DEBUG = ${boolToString cfg.debug} - ALLOWED_HOSTS = ${builtins.toJSON cfg.allowedHosts} - - # Localization - LANGUAGE_CODE = '${cfg.language}' - TIME_ZONE = '${cfg.timezone}' - - # Database configuration - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': '${cfg.database.name}', - 'USER': '${cfg.database.user}', - 'PASSWORD': '${cfg.database.password}', - 'HOST': '${cfg.database.host}', - 'PORT': '${toString cfg.database.port}', - } - } - - # File storage paths - MEDIA_ROOT = '${dataDir}/media' - STATIC_ROOT = '${dataDir}/static' - - # Additional settings - ${concatStringsSep "\n" (mapAttrsToList (k: v: "${k} = ${builtins.toJSON v}") cfg.extraSettings)} - ''; - - # Generate uWSGI configuration - uwsgiConfig = pkgs.writeText "uwsgi.ini" '' - [uwsgi] - procname-master = uwsgi bitpoll - master = true - socket = ${cfg.listenAddress}:${toString cfg.port} - ${optionalString (cfg.httpPort != null) "http-socket = ${cfg.listenAddress}:${toString cfg.httpPort}"} - - plugins = python3 - - chdir = ${dataDir} - virtualenv = ${cfg.package} - pythonpath = ${cfg.package}/lib/python*/site-packages - - module = bitpoll.wsgi:application - env = DJANGO_SETTINGS_MODULE=bitpoll.settings - env = BITPOLL_SETTINGS_FILE=${settingsFile} - env = LANG=C.UTF-8 - env = LC_ALL=C.UTF-8 - - # Process management - uid = ${cfg.user} - gid = ${cfg.group} - umask = 027 - - processes = ${toString cfg.processes} - threads = ${toString cfg.threads} - cheaper = ${toString cfg.cheaperProcesses} - - # Logging - disable-logging = ${boolToString cfg.disableLogging} - - # Static files - static-map = /static=${dataDir}/static - - # Additional uWSGI options - ${cfg.extraUwsgiConfig} - ''; - -in { - options.services.bitpoll = { - enable = mkEnableOption "Bitpoll polling application"; - - package = mkOption { - type = types.package; - default = pkgs.bitpoll or (pkgs.callPackage ./package.nix { }); - description = "The Bitpoll package to use"; - }; - - user = mkOption { - type = types.str; - default = "bitpoll"; - description = "User account under which Bitpoll runs"; - }; - - group = mkOption { - type = types.str; - default = "bitpoll"; - description = "Group under which Bitpoll runs"; - }; - - listenAddress = mkOption { - type = types.str; - default = "127.0.0.1"; - description = "Address to listen on"; - }; - - port = mkOption { - type = types.port; - default = 3008; - description = "Port for uWSGI socket"; - }; - - httpPort = mkOption { - type = types.nullOr types.port; - default = 3009; - description = "Port for HTTP socket (null to disable)"; - }; - - # Django settings - secretKey = mkOption { - type = types.str; - description = "Django secret key"; - example = "your-secret-key-here"; - }; - - encryptionKey = mkOption { - type = types.str; - description = "Field encryption key"; - example = "BnEAJ5eEXb4HfYbaCPuW5RKQSoO02Uhz1RH93eQz0GM="; - }; - - debug = mkOption { - type = types.bool; - default = false; - description = "Enable Django debug mode"; - }; - - allowedHosts = mkOption { - type = types.listOf types.str; - default = [ "*" ]; - description = "List of allowed hosts"; - }; - - language = mkOption { - type = types.str; - default = "en-us"; - description = "Language code"; - }; - - timezone = mkOption { - type = types.str; - default = "Europe/Berlin"; - description = "Time zone"; - }; - - # Database settings - database = { - name = mkOption { - type = types.str; - default = "bitpoll"; - description = "Database name"; - }; - - user = mkOption { - type = types.str; - default = "bitpoll"; - description = "Database user"; - }; - - password = mkOption { - type = types.str; - default = ""; - description = "Database password"; - }; - - host = mkOption { - type = types.str; - default = "localhost"; - description = "Database host"; - }; - - port = mkOption { - type = types.port; - default = 5432; - description = "Database port"; - }; - }; - - # uWSGI settings - processes = mkOption { - type = types.int; - default = 8; - description = "Number of uWSGI processes"; - }; - - threads = mkOption { - type = types.int; - default = 4; - description = "Number of threads per process"; - }; - - cheaperProcesses = mkOption { - type = types.int; - default = 2; - description = "Minimum number of processes"; - }; - - disableLogging = mkOption { - type = types.bool; - default = true; - description = "Disable uWSGI request logging"; - }; - - extraUwsgiConfig = mkOption { - type = types.lines; - default = ""; - description = "Additional uWSGI configuration"; - }; - - extraSettings = mkOption { - type = types.attrsOf types.anything; - default = { }; - description = "Additional Django settings"; - }; - - # PostgreSQL integration - enablePostgreSQL = mkOption { - type = types.bool; - default = true; - description = "Enable and configure PostgreSQL"; - }; - }; - - config = mkIf cfg.enable { - # PostgreSQL setup - services.postgresql = mkIf cfg.enablePostgreSQL { - enable = true; - ensureDatabases = [ cfg.database.name ]; - ensureUsers = [{ - name = cfg.database.user; - ensureDBOwnership = true; - }]; - }; - - # Create user and group - users.users.${cfg.user} = { - isSystemUser = true; - group = cfg.group; - home = dataDir; - createHome = true; - }; - - users.groups.${cfg.group} = { }; - - # Create data directories - systemd.tmpfiles.rules = [ - "d ${dataDir} 0750 ${cfg.user} ${cfg.group} -" - "d ${dataDir}/media 0750 ${cfg.user} ${cfg.group} -" - "d ${dataDir}/static 0750 ${cfg.user} ${cfg.group} -" - ]; - - # Bitpoll service - systemd.services.bitpoll = { - description = "Bitpoll polling application"; - after = [ "network.target" ] ++ optional cfg.enablePostgreSQL "postgresql.service"; - wants = optional cfg.enablePostgreSQL "postgresql.service"; - wantedBy = [ "multi-user.target" ]; - - environment = { - DJANGO_SETTINGS_MODULE = "bitpoll.settings"; - BITPOLL_SETTINGS_FILE = "${settingsFile}"; - PYTHONPATH = "${cfg.package}/lib/python*/site-packages"; - }; - - serviceConfig = { - Type = "notify"; - User = cfg.user; - Group = cfg.group; - WorkingDirectory = dataDir; - ExecStart = "${pkgs.uwsgi}/bin/uwsgi --ini ${uwsgiConfig}"; - ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; - KillMode = "mixed"; - KillSignal = "SIGINT"; - PrivateTmp = true; - ProtectSystem = "strict"; - ProtectHome = true; - ReadWritePaths = [ dataDir ]; - NoNewPrivileges = true; - - # Security hardening - CapabilityBoundingSet = ""; - DeviceAllow = ""; - LockPersonality = true; - MemoryDenyWriteExecute = true; - PrivateDevices = true; - ProtectClock = true; - ProtectControlGroups = true; - ProtectHostname = true; - ProtectKernelLogs = true; - ProtectKernelModules = true; - ProtectKernelTunables = true; - ProtectProc = "invisible"; - ProcSubset = "pid"; - RemoveIPC = true; - RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; - RestrictNamespaces = true; - RestrictRealtime = true; - RestrictSUIDSGID = true; - SystemCallArchitectures = "native"; - SystemCallFilter = [ "@system-service" "~@privileged @resources" ]; - UMask = "0027"; - }; - - # Pre-start script for migrations and static files - preStart = '' - # Wait for database to be ready - ${optionalString cfg.enablePostgreSQL '' - while ! ${pkgs.postgresql}/bin/pg_isready -h ${cfg.database.host} -p ${toString cfg.database.port} -U ${cfg.database.user} -d ${cfg.database.name}; do - echo "Waiting for PostgreSQL..." - sleep 2 - done - ''} - - # Run migrations - cd ${dataDir} - export DJANGO_SETTINGS_MODULE=bitpoll.settings - export BITPOLL_SETTINGS_FILE=${settingsFile} - export PYTHONPATH=${cfg.package}/lib/python*/site-packages - - ${cfg.package}/bin/python ${cfg.package}/manage.py migrate --noinput - ${cfg.package}/bin/python ${cfg.package}/manage.py collectstatic --noinput --clear - - # Set proper permissions - chown -R ${cfg.user}:${cfg.group} ${dataDir} - chmod -R u=rwX,g=rX,o= ${dataDir} - ''; - }; - - # Open firewall ports if needed - networking.firewall.allowedTCPPorts = mkIf (cfg.httpPort != null) [ cfg.httpPort ]; - }; -} diff --git a/package.nix b/package.nix deleted file mode 100644 index 183664e..0000000 --- a/package.nix +++ /dev/null @@ -1,116 +0,0 @@ -{ lib -, buildPythonApplication -, fetchFromGitHub -, python3Packages -, gettext -, libsass -, pkg-config -, postgresql -, uwsgi -}: - -buildPythonApplication rec { - pname = "bitpoll"; - version = "unstable-2024-11-23"; - format = "setuptools"; - - src = fetchFromGitHub { - owner = "fsinfuhh"; - repo = "Bitpoll"; - rev = "4a3e6a5e3500308a428a6c7644f50d423adca6fc"; - hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; - }; - - nativeBuildInputs = [ - gettext - pkg-config - ]; - - buildInputs = [ - libsass - postgresql - ]; - - propagatedBuildInputs = with python3Packages; [ - # Core Django dependencies - django - django-auth-ldap - django-encrypted-model-fields - django-friendly-tag-loader - django-markdownify - django-pipeline - django-token-bucket - django-widget-tweaks - - # Database - psycopg2 - - # Calendar and date handling - caldav - icalendar - python-dateutil - pytz - recurring-ical-events - x-wr-timezone - - # Authentication and security - simple-openid-connect - cryptography - cryptojwt - - # Utilities - bleach - furl - lxml - markdown - requests - sentry-sdk - - # SASS compilation - libsasscompiler - - # Other dependencies - pydantic - six - vobject - ]; - - # Create a setup.py since the project doesn't have one - preBuild = '' - cat > setup.py << EOF -from setuptools import setup, find_packages - -setup( - name='bitpoll', - version='${version}', - packages=find_packages(), - include_package_data=True, - install_requires=[], - scripts=['manage.py'], - entry_points={ - 'console_scripts': [ - 'bitpoll-manage=manage:main', - ], - }, -) -EOF - ''; - - # Compile messages and collect static files - postBuild = '' - export DJANGO_SETTINGS_MODULE=bitpoll.settings.production - python manage.py compilemessages - python manage.py collectstatic --noinput --clear - ''; - - # Skip tests for now as they require additional setup - doCheck = false; - - meta = with lib; { - description = "A web application for scheduling meetings and general polling"; - homepage = "https://github.com/fsinfuhh/Bitpoll"; - license = licenses.gpl3Only; - maintainers = [ ]; - platforms = platforms.linux; - }; -}