370 lines
11 KiB
Markdown
370 lines
11 KiB
Markdown
# Deploying the audit endpoint on the IONOS VPS
|
|
|
|
The website itself stays on the Bitpalast webspace (static files via FTP). Only
|
|
this endpoint runs on the VPS, alongside the existing Git and Nextcloud
|
|
services. Nothing here modifies their configuration.
|
|
|
|
Assumes Debian or Ubuntu with `sudo`. Adjust package commands for other
|
|
distributions.
|
|
|
|
---
|
|
|
|
## 0. DNS
|
|
|
|
Create an **A record** `api.sascha-bach.de` pointing at the VPS IP address
|
|
(and an AAAA record if the VPS has IPv6). Wait until it resolves:
|
|
|
|
```bash
|
|
dig +short api.sascha-bach.de
|
|
```
|
|
|
|
Nothing below works until this answers.
|
|
|
|
---
|
|
|
|
## 1. Survey the machine first
|
|
|
|
```bash
|
|
ssh <user>@<vps>
|
|
|
|
# Which web server is in front? The nginx config below assumes nginx.
|
|
systemctl is-active nginx apache2 2>/dev/null
|
|
|
|
# Is port 3001 free? Gitea commonly sits on 3000, so check before assuming.
|
|
sudo ss -tlnp | grep -E ':(3000|3001)\b' || echo "3001 frei"
|
|
|
|
# Node 20 or newer present?
|
|
node --version 2>/dev/null || echo "Node fehlt"
|
|
```
|
|
|
|
If **Apache** is in front instead of nginx, skip step 6 and set up an Apache
|
|
`ProxyPass` for `api.sascha-bach.de` instead — the rest is unchanged.
|
|
|
|
If **3001 is taken**, pick another free port and change it in three places:
|
|
`.env` (`PORT`), the nginx `proxy_pass` lines, and nothing else.
|
|
|
|
---
|
|
|
|
## 2. Install Node (only if missing)
|
|
|
|
```bash
|
|
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
|
sudo apt-get install -y nodejs
|
|
node --version # must be >= 20
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Dedicated service user
|
|
|
|
The endpoint must not run as root and must not share the Git user's account.
|
|
|
|
```bash
|
|
sudo useradd --system --create-home --home-dir /var/lib/auditapi \
|
|
--shell /usr/sbin/nologin auditapi
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Fetch the code
|
|
|
|
```bash
|
|
sudo mkdir -p /opt/audit-endpoint
|
|
sudo chown auditapi:auditapi /opt/audit-endpoint
|
|
|
|
sudo -u auditapi git clone https://git.sascha-bach.de/saschabach/portfolio-page.git \
|
|
/opt/audit-endpoint
|
|
```
|
|
|
|
If the repository is private, use a deploy token or an SSH key belonging to
|
|
`auditapi`.
|
|
|
|
```bash
|
|
cd /opt/audit-endpoint/backend
|
|
sudo -u auditapi npm install --omit=dev
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Configuration
|
|
|
|
```bash
|
|
sudo -u auditapi cp /opt/audit-endpoint/backend/.env.example \
|
|
/opt/audit-endpoint/backend/.env
|
|
sudo -u auditapi nano /opt/audit-endpoint/backend/.env
|
|
```
|
|
|
|
Fill in:
|
|
|
|
| Variable | Value |
|
|
| --- | --- |
|
|
| `PORT` | `3001` (or the free port from step 1) |
|
|
| `ALLOWED_ORIGINS` | `https://sascha-bach.de,https://www.sascha-bach.de` — the **website** origin, not the API origin |
|
|
| `AUDIT_RECIPIENT` | where requests are delivered |
|
|
| `SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASS` | mailbox credentials |
|
|
| `SMTP_FROM` | an address **on your own domain**, so SPF and DMARC pass |
|
|
|
|
Lock the file down — it holds the mailbox password:
|
|
|
|
```bash
|
|
sudo chmod 600 /opt/audit-endpoint/backend/.env
|
|
sudo chown auditapi:auditapi /opt/audit-endpoint/backend/.env
|
|
```
|
|
|
|
Smoke-test before wiring up systemd:
|
|
|
|
```bash
|
|
cd /opt/audit-endpoint/backend && sudo -u auditapi node server.js
|
|
# expect: Audit endpoint listening on 127.0.0.1:3001
|
|
# Ctrl+C
|
|
```
|
|
|
|
---
|
|
|
|
## 6. systemd service
|
|
|
|
```bash
|
|
sudo cp /opt/audit-endpoint/backend/deploy/audit-endpoint.service \
|
|
/etc/systemd/system/
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable --now audit-endpoint
|
|
|
|
systemctl status audit-endpoint --no-pager
|
|
curl -s http://127.0.0.1:3001/health # → {"status":"ok"}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. nginx vhost
|
|
|
|
Adding a file and reloading never touches the running Git or Nextcloud vhosts.
|
|
|
|
```bash
|
|
sudo cp /opt/audit-endpoint/backend/deploy/nginx-api.sascha-bach.de.conf \
|
|
/etc/nginx/sites-available/api.sascha-bach.de
|
|
sudo ln -s /etc/nginx/sites-available/api.sascha-bach.de \
|
|
/etc/nginx/sites-enabled/
|
|
|
|
# Validates the WHOLE config. If this fails, nothing has changed yet.
|
|
sudo nginx -t
|
|
|
|
# reload, not restart - existing connections to git/nextcloud survive
|
|
sudo systemctl reload nginx
|
|
```
|
|
|
|
---
|
|
|
|
## 8. TLS certificate
|
|
|
|
```bash
|
|
sudo apt-get install -y certbot python3-certbot-nginx # if missing
|
|
sudo certbot --nginx -d api.sascha-bach.de
|
|
```
|
|
|
|
certbot only edits the `api.sascha-bach.de` block. Verify renewal works:
|
|
|
|
```bash
|
|
sudo certbot renew --dry-run
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Verify from outside
|
|
|
|
```bash
|
|
curl -sI https://api.sascha-bach.de/health # → HTTP/2 200
|
|
|
|
curl -s -X POST https://api.sascha-bach.de/api/audit-request \
|
|
-H 'Content-Type: application/json' \
|
|
-H 'Origin: https://sascha-bach.de' \
|
|
-d '{"name":"Test","company":"Test GmbH","email":"du@example.de",
|
|
"url":"https://example.de","role":"client","motivation":"Test",
|
|
"businessConfirmation":"true","gdprConsent":"true","website":""}'
|
|
# → {"success":true,...} and an email arrives
|
|
```
|
|
|
|
Then confirm the protections actually bite:
|
|
|
|
```bash
|
|
# Foreign origin → rejected
|
|
curl -s -X POST https://api.sascha-bach.de/api/audit-request \
|
|
-H 'Content-Type: application/json' -H 'Origin: https://evil.example' \
|
|
-d '{}' -o /dev/null -w '%{http_code}\n'
|
|
|
|
# Fourth request within 15 minutes → 429
|
|
```
|
|
|
|
---
|
|
|
|
## 10. Point the website at it
|
|
|
|
Back on the workstation, in the project root:
|
|
|
|
```bash
|
|
echo 'VITE_AUDIT_ENDPOINT=https://api.sascha-bach.de/api/audit-request' > .env
|
|
npm run deploy
|
|
```
|
|
|
|
Vite bakes the value in at build time, so this must happen **before** the
|
|
upload. `.env` is gitignored.
|
|
|
|
The hostname must match in three places or the browser blocks the request:
|
|
|
|
- `public/.htaccess` → `connect-src 'self' https://api.sascha-bach.de`
|
|
- `VITE_AUDIT_ENDPOINT`
|
|
- the nginx `server_name` and its certificate
|
|
|
|
---
|
|
|
|
## 11. Auto-deploy via webhook (optional)
|
|
|
|
Without this, updating means SSHing in and running the commands under
|
|
"Updating later" by hand every time. This section makes a `git push` to
|
|
your own Gitea/Forgejo trigger a pull + restart automatically.
|
|
|
|
**Check this first:** run `docker ps` to see whether Gitea/Forgejo runs in a
|
|
Docker container. This decides which of the two setups below applies.
|
|
|
|
**Gitea runs natively on this machine (not in Docker):** the webhook listener
|
|
never needs to be reachable from the internet - it binds to `127.0.0.1`
|
|
only, and Gitea calls it over loopback. Use `http://127.0.0.1:3002/deploy-webhook`
|
|
as the Target URL in step 11.4 and skip the nginx part of step 11.3.
|
|
|
|
**Gitea runs in Docker:** inside that container, `127.0.0.1` is the
|
|
container's own loopback, not this host - a target of `127.0.0.1:3002` would
|
|
never connect, regardless of Docker's networking mode. Rather than depending
|
|
on that container's specific network setup (bridge vs. host, custom networks,
|
|
`host.docker.internal` availability - all of which vary by how it was set
|
|
up), route the webhook through the same nginx vhost as the audit endpoint
|
|
instead: the request just goes out over HTTPS to `api.sascha-bach.de` and
|
|
back in, exactly like any other webhook call, independent of how Gitea's
|
|
container is networked. `backend/deploy/nginx-api.sascha-bach.de.conf`
|
|
already has the `/deploy-webhook` location for this - see step 11.3. Use
|
|
`https://api.sascha-bach.de/deploy-webhook` as the Target URL in step 11.4.
|
|
|
|
### 11.1 Generate a secret and configure it
|
|
|
|
```bash
|
|
openssl rand -hex 32
|
|
```
|
|
|
|
Add the output as `WEBHOOK_SECRET` in `/opt/audit-endpoint/backend/.env`
|
|
(same file as the SMTP settings - `WEBHOOK_PORT` and `WEBHOOK_BRANCH` already
|
|
have sane defaults in `.env.example` and normally do not need changing).
|
|
|
|
### 11.2 Grant the narrow sudo rule
|
|
|
|
The webhook process runs as the unprivileged `auditapi` user but needs to
|
|
restart a systemd unit, which normally requires root. This grants exactly
|
|
that, and nothing else:
|
|
|
|
```bash
|
|
sudo visudo -cf /opt/audit-endpoint/backend/deploy/audit-deploy-sudoers
|
|
# only proceed if that reports "parsed OK"
|
|
sudo cp /opt/audit-endpoint/backend/deploy/audit-deploy-sudoers \
|
|
/etc/sudoers.d/audit-deploy
|
|
sudo chmod 440 /etc/sudoers.d/audit-deploy
|
|
```
|
|
|
|
`visudo -cf` validates the file **before** it takes effect - a broken
|
|
`/etc/sudoers.d/` file can lock out sudo for the entire machine, so never
|
|
skip this check.
|
|
|
|
### 11.3 systemd service for the webhook
|
|
|
|
```bash
|
|
sudo cp /opt/audit-endpoint/backend/deploy/audit-webhook.service \
|
|
/etc/systemd/system/
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable --now audit-webhook
|
|
|
|
systemctl status audit-webhook --no-pager
|
|
```
|
|
|
|
**Only if Gitea runs in Docker**, additionally expose it through nginx (the
|
|
config was already updated when you copied it in step 7 - if that was before
|
|
this section existed, re-copy it):
|
|
|
|
```bash
|
|
sudo cp /opt/audit-endpoint/backend/deploy/nginx-api.sascha-bach.de.conf \
|
|
/etc/nginx/sites-available/api.sascha-bach.de
|
|
sudo nginx -t
|
|
sudo systemctl reload nginx
|
|
```
|
|
|
|
### 11.4 Register the webhook in Gitea/Forgejo
|
|
|
|
In the repository on `git.sascha-bach.de`: **Settings → Webhooks → Add
|
|
Webhook → Gitea** (Forgejo: the same menu, still labelled "Gitea" - it is a
|
|
Gitea fork and uses the same webhook format).
|
|
|
|
| Field | Value |
|
|
| --- | --- |
|
|
| Target URL | `http://127.0.0.1:3002/deploy-webhook` (native Gitea) **or** `https://api.sascha-bach.de/deploy-webhook` (Gitea in Docker) |
|
|
| HTTP Method | `POST` |
|
|
| POST Content Type | `application/json` |
|
|
| Secret | the value generated in 11.1 |
|
|
| Trigger On | Push events |
|
|
| Branch filter | `master` |
|
|
|
|
Save, then use Gitea's "Test Delivery" button.
|
|
|
|
```bash
|
|
journalctl -u audit-webhook -f
|
|
```
|
|
|
|
Expect `Deploy started.` followed by `Deployed <short-sha>`. If it instead
|
|
shows a signature mismatch, the secret in Gitea and in `.env` do not match.
|
|
|
|
### 11.5 Try it for real
|
|
|
|
```bash
|
|
# on the workstation, after a real commit
|
|
git push origin master
|
|
```
|
|
|
|
Within a few seconds, `journalctl -u audit-endpoint -f` should show a
|
|
restart, and `curl http://127.0.0.1:3001/health` (run locally on the VPS, or
|
|
`curl https://api.sascha-bach.de/health` from anywhere) keeps answering `200`
|
|
throughout - restart happens fast enough that a health check running once a
|
|
minute would not even notice a gap.
|
|
|
|
### Known limitation
|
|
|
|
Changes to `backend/webhook/` itself (the webhook listener's own code) take
|
|
effect only after the **next** manual restart:
|
|
|
|
```bash
|
|
sudo systemctl restart audit-webhook
|
|
```
|
|
|
|
It cannot cleanly restart its own systemd unit mid-deploy - see the comment
|
|
in `backend/webhook/deploy.sh`. This code changes far less often than the
|
|
audit endpoint's own logic, so it is a fair trade for not adding fragile
|
|
self-restart handling.
|
|
|
|
---
|
|
|
|
## Updating later (manual, without the webhook)
|
|
|
|
```bash
|
|
cd /opt/audit-endpoint
|
|
sudo -u auditapi git fetch origin master
|
|
sudo -u auditapi git reset --hard origin/master
|
|
cd backend && sudo -u auditapi npm install --omit=dev
|
|
sudo systemctl restart audit-endpoint
|
|
```
|
|
|
|
Still useful even with the webhook configured: for a manual redeploy without
|
|
waiting for the next push, or if the webhook service itself is down.
|
|
|
|
## Logs
|
|
|
|
```bash
|
|
journalctl -u audit-endpoint -f
|
|
journalctl -u audit-webhook -f # only relevant if step 11 is set up
|
|
```
|
|
|
|
Request contents are never logged, only that a request was forwarded — see the
|
|
note in `services/emailService.js`.
|