This commit is contained in:
@@ -54,6 +54,9 @@ The image serves HTTP only. Its `scratch` filesystem contains a static musl bina
|
|||||||
and CA certificates for outgoing HTTPS requests, with no shell or package manager.
|
and CA certificates for outgoing HTTPS requests, with no shell or package manager.
|
||||||
It runs as an unprivileged user.
|
It runs as an unprivileged user.
|
||||||
|
|
||||||
|
The maintainer's [VPS deployment files](deploy/README.md) provide a systemd
|
||||||
|
unit for Podman, an nginx configuration and a plain HTML usage page.
|
||||||
|
|
||||||
## Calendar subscription and privacy
|
## Calendar subscription and privacy
|
||||||
|
|
||||||
**Self-hosting is recommended. Do not blindly trust any hosted instance,
|
**Self-hosting is recommended. Do not blindly trust any hosted instance,
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# VPS deployment
|
||||||
|
|
||||||
|
These files configure the maintainer's Debian 12 server with Podman 4.3,
|
||||||
|
systemd and nginx. The nginx configuration uses the existing wildcard
|
||||||
|
certificate at `/etc/letsencrypt/live/popov.link/` and the server's TLS settings.
|
||||||
|
|
||||||
|
| Source | Installed path |
|
||||||
|
| --- | --- |
|
||||||
|
| `vacationplanner.service` | `/etc/systemd/system/vacationplanner.service` |
|
||||||
|
| `vacationplanner.conf` | `/etc/nginx/sites-available/vacationplanner.conf` |
|
||||||
|
| `index.html` | `/var/www/vacationplanner/index.html` |
|
||||||
|
|
||||||
|
Enable the nginx site with a symlink in `/etc/nginx/sites-enabled/`. Validate
|
||||||
|
the unit with `systemd-analyze verify`, run `systemctl daemon-reload`, then
|
||||||
|
`systemctl enable --now vacationplanner.service`. Run `nginx -t` before
|
||||||
|
`systemctl reload nginx`. Back up existing configuration before replacing it.
|
||||||
|
|
||||||
|
The container listens on `127.0.0.1:8080` using host networking. It runs as
|
||||||
|
`65532:65532`, with a read-only filesystem, no capabilities or additional
|
||||||
|
privileges, and limits of 128 MiB without swap, 0.5 CPU and 64 processes.
|
||||||
|
systemd restarts it after an exit and checks `/healthz` before marking the
|
||||||
|
start successful. Graceful shutdown allows 65 seconds.
|
||||||
|
|
||||||
|
Only `/calendar.ics` is proxied. Other valid GET/HEAD paths serve the same
|
||||||
|
HTML 4.01 Strict page; unsupported methods return `405`. Calendar limits are
|
||||||
|
6 requests/minute per IP with a burst of 4, 2 requests/second overall with a
|
||||||
|
burst of 16, and 4 concurrent requests per IP or 16 overall. Rejections return
|
||||||
|
`429` and `Retry-After: 60`. Client addresses come from nginx's existing
|
||||||
|
trusted proxy configuration. Both virtual hosts disable access and error
|
||||||
|
logging; calendar responses are not cached or buffered to disk.
|
||||||
|
|
||||||
|
## Updates and recovery
|
||||||
|
|
||||||
|
The existing `podman-auto-update.timer` checks the registry daily. The
|
||||||
|
`io.containers.autoupdate=registry` label and `PODMAN_SYSTEMD_UNIT` connect
|
||||||
|
the container to its unit. Podman's default rollback restores the previous
|
||||||
|
image if restarting the updated service fails, including its HTTP readiness
|
||||||
|
check. Ordinary restarts use the saved image with `--pull=missing`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo systemctl status vacationplanner.service podman-auto-update.timer
|
||||||
|
curl --fail http://127.0.0.1:8080/healthz
|
||||||
|
sudo podman auto-update --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
Before an update, retain a known-good image under a separate local tag and
|
||||||
|
record its digest. This also protects it from the existing timer's image
|
||||||
|
pruning. To recover manually, tag that saved image as
|
||||||
|
`code.popov.link/valentineus/vacationplanner2ics:latest` and restart
|
||||||
|
`vacationplanner.service`. If the registry image is still faulty, temporarily
|
||||||
|
remove the container's auto-update label from this unit before restarting;
|
||||||
|
restore it when a fixed image is available. Restore nginx files from the
|
||||||
|
backup, validate them and reload nginx if the proxy change must be reverted.
|
||||||
|
|
||||||
|
HTML validation uses OpenSP with the W3C HTML 4.01 Strict DTD, HTML Tidy and
|
||||||
|
Lynx in Docker. Deployment checks use a local mock API to exercise query
|
||||||
|
forwarding, routes, headers, rate limits and concurrent request limits.
|
||||||
|
Use fake credentials for these checks and verify logging before making a
|
||||||
|
real calendar request.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
||||||
|
"http://www.w3.org/TR/html4/strict.dtd">
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii">
|
||||||
|
<title>Vacationplanner calendar subscriptions</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Vacationplanner calendar subscriptions</h1>
|
||||||
|
<p>This service turns Vacationplanner vacations into an iCalendar
|
||||||
|
subscription for Apple Calendar and other calendar applications.</p>
|
||||||
|
|
||||||
|
<h2>Usage</h2>
|
||||||
|
<p>Add a calendar subscription using the following URL. Replace
|
||||||
|
<code>YOUR_API_TOKEN</code> with your Vacationplanner API token and
|
||||||
|
URL-encode its value.</p>
|
||||||
|
<pre>https://vacationplanner.popov.link/calendar.ics?token=YOUR_API_TOKEN</pre>
|
||||||
|
<dl>
|
||||||
|
<dt><code>token</code></dt>
|
||||||
|
<dd>Required: your Vacationplanner API token.</dd>
|
||||||
|
<dt><code>years</code></dt>
|
||||||
|
<dd>Optional: comma-separated years. The default is the current UTC
|
||||||
|
year and the next year. Up to 10 distinct years from 1 to 9998 are
|
||||||
|
accepted; duplicate years are removed.</dd>
|
||||||
|
</dl>
|
||||||
|
<p>To select years explicitly:</p>
|
||||||
|
<pre>https://vacationplanner.popov.link/calendar.ics?token=YOUR_API_TOKEN&years=2026,2027,2028</pre>
|
||||||
|
<p>The calendar application controls the refresh interval. Occasional
|
||||||
|
requests from several devices are supported; frequent requests may be
|
||||||
|
rate limited. Keep the subscription URL private.</p>
|
||||||
|
|
||||||
|
<h2>Privacy and self-hosting</h2>
|
||||||
|
<p><strong>Self-hosting is recommended.</strong> Do not blindly trust
|
||||||
|
any hosted instance, including this one. The subscription URL contains
|
||||||
|
your API token. Service operators and providers that terminate HTTPS
|
||||||
|
can read it. HTTPS does not hide the token from those operators.</p>
|
||||||
|
<p>The service does not store calendars or log requests. Query-string
|
||||||
|
logging is disabled on this server, but this does not guarantee privacy
|
||||||
|
across the infrastructure. Prefer a server you control and replace
|
||||||
|
the host in the example URLs with your own.</p>
|
||||||
|
<p>The repositories below contain source code, ready-made container
|
||||||
|
image links and self-hosting instructions.</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<p>Repository locations:
|
||||||
|
<a href="https://code.popov.link/valentineus/vacationplanner2ics">canonical source</a>
|
||||||
|
· <a href="https://github.com/valentineus/vacationplanner2ics">github</a>
|
||||||
|
· <a href="https://git.popov.link/popov.link/vacationplanner2ics/">read-only mirror</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
limit_req_zone $binary_remote_addr zone=vacationplanner_ip_rate:1m rate=6r/m;
|
||||||
|
limit_req_zone $server_name zone=vacationplanner_total_rate:1m rate=2r/s;
|
||||||
|
limit_conn_zone $binary_remote_addr zone=vacationplanner_ip_conn:1m;
|
||||||
|
limit_conn_zone $server_name zone=vacationplanner_total_conn:1m;
|
||||||
|
|
||||||
|
map $status $vacationplanner_retry_after {
|
||||||
|
default "";
|
||||||
|
429 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
map $status $vacationplanner_allow {
|
||||||
|
default "";
|
||||||
|
405 "GET, HEAD";
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name vacationplanner.popov.link;
|
||||||
|
|
||||||
|
access_log off;
|
||||||
|
error_log /dev/null;
|
||||||
|
add_header Referrer-Policy "no-referrer" always;
|
||||||
|
|
||||||
|
return 301 https://vacationplanner.popov.link$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
server_name vacationplanner.popov.link;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/popov.link/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/popov.link/privkey.pem;
|
||||||
|
ssl_trusted_certificate /etc/letsencrypt/live/popov.link/chain.pem;
|
||||||
|
|
||||||
|
root /var/www/vacationplanner;
|
||||||
|
access_log off;
|
||||||
|
error_log /dev/null;
|
||||||
|
|
||||||
|
# Defining a header here replaces the inherited add_header directives.
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "no-referrer" always;
|
||||||
|
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||||
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||||
|
add_header Retry-After $vacationplanner_retry_after always;
|
||||||
|
add_header Allow $vacationplanner_allow always;
|
||||||
|
|
||||||
|
if ($request_method !~ ^(GET|HEAD)$) {
|
||||||
|
return 405;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /calendar.ics {
|
||||||
|
limit_req zone=vacationplanner_ip_rate burst=4 nodelay;
|
||||||
|
limit_req zone=vacationplanner_total_rate burst=16 nodelay;
|
||||||
|
limit_req_status 429;
|
||||||
|
limit_conn vacationplanner_ip_conn 4;
|
||||||
|
limit_conn vacationplanner_total_conn 16;
|
||||||
|
limit_conn_status 429;
|
||||||
|
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_pass_request_body off;
|
||||||
|
proxy_set_header Content-Length "";
|
||||||
|
proxy_hide_header Referrer-Policy;
|
||||||
|
proxy_hide_header X-Content-Type-Options;
|
||||||
|
proxy_read_timeout 65s;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_cache off;
|
||||||
|
proxy_max_temp_file_size 0;
|
||||||
|
proxy_intercept_errors off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files /index.html =404;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Vacationplanner calendar subscriptions
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
RequiresMountsFor=/var/lib/containers/storage
|
||||||
|
StartLimitIntervalSec=0
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=notify
|
||||||
|
NotifyAccess=all
|
||||||
|
Environment=PODMAN_SYSTEMD_UNIT=%n
|
||||||
|
RuntimeDirectory=vacationplanner
|
||||||
|
RuntimeDirectoryMode=0700
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
TimeoutStartSec=180
|
||||||
|
TimeoutStopSec=80
|
||||||
|
ExecStart=/usr/bin/podman run --detach --rm --replace \
|
||||||
|
--name vacationplanner --cidfile=/run/vacationplanner/container.cid \
|
||||||
|
--cgroups=no-conmon --sdnotify=conmon --pull=missing \
|
||||||
|
--label=io.containers.autoupdate=registry \
|
||||||
|
--network=host --env=BIND_ADDR=127.0.0.1:8080 \
|
||||||
|
--user=65532:65532 --read-only --read-only-tmpfs=false \
|
||||||
|
--cap-drop=ALL --security-opt=no-new-privileges \
|
||||||
|
--memory=128m --memory-swap=128m --cpus=0.5 --pids-limit=64 \
|
||||||
|
--stop-timeout=65 --log-driver=none \
|
||||||
|
code.popov.link/valentineus/vacationplanner2ics:latest
|
||||||
|
ExecStartPost=/usr/bin/curl --fail --silent --show-error \
|
||||||
|
--retry 20 --retry-all-errors --retry-delay 1 --retry-max-time 45 \
|
||||||
|
--connect-timeout 1 --max-time 2 --output /dev/null \
|
||||||
|
http://127.0.0.1:8080/healthz
|
||||||
|
ExecStop=-/usr/bin/podman stop --ignore --time=65 --cidfile=/run/vacationplanner/container.cid
|
||||||
|
ExecStopPost=-/usr/bin/podman rm --force --ignore --cidfile=/run/vacationplanner/container.cid
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Reference in New Issue
Block a user