# Flowers Flowers is a small self-hosted credential vault built with Flask and MariaDB/MySQL. This version rewrites the legacy server and browser UI while intentionally preserving its on-disk data contract. ## Existing database compatibility No data migration is required. The rewritten application continues to use: - the `Flowers` database; - one database account and table named `_` per vault; - the existing `organization`, `myID`, `myName`, `mySecret`, `dateCreated`, and `deleted` columns; - the historical password transformation `(password * 10)[:43] + "="` for both the database password and Fernet key; - Fernet ciphertext in the three protected columns; - append-only edits, where the newest timestamp is the visible version; and - `deleted = 1` for deactivation. The old form-encoded `POST /app` endpoint is also retained. Its `list` and `flower` properties remain JSON strings inside the outer JSON response for existing client compatibility. The legacy key derivation is preserved only because changing it would make existing data unreadable. It is not a modern password KDF. A future KDF upgrade requires an explicit, tested data migration rather than an in-place code change. ## What changed - All stored values are passed to MariaDB as query parameters. - Usernames are validated before being used as table/account identifiers. - Database and runtime settings can be supplied through environment variables. - Browser forms have CSRF protection. - Database credentials are stored in server memory behind an opaque session token, not in Flask's signed browser cookie. - Browser sessions expire after ten minutes of inactivity. - Failed-login state is JSON rather than unsafe pickle data. - The browser interface is responsive and no longer depends on jQuery, Pure CSS, or remote assets. - The Flask application factory and WSGI entry point both work. - Compatibility tests cover legacy encryption, reads, writes, listing, and the old API format. ## Requirements - Python 3.10 or newer - MariaDB or MySQL on the configured host - the packages in `requirements.txt` ```bash python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt ``` ## Configuration Local secrets are loaded from the git-ignored `secrets.yaml` file: ```yaml flask: secret_key: "a-long-random-value" database: admin_password: "the-provisioning-account-password" ``` Use `secrets.example.yaml` as a template. `FLOWERS_SECRETS_FILE` can point to a different file. Secret environment variables take precedence over YAML, which is useful for containers and managed deployments. The other settings remain environment-based: | Environment variable | Default | | --- | --- | | `FLOWERS_SECRETS_FILE` | `secrets.yaml` beside the application | | `FLOWERS_SECRET_KEY` | overrides `flask.secret_key` from YAML | | `FLOWERS_DB_HOST` | `localhost` | | `FLOWERS_DB_PORT` | `3306` | | `FLOWERS_DB_NAME` | `Flowers` | | `FLOWERS_DB_ADMIN_USER` | `flower` | | `FLOWERS_DB_ADMIN_PASSWORD` | overrides `database.admin_password` from YAML | | `FLOWERS_URL_PREFIX` | empty | | `FLOWERS_SESSION_MINUTES` | `10` | | `FLOWERS_COOKIE_SECURE` | `0`; set to `1` behind HTTPS | | `FLOWERS_ACCESS_FILE` | `/tmp/wsgi_flower_accessfile` | | `FLOWERS_LOG_FILE` | `/tmp/wsgi_flowers.log` | 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 An existing Flowers database should be left untouched. For a new installation, create the database and provisioning user, replacing the example password: ```sql CREATE DATABASE Flowers; CREATE USER 'flower'@'localhost' IDENTIFIED BY 'replace-this'; GRANT CREATE, SELECT, UPDATE, INSERT ON Flowers.* TO 'flower'@'localhost' WITH GRANT OPTION; GRANT CREATE USER, RELOAD ON *.* TO 'flower'@'localhost'; FLUSH PRIVILEGES; ``` Put the same value in `database.admin_password` in `secrets.yaml` before starting Flowers. ## Run locally Development: ```bash python flowers.py ``` To exercise the production WSGI server locally: ```bash gunicorn --workers 1 --threads 6 --bind 127.0.0.1:5012 'flowers:create_app()' ``` 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 The tests do not need a live database; they use the actual legacy Fernet implementation with a MariaDB-compatible fake connection. ```bash pip install -r requirements-dev.txt python -m pytest ``` They can also run using only the standard library test runner: ```bash python -m unittest discover -s tests -v ``` ## Remaining security constraints The rewrite removes the immediately exploitable SQL interpolation and browser-cookie credential storage, but Flowers remains a small personal vault with a legacy storage design: - the historical password-derived Fernet key is intentionally still weak; - the web process still has provisioning privileges if `/create` is enabled; - 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 Gunicorn to localhost, and expose it only through an authenticated HTTPS reverse proxy. Set stable provisioning and Flask secrets before deployment.