another deployment update
This commit is contained in:
@@ -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.
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user