# Synology Snapshot Admin POC This guide enables external access to the Python snapshot admin service on Synology without exposing the raw service port to the internet. ## Recommended topology 1. Keep the Python service bound to loopback only: ```bash python tools/run_snapshot_admin_server_v1.py \ --host 127.0.0.1 \ --port 8787 \ --db src/quant_engine/snapshot_admin.db \ --seed GatherTradingData.json ``` 2. Put Synology DSM reverse proxy in front of it: - Source: `https://:443` - Destination: `http://127.0.0.1:8787` - Keep the service port closed from direct WAN access. 3. Add browser authentication with the built-in Basic Auth gate: - Set `SNAPSHOT_ADMIN_AUTH_USER` - Set `SNAPSHOT_ADMIN_AUTH_PASSWORD` - Or pass `--auth-user` and `--auth-password` on the wrapper command 4. Verify from the NAS: ```bash curl -i http://127.0.0.1:8787/api/state curl -u "$SNAPSHOT_ADMIN_AUTH_USER:$SNAPSHOT_ADMIN_AUTH_PASSWORD" http://127.0.0.1:8787/api/state ``` 5. Verify from outside the NAS: - Open `https:///` - The browser should prompt for Basic Auth - `https:///tables` should render after login ## DSM Checklist Use these exact values for the first POC. 1. **DSM app path** - `Control Panel` - `Login Portal` - `Advanced` - `Reverse Proxy` 2. **Create reverse proxy rule** - Description: `snapshot-admin` - Source protocol: `HTTPS` - Source hostname: your public DNS name, for example `admin.example.com` - Source port: `443` - Source path: `/` - Destination protocol: `HTTP` - Destination hostname: `127.0.0.1` - Destination port: `8787` 3. **Certificate** - Attach a valid TLS certificate for the public hostname - Prefer a Synology-managed or imported certificate that matches `admin.example.com` 4. **Firewall** - Allow inbound `443/TCP` only for the reverse proxy endpoint - Do not expose `8787/TCP` on WAN - If the NAS must be reachable only from a VPN or office IP range, allowlist those ranges and block the rest 5. **Service start policy** - Start the Python service on boot or via DSM Task Scheduler - Keep it bound to `127.0.0.1` unless you intentionally use direct bind mode - If you use direct bind mode, keep `--allow-remote` and Basic Auth enabled together - For Gitea Actions runner verification, register `act_runner` with a dedicated host label (`self-hosted:host,snapshot-admin-host:host`) if you want to avoid Docker job containers and the `Cleaning up container` log line - Preferred launcher script: `tools/run_snapshot_admin_synology.sh` - Gitea CI deploy path: trigger `.gitea/workflows/snapshot_admin_deploy.yml` `workflow_dispatch` and let the host runner call the launcher script - Runner bootstrap: `tools/re_register_act_runner_synology.sh` - Runner daemon start: `tools/start_act_runner_synology.sh` 6. **Runner re-registration** - Use this when you want to switch an existing runner from Docker mode to host mode: ```bash cd /volume1/projects/data_feed REG_TOKEN="" \ GITEA_URL="http://192.168.123.100:8418" \ bash tools/re_register_act_runner_synology.sh ``` - Expected effect: - removes the existing `.runner` registration file - registers `self-hosted:host,snapshot-admin-host:host` - writes an updated `config.yaml` - If the old runner remains listed in Gitea, remove it from the repository runner page and re-run the command above 7. **Runner start** - After re-registration, start the daemon: ```bash bash tools/start_act_runner_synology.sh ``` - Expected effect: - launches `act_runner daemon` using the existing config - records `runner.pid` and `runner.log` under the runner directory ## DSM Task Scheduler Create two scheduled tasks in `Control Panel > Task Scheduler`. 1. **Boot task** - Task name: `snapshot-admin-start` - User: `root` or a dedicated service account with access to the project folder - Event: `Boot-up` - Command: ```bash bash /volume1/projects/data_feed/tools/run_snapshot_admin_synology.sh start ``` 2. **Healthcheck task** - Task name: `snapshot-admin-healthcheck` - User: same as boot task - Event: `Scheduled Task` - Repeat: every 5 minutes - Command: ```bash bash /volume1/projects/data_feed/tools/run_snapshot_admin_synology.sh healthcheck ``` 3. **Manual restart task** - Task name: `snapshot-admin-restart` - User: same as boot task - Event: `Scheduled Task` - Repeat: manual only, or keep disabled until needed - Command: ```bash bash /volume1/projects/data_feed/tools/run_snapshot_admin_synology.sh restart ``` ## Direct bind mode Direct binding to `0.0.0.0` is allowed only when both auth values are configured: ```bash python tools/run_snapshot_admin_server_v1.py \ --host 0.0.0.0 \ --port 8787 \ --allow-remote \ --auth-user "$SNAPSHOT_ADMIN_AUTH_USER" \ --auth-password "$SNAPSHOT_ADMIN_AUTH_PASSWORD" ``` Use this only if you have a separate firewall or VPN rule in place. The default POC path is still loopback + reverse proxy. ## Validation Run the unit/web checks before and after deployment: ```bash python -m pytest tests/unit/test_snapshot_admin_web_v1.py -q python tools/validate_snapshot_admin_web_v1.py ``` The auth gate is part of the service now, so public exposure without credentials is rejected by the server itself. ## Curl checklist Use this as the POC run sheet. 1. Local service check: ```bash curl -i http://127.0.0.1:8787/api/state ``` Expected: - `200 OK` - JSON payload contains `version.app` 2. Reverse proxy auth challenge: ```bash curl -i https:///api/state ``` Expected: - `401 Unauthorized` - `WWW-Authenticate: Basic` 3. Reverse proxy authenticated access: ```bash curl -u ':' https:///api/state ``` Expected: - `200 OK` - JSON payload contains the same `version.app` 4. UI rendering: ```bash curl -I https:/// curl -I https:///tables ``` Expected: - `200 OK` after auth - HTML response, not a redirect to the raw port 5. Restart persistence: ```bash bash tools/run_snapshot_admin_synology.sh restart bash tools/run_snapshot_admin_synology.sh healthcheck ``` Expected: - `healthcheck ok` - The proxy URL continues to answer after the service restarts ## Live verification Use this sequence on the actual Synology box after the reverse proxy rule is in place: 1. Start the service and confirm the local health endpoint: ```bash curl -i http://127.0.0.1:8787/api/state ``` 2. Confirm the auth gate: ```bash curl -i https:///api/state ``` Expected result: - `401 Unauthorized` when no credentials are provided - `200 OK` when valid Basic Auth credentials are supplied 3. Confirm the browser surface: - Open `https:///` - Sign in with the Basic Auth credentials - Open `https:///tables` - Confirm rows render from the three SQLite sources 4. Confirm the deployment survives a process restart: - Restart the Python service or the task that launches it - Re-run `curl -i http://127.0.0.1:8787/api/state` - Re-open the browser URL and confirm login still works 5. Archive evidence: - Save the `curl` outputs - Save a screenshot of `/` and `/tables` - Record the DSM reverse proxy rule values and certificate name