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.
|
||||
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
|
||||
|
||||
**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