From 73380b0c070231e58e1cef7b92f1847e3c13c821 Mon Sep 17 00:00:00 2001 From: Ignace Date: Sun, 26 Jul 2026 12:44:50 +0200 Subject: [PATCH] another deployment update --- README.md | 209 ++++++++++++++++++++++++++++++++++++++++++++--- requirements.txt | 3 +- 2 files changed, 200 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c07c637..07fd87d 100644 --- a/README.md +++ b/README.md @@ -74,12 +74,6 @@ The other settings remain environment-based: | `FLOWERS_ACCESS_FILE` | `/tmp/wsgi_flower_accessfile` | | `FLOWERS_LOG_FILE` | `/tmp/wsgi_flowers.log` | -For example: - -```bash -waitress-serve --threads=6 --host=127.0.0.1 --port=5012 --call flowers:create_app -``` - The provisioning account is needed only by the web-based `/create` flow. Existing vault reads and writes connect with their existing per-user database accounts. ## Database bootstrap for a new installation @@ -97,7 +91,7 @@ FLUSH PRIVILEGES; Put the same value in `database.admin_password` in `secrets.yaml` before starting Flowers. -## Run +## Run locally Development: @@ -105,13 +99,204 @@ Development: python flowers.py ``` -Production application server: +To exercise the production WSGI server locally: ```bash -waitress-serve --threads=6 --host=127.0.0.1 --port=5012 --call flowers:create_app +gunicorn --workers 1 --threads 6 --bind 127.0.0.1:5012 'flowers:create_app()' ``` -Keep the service behind HTTPS. If it is mounted under `/flowers`, set `FLOWERS_URL_PREFIX=/flowers` before starting it. +Flowers keeps authenticated database credentials in process memory. Always use exactly +one Gunicorn worker: users would otherwise appear to be logged out whenever a request +reached a different worker. Threads provide concurrency within that worker. Restarting +Gunicorn intentionally expires all active Flowers sessions. + +## Deploy with Nginx, Gunicorn, and systemd + +The following example installs Flowers in `/opt/flowers`, runs it as an unprivileged +`flowers` user, binds Gunicorn only to loopback, and exposes it through Nginx over +HTTPS. Adapt the paths, hostname, user, and certificate locations for your server. + +### 1. Install the application + +Install Python, its virtual-environment support, MariaDB/MySQL client libraries, Nginx, +and your distribution's certificate tooling. Then copy or clone the repository and +create the service account and virtual environment: + +```bash +sudo useradd --system --user-group --home /opt/flowers \ + --shell /usr/sbin/nologin flowers +sudo install -d -o flowers -g flowers /opt/flowers +sudo -u flowers git clone https://example.invalid/flowers.git /opt/flowers +sudo -u flowers python3 -m venv /opt/flowers/.venv +sudo -u flowers /opt/flowers/.venv/bin/pip install \ + -r /opt/flowers/requirements.txt +``` + +Replace the example clone URL with this repository's URL. For an artifact-based +deployment, copy the release into `/opt/flowers` instead and make it owned by +`flowers:flowers`. + +### 2. Configure secrets and the environment + +Keep deployment secrets outside the repository: + +```bash +sudo install -d -m 0750 -o root -g flowers /etc/flowers +sudo install -m 0640 -o root -g flowers \ + /opt/flowers/secrets.example.yaml /etc/flowers/secrets.yaml +sudoedit /etc/flowers/secrets.yaml +sudoedit /etc/flowers/flowers.env +sudo chown root:flowers /etc/flowers/flowers.env +sudo chmod 0640 /etc/flowers/flowers.env +``` + +Use this as `/etc/flowers/flowers.env`: + +```dotenv +FLOWERS_SECRETS_FILE=/etc/flowers/secrets.yaml +FLOWERS_DB_HOST=127.0.0.1 +FLOWERS_DB_PORT=3306 +FLOWERS_DB_NAME=Flowers +FLOWERS_DB_ADMIN_USER=flower +FLOWERS_COOKIE_SECURE=1 +FLOWERS_ACCESS_FILE=/var/lib/flowers/access +FLOWERS_LOG_FILE=/var/log/flowers/flowers.log +``` + +Create the writable locations referenced above: + +```bash +sudo install -d -m 0750 -o flowers -g flowers /var/lib/flowers +sudo install -d -m 0750 -o flowers -g flowers /var/log/flowers +``` + +Set a stable, random `flask.secret_key` and the database provisioning password in +`/etc/flowers/secrets.yaml`. Do not generate a new Flask secret on each deployment, +because changing it invalidates every browser session. If the database is on another +host, adjust `FLOWERS_DB_HOST` and ensure its grants and firewall allow the Flowers +server. + +### 3. Create the systemd service + +Create `/etc/systemd/system/flowers.service`: + +```ini +[Unit] +Description=Flowers credential vault +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=flowers +Group=flowers +WorkingDirectory=/opt/flowers +EnvironmentFile=/etc/flowers/flowers.env +ExecStart=/opt/flowers/.venv/bin/gunicorn --workers 1 --threads 6 --bind 127.0.0.1:5012 --access-logfile - --error-logfile - "flowers:create_app()" +Restart=on-failure +RestartSec=5 +TimeoutStopSec=30 +PrivateTmp=true +NoNewPrivileges=true +UMask=0077 + +[Install] +WantedBy=multi-user.target +``` + +Enable the service and confirm that Gunicorn answers locally: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now flowers +sudo systemctl status flowers +curl --fail --head http://127.0.0.1:5012/ +``` + +Service and access logs are available through `journalctl -u flowers`. The application +also writes its own log to the configured `FLOWERS_LOG_FILE`. + +### 4. Configure Nginx + +Create `/etc/nginx/sites-available/flowers` (or the equivalent include path on your +distribution): + +```nginx +server { + listen 80; + listen [::]:80; + server_name flowers.example.com; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name flowers.example.com; + + ssl_certificate /etc/letsencrypt/live/flowers.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/flowers.example.com/privkey.pem; + + client_max_body_size 64k; + + location / { + proxy_pass http://127.0.0.1:5012; + proxy_http_version 1.1; + 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; + proxy_set_header Connection ""; + proxy_read_timeout 30s; + } +} +``` + +Obtain the certificate before enabling the TLS server, or first use your ACME client's +HTTP-only bootstrap configuration. Then enable and validate the site: + +```bash +sudo ln -s /etc/nginx/sites-available/flowers /etc/nginx/sites-enabled/flowers +sudo nginx -t +sudo systemctl reload nginx +curl --fail --head https://flowers.example.com/ +``` + +Keep port `5012` closed in the host firewall; only ports 80 and 443 need to be public. +If another reverse proxy sits in front of Nginx, configure Nginx to accept client IP +headers only from that proxy rather than from arbitrary clients. + +### Deploy under a URL prefix + +To publish Flowers at `https://example.com/flowers/`, add this setting to +`/etc/flowers/flowers.env`: + +```dotenv +FLOWERS_URL_PREFIX=/flowers +``` + +Use these Nginx locations without a trailing path on `proxy_pass`, so the `/flowers` +prefix reaches Flask unchanged: + +```nginx +location = /flowers { + return 301 /flowers/; +} + +location /flowers/ { + proxy_pass http://127.0.0.1:5012; + proxy_http_version 1.1; + 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; + proxy_set_header Connection ""; +} +``` + +After changing the environment or application code, restart Gunicorn with +`sudo systemctl restart flowers`. After changing Nginx, run `sudo nginx -t` before +reloading it. ## Tests @@ -137,4 +322,6 @@ The rewrite removes the immediately exploitable SQL interpolation and browser-co - the in-memory credential store is suitable for one application process, not a multi-process cluster; and - rate-limit state is local to one host. -Use a strong unique password made only of Base64-compatible characters, bind Waitress to localhost, and expose it only through an authenticated HTTPS reverse proxy. Set stable provisioning and Flask secrets before deployment. +Use a strong unique password made only of Base64-compatible characters, bind Gunicorn +to localhost, and expose it only through an authenticated HTTPS reverse proxy. Set +stable provisioning and Flask secrets before deployment. diff --git a/requirements.txt b/requirements.txt index e94efb0..c8f8900 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ Flask>=3.1,<4 fernet>=1.0,<2 +gunicorn>=23.0,<24 PyMySQL>=1.1,<3 PyYAML>=6.0,<7 -waitress>=3.0,<4 +