full reset

This commit is contained in:
Philip Henning 2025-07-03 23:35:28 +02:00
parent 0b3e086c03
commit 1b58a0ded8
5 changed files with 0 additions and 898 deletions

231
README.md
View file

@ -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).

View file

@ -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";
};
};
};
}

View file

@ -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;
};
};
}

View file

@ -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 ];
};
}

View file

@ -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;
};
}