This commit is contained in:
@@ -54,9 +54,6 @@ 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,
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
[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