Files
verdande/rune/verdande.yaml
T
Kristian WindandClaude Opus 5 a38a79e9b6 feat(secrets): tokens og kodeord forlader ikke længere huset med en kopi
Gmails refresh-token lå i klartekst i databasen. Det er ikke filen på serveren,
der er problemet, men kopierne af den: sikkerhedskopier kan hentes ned gennem
fladen, så hver eneste kopi, der er taget indtil nu, bar et levende token.

Derfor ligger nøglen uden for databasen — i miljøet, eller i en fil ved siden af
dataene med tilstand 0600. VAPID-parret er præcedens for en hemmelighed,
instansen selv laver og gemmer i basen, men det mønster dur ikke her: en nøgle i
samme fil som det, den låser, er ingen lås. Backup er VACUUM INTO af selve
databasen, så filen følger ikke med.

Kun de navngivne felter forsegles — refresh_token, access_token, password,
client_secret. Resten af klatten er det, man læser, når en postkasse driller:
hvilken mappe, hvilken etiket, hvilken adresse. At kryptere det med ville ikke
købe noget og ville koste muligheden for at kigge.

Ingen migration, og ingen der skal forbinde forfra: en værdi uden mærket læses
som sig selv, og næste skrivning lægger den forseglet på plads. Kolonnen
konverterer sig selv efterhånden som den bruges, i stedet for i én omgang, der
skal have ret om hver eneste række på én gang. Der er en prøve for netop det.

Fladen siger det på siden, hvor man henter kopien — for den dag nøglen betyder
noget, er den dag, man gendanner et andet sted og undrer sig over, at postkassen
er tavs. Runen er på v4; en ny variabel uden et versionsspring er usynlig.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:18:48 +02:00

160 lines
6.1 KiB
YAML

gameskill:
id: verdande
name: "verdande"
category: "Apps"
description: >
Self-hosted tasks and projects, shared with other people. Natural-language
quick add in Danish and English, sub-tasks, labels, saved filters, board and
calendar views, recurring tasks, reminders, comments and attachments. One
binary with SQLite inside it — no database to set up. Set BASE_URL to the
address you reach it on, point the SMTP settings at your mail server for
invites and reminders, then start it and open the URL to create the first
admin account.
author: "kristianwind"
# Bumped whenever the variables below change. The panel keys its catalogue on
# this: a manifest with new fields and an unchanged version is a manifest the
# panel has no reason to re-read, so the fields never appear and the failure is
# silent — the server's settings page simply does not offer them.
version: 4
icon: "app"
# The image is distroless — no shell — so keep_entrypoint runs the binary the
# image already declares as its ENTRYPOINT, and startup.exec passes it argv
# directly rather than through /bin/sh, which does not exist in there.
docker:
image: "ghcr.io/kristianwind/verdande:latest"
data_path: /data
keep_entrypoint: true
variables:
# verdande cannot work out its own public address: the host port is not chosen
# until the server is created. {{PUBLIC_URL}} is what the panel knows and this
# is what invite links, password resets and ICS feeds are built from — get it
# wrong and the links in your email point somewhere that does not answer.
- key: VERDANDE_BASE_URL
name: "Public address (used in invite and reset links)"
type: string
default: "{{PUBLIC_URL}}"
required: true
- key: VERDANDE_SMTP_HOST
name: "SMTP host (blank = show invite links in the app instead of mailing them)"
type: string
default: ""
- key: VERDANDE_SMTP_PORT
name: "SMTP port"
type: int
default: 587
min: 1
max: 65535
- key: VERDANDE_SMTP_USER
name: "SMTP username"
type: string
default: ""
- key: VERDANDE_SMTP_PASS
name: "SMTP password"
type: string
default: ""
secret: true
- key: VERDANDE_SMTP_FROM
name: "Sender address"
type: string
default: ""
pattern: '^$|^[^@\s]+@[^@\s]+\.[^@\s]+$'
hint: "must be an email address like verdande@example.dk, or blank to leave it unset"
- key: VERDANDE_SMTP_STARTTLS
name: "Use STARTTLS (turn off only for port 465, which is TLS from the start)"
type: bool
default: true
- key: VERDANDE_SESSION_TTL
name: "How long a login lasts (Go duration, e.g. 720h for 30 days)"
type: string
default: "720h"
pattern: '^[0-9]+(ns|us|ms|s|m|h)$'
hint: "must be a duration like 720h, 30m or 45s"
# Off by default because it is an outbound request nobody asked for. It sends
# no identifier and reads one public URL — GitHub's releases API — but that is
# a choice to make rather than a default to inherit.
- key: VERDANDE_UPDATE_CHECK
name: "Look for new releases (asks GitHub once a day; sends nothing about you)"
type: bool
default: false
# Restarting from verdande's own settings, which is also how it picks up a new
# version: a container cannot replace its own image, so the panel recreates it
# and pulls :latest on the way. All three are needed together; the page names
# whichever are missing.
- key: VERDANDE_PANEL_URL
name: "Panel address, so verdande can ask to be restarted from its own settings"
type: string
default: ""
- key: VERDANDE_PANEL_TOKEN
name: "Panel API token — from an account with control of this server"
type: string
secret: true
default: ""
- key: VERDANDE_PANEL_SERVER_ID
name: "This server's id in the panel (the one in its URL)"
type: string
default: ""
- key: VERDANDE_SECRET_KEY
name: "Key for the tokens and mailbox passwords in the database — 32 bytes of base64, from: openssl rand -base64 32. Leave empty to keep one in a file beside the data. Change it and every mailbox must be connected again."
type: string
secret: true
default: ""
install:
# The image has no shell, so the install step borrows one. It only has to make
# the directory layout exist; verdande creates and migrates its own database on
# first start.
image: "busybox:latest"
script: |
set -eu
mkdir -p /data/files /data/backups
echo "verdande data directory ready at /data"
startup:
# No command: with keep_entrypoint the image's own ENTRYPOINT (/verdande) runs,
# and everything is configured through the environment above.
done_regex: '"msg":"listening"'
ports:
- { name: web, default: 8080, protocol: tcp }
# Everything worth keeping is under /data. The -wal and -shm files are included
# deliberately: a WAL-mode SQLite database copied without them can be missing the
# most recent writes.
backup:
include: ["."]
# The container logs JSON — VERDANDE_DEV is off in production — so these patterns
# match the JSON form, not the human-readable one seen when developing locally.
watchers:
- name: "Database errors"
pattern: 'database is locked|disk I/O error|database disk image is malformed'
threshold: 1
window_secs: 300
action: kvasir
- name: "Failed logins"
pattern: '"path":"/api/v1/auth/login".*"status":401'
threshold: 10
window_secs: 300
action: notify
- name: "Server errors"
pattern: '"status":5[0-9][0-9]'
threshold: 5
window_secs: 300
action: kvasir
events:
# Capture group 1 is the client address, which the panel rolls up per source —
# so a brute-force run shows up as one row per address per hour.
- key: login_failed
label: "Failed login"
match: '"path":"/api/v1/auth/login".*"status":401.*"remote":"([^"]+)"'
- key: http_5xx
label: "HTTP 5xx"
match: '"status":5[0-9][0-9].*"remote":"([^"]+)"'