273 lines
18 KiB
Markdown
273 lines
18 KiB
Markdown
# Configuration
|
|||
|
|
|
||
|
|
Volumen reads its configuration from one TOML file:
|
||
|
|
`/etc/volumen/config.toml` for a system install,
|
||
|
|
`~/.config/volumen/config.toml` for a per-user install, with the file that
|
||
|
|
exists tried first when no `--config` is given. With no file anywhere the
|
||
|
|
server runs on the built-in defaults, which put the state under
|
||
|
|
`~/.local/share/volumen`. No environment variable is read (the XDG ones only
|
||
|
|
locate those paths). Flags passed to `volumen serve` override the file, as
|
||
|
|
described under [Precedence](#precedence).
|
||
|
|
|
||
|
|
## File
|
||
|
|
|
||
|
|
The file is copied from the commented template (`internal/config/template.go`),
|
||
|
|
which is committed as `config.toml.example` at the repository root. Every key
|
||
|
|
the loader accepts is listed there with its
|
||
|
|
meaning, so an operator sees the whole surface and edits what this deployment
|
||
|
|
needs rather than discovering keys from a bare table dump. The copy lives at
|
||
|
|
the config path with mode `600`, because it is deployment state.
|
||
|
|
|
||
|
|
```toml
|
||
|
|
# volumen configuration.
|
||
|
|
#
|
||
|
|
# Every key this file accepts is listed here. Copy it to
|
||
|
|
# /etc/volumen/config.toml (or ~/.config/volumen/config.toml for a
|
||
|
|
# per-user installation), then edit.
|
||
|
|
#
|
||
|
|
# Keys that belong to no table come first, because a key written below a
|
||
|
|
# [table] header belongs to that table.
|
||
|
|
|
||
|
|
# Directory of the Markdown posts (.md with TOML frontmatter).
|
||
|
|
content_dir = "/var/lib/volumen/posts"
|
||
|
|
|
||
|
|
# File holding the admin accounts (managed from the admin Settings page).
|
||
|
|
users_file = "/var/lib/volumen/users.toml"
|
||
|
|
|
||
|
|
# How many previous versions of each post to keep in .revisions/
|
||
|
|
# (0 keeps none, which also makes deleting a post permanent).
|
||
|
|
revision_limit = 10
|
||
|
|
|
||
|
|
# Where the audit log is appended, or "" to disable auditing. Records
|
||
|
|
# who changed what, and when, in JSON lines.
|
||
|
|
audit_log = ""
|
||
|
|
|
||
|
|
[server]
|
||
|
|
# Address to bind, as an IP address: "::" is every interface, "::1" is
|
||
|
|
# loopback only, which is what a reverse proxy needs.
|
||
|
|
host = "::"
|
||
|
|
port = 9091
|
||
|
|
# Environment label: "development" or "production". It decides the
|
||
|
|
# startup safety checks (session key length, cookie flags, password
|
||
|
|
# policy).
|
||
|
|
env = "development"
|
||
|
|
# Set true ONLY when a trusted reverse proxy terminates TLS in front of
|
||
|
|
# volumen. Client addresses are then taken from X-Forwarded-For and
|
||
|
|
# cookies are marked Secure.
|
||
|
|
trust_proxy = false
|
||
|
|
# Addresses whose X-Forwarded-For may be believed, as addresses or CIDR
|
||
|
|
# prefixes. An empty list never reads the header and always uses the
|
||
|
|
# connection address; list the proxy so its clients each rate-limit
|
||
|
|
# under their own address.
|
||
|
|
trusted_proxies = []
|
||
|
|
# Set true in production to force the Secure flag on session cookies.
|
||
|
|
cookie_secure = false
|
||
|
|
# Log output format: "text" (human readable) or "json" (structured).
|
||
|
|
log_format = "text"
|
||
|
|
|
||
|
|
[site]
|
||
|
|
title = "Volumen"
|
||
|
|
description = "Powered by Volumen."
|
||
|
|
# Absolute URL of the public site, without a trailing slash.
|
||
|
|
base_url = "https://example.com"
|
||
|
|
language = "en"
|
||
|
|
author = "Anonymous"
|
||
|
|
# Fediverse handle surfaced as the author in feeds and meta tags.
|
||
|
|
# Leave empty to disable.
|
||
|
|
fediverse_creator = ""
|
||
|
|
|
||
|
|
[admin]
|
||
|
|
# Secret that signs session cookies (at least 64 bytes in production).
|
||
|
|
# Leave empty: the server generates one and keeps it in secret.key next
|
||
|
|
# to users.toml. A value here overrides that file.
|
||
|
|
session_key = ""
|
||
|
|
# Session lifetime in seconds (24 hours by default).
|
||
|
|
session_ttl = 86400
|
||
|
|
# Minimum password length enforced when a password is set in the admin UI.
|
||
|
|
min_password_length = 10
|
||
|
|
# Maximum password length, to bound the scrypt work.
|
||
|
|
max_password_length = 1024
|
||
|
|
# Maximum upload size in bytes (10 MB by default).
|
||
|
|
max_upload_bytes = 10485760
|
||
|
|
|
||
|
|
[api]
|
||
|
|
# Public API rate limit: requests allowed per window per client address.
|
||
|
|
# 0 disables rate limiting.
|
||
|
|
rate_limit = 60
|
||
|
|
# Rate-limit window length in seconds.
|
||
|
|
rate_limit_window = 60
|
||
|
|
|
||
|
|
# Scheduled publishing, for a post whose frontmatter carries publish_at.
|
||
|
|
# [scheduler]
|
||
|
|
# enabled = false
|
||
|
|
# interval = 300
|
||
|
|
|
||
|
|
# Outgoing webhooks: POST a signed JSON payload on post changes so a
|
||
|
|
# front-end can rebuild its cache or static pages. Repeat the block for
|
||
|
|
# more endpoints; events may be omitted to receive every event.
|
||
|
|
# [[webhooks]]
|
||
|
|
# url = "https://example.com/hooks/rebuild"
|
||
|
|
# secret = "a-long-random-string" # HMAC-SHA256 signing key
|
||
|
|
# events = ["post.created", "post.updated", "post.deleted", "post.published"]
|
||
|
|
# enabled = true
|
||
|
|
```
|
||
|
|
|
||
|
|
The template lists the four keys that belong to no table first, before any
|
||
|
|
header, because TOML puts a key written below a `[table]` header inside that
|
||
|
|
table. The loader refuses such a file rather than falling back to the default:
|
||
|
|
a `content_dir` written under `[server]` stops the start with a message naming
|
||
|
|
the fix, so a misplaced key is never silently ignored.
|
||
|
|
|
||
|
|
Five state files live next to `users_file`, in the same directory, and are not
|
||
|
|
read from the configuration file itself: `users.toml` holds the admin
|
||
|
|
accounts, started by the first-run wizard; `secret.key` holds the session
|
||
|
|
secret the server generated on first start (unless `[admin].session_key`
|
||
|
|
overrides it); `templates.toml` the `[[templates]]` array of post templates offered
|
||
|
|
in the admin "New post" form, each with `name`, `title`, `slug`, `tags`,
|
||
|
|
`body`, and an optional `[templates.fields]` table of editor inputs to
|
||
|
|
pre-fill (`author`, `lang`, `doi`, `orcid`, `series`, `series_order`,
|
||
|
|
`excerpt`, `cover`, …); `tokens.toml` the API token records, each with `name`, `token_hash`
|
||
|
|
(the SHA-256 digest, never the raw token), `created`, `last_used` and `scopes`;
|
||
|
|
and `webhooks.toml` the admin-managed webhook endpoints, of the same shape as
|
||
|
|
`[[webhooks]]` below. The admin rewrites each atomically at mode `600`, so
|
||
|
|
editing one by hand while the server runs is not advised. A token record
|
||
|
|
without a `scopes` list is unrestricted; a token created with a scope list
|
||
|
|
that names no recognised scope is refused rather than turned into an
|
||
|
|
unrestricted one; the scopes are `write` and `delete`
|
||
|
|
([`internal/tokens/tokens.go`](../internal/tokens/tokens.go)). A `webhooks.toml`
|
||
|
|
that cannot be parsed is logged and ignored, and the config-declared hooks
|
||
|
|
keep working.
|
||
|
|
|
||
|
|
## Keys
|
||
|
|
|
||
|
|
| Key | Type | Default | Effect |
|
||
|
|
|---|---|---|---|
|
||
|
|
| `content_dir` | string | `"/var/lib/volumen/posts"` | Directory of `.md` files with `+++` TOML frontmatter. Archived revisions live under `<content_dir>/.revisions/`, uploaded media under `<content_dir>/media/`. The parent directory must be writable, checked at startup with a write probe |
|
||
|
|
| `users_file` | string | `"/var/lib/volumen/users.toml"` | File holding the admin accounts. Its parent directory must be writable. `templates.toml`, `tokens.toml` and `webhooks.toml` are created next to it |
|
||
|
|
| `revision_limit` | integer | `10` | Archived versions kept per post under `<content_dir>/.revisions/`. `0` disables archiving: a save then keeps no previous version and a delete is permanent |
|
||
|
|
| `audit_log` | string | `""` | Path of the JSON-lines audit log, or empty to disable auditing. Each line is one JSON object carrying `ts` (RFC 3339, UTC), `action` and `user`, and, where the action knows them, `resource`, `detail` and `ip`. The file is created at mode `600`; a write failure is logged and never fatal |
|
||
|
|
| `server.host` | string | `"::"` | Bind address, written as an IP address rather than a name: the address is parsed, not resolved, so `"localhost"` is refused. `"::"` listens on every interface with IPv4 dual-stack; `"::1"` or `"127.0.0.1"` binds loopback only, for a reverse proxy in front |
|
||
|
|
| `server.port` | integer | `9091` | TCP port to bind. `1` to `65535` |
|
||
|
|
| `server.env` | string | `"development"` | `"development"` or `"production"`. The label decides one thing beyond its own validation: in production the session-key rules are fatal, as the `admin.session_key` row and [Validation](#validation) describe |
|
||
|
|
| `server.trust_proxy` | boolean | `false` | Take the client address from `X-Forwarded-For` instead of the connection, and mark session cookies `Secure`. The header is read only when the peer address is inside `server.trusted_proxies`, and the address taken is its last entry, the one the proxy appends when it forwards a request; the entries to its left are client-supplied and can be forged to rotate the rate-limit key, so the proxy must append the connecting address rather than pass the header through. Set it only behind a trusted reverse proxy that terminates TLS, and outside production the start logs a warning saying so |
|
||
|
|
| `server.trusted_proxies` | array of strings | `[]` | Addresses or CIDR prefixes whose `X-Forwarded-For` may be believed. An empty list never reads the header and always uses the connection address, so every client behind the proxy shares one rate-limit budget; a loopback deployment behind nginx lists `["::1", "127.0.0.1"]`. Each entry must parse as an address or a prefix. The address taken is always the header's last entry, the one the trusted proxy appended; with two chained proxies in front of the listener that entry is the front proxy's address, so all of its clients then share one rate-limit bucket, and the deployment must either let only the immediate proxy append the header or size the limit for the aggregate |
|
||
|
|
| `server.cookie_secure` | boolean | `false` | Mark session cookies `Secure` so a browser sends them over HTTPS only. Cookies also carry the flag when `server.trust_proxy` is set, and either setting makes responses carry `Strict-Transport-Security` |
|
||
|
|
| `server.log_format` | string | `"text"` | `"text"` for human-readable lines or `"json"` for one structured object per line through `log/slog`. Applied once at startup by `volumen serve` |
|
||
|
|
| `site.title` | string | `"Volumen"` | Site title, served by `/api/volumen/site` and used in the feeds |
|
||
|
|
| `site.description` | string | `"Powered by Volumen."` | Site description, used in the RSS channel, the Atom subtitle, the JSON Feed and `/api/volumen/site` |
|
||
|
|
| `site.base_url` | string | `"https://example.com"` | Canonical site URL, without a trailing slash. Must be an absolute URL. Builds every absolute link: post permalinks in the feeds, `feed_url`, the sitemap, preview links and the `Sitemap:` line in `robots.txt` |
|
||
|
|
| `site.language` | string | `"en"` | Default language, inherited by a post whose frontmatter and directory carry none. Emitted as `<language>` in RSS and `language` in the JSON Feed. Seeds the admin interface language for the login screen until an account picks its own |
|
||
|
|
| `site.author` | string | `"Anonymous"` | Site author, served by `/api/volumen/site`. It is not substituted into a post's own `author` field |
|
||
|
|
| `site.fediverse_creator` | string | `""` | Optional site-wide handle in `@user@host` form, validated against that pattern when set. Served by `/api/volumen/site`, used as the author of a JSON Feed item whose post has none, and offered as the default in the admin post form. A per-post value takes precedence |
|
||
|
|
| `admin.session_key` | string | `""` | Secret that signs session cookies and preview links. Leave it empty: the server generates a 64-character hex secret on first start and keeps it in `secret.key` beside the users file, so sessions survive restarts without the operator doing anything. A value here overrides that file and must be at least 64 bytes in production; a shorter configured value is refused at startup there, and tolerated in development, where an empty or short key means an ephemeral secret |
|
||
|
|
| `admin.session_ttl` | integer | `86400` | Session lifetime in seconds, used as the cookie `max-age` and enforced when the cookie is loaded. `1` to `31536000` (24 hours by default, one year at most) |
|
||
|
|
| `admin.min_password_length` | integer | `10` | Shortest password the admin accepts when an account is created or a password is changed. At least `1` and at most `admin.max_password_length` |
|
||
|
|
| `admin.max_password_length` | integer | `1024` | Longest password the admin accepts. Bounds the scrypt work, because unbounded input would be a denial-of-service vector. `1` to `1024`; a larger value is refused at startup rather than turning into a failed password change later |
|
||
|
|
| `admin.max_upload_bytes` | integer | `10485760` | Largest accepted upload in bytes: a media upload (`POST /admin/uploads`, refused with `413` and the code `too_large`), a post import and a profile photo, which report the size in the form instead. `1` to `1073741824` (10 MiB by default, 1 GiB at most) |
|
||
|
|
| `api.rate_limit` | integer | `60` | Requests allowed per window per client address across `/api/volumen/*`, counted in a sliding window. `0` disables rate limiting: the middleware is not installed and no `X-RateLimit-*` header is sent. Zero or greater. See the [API documentation](API.md#rate-limiting) for the headers and the `429` body |
|
||
|
|
| `api.rate_limit_window` | integer | `60` | Window length in seconds for the limit above. `1` to `86400` while rate limiting is enabled; a value outside that range is refused at startup |
|
||
|
|
| `scheduler.enabled` | boolean | `false` | Start the in-process publish loop, which is the alternative to running `volumen publish-due` from cron: with the scheduler enabled, `volumen serve` publishes due posts itself, once at startup and then on the interval. Read once, at startup |
|
||
|
|
| `scheduler.interval` | integer | `300` | Seconds between runs. At least `1` when the scheduler is enabled |
|
||
|
|
| `[[webhooks]].url` | string | unset | Endpoint URL, required, absolute `http` or `https`; an entry without it stops the start |
|
||
|
|
| `[[webhooks]].secret` | string | `""` | HMAC-SHA256 signing key. When set, each request carries `X-Volumen-Signature: sha256=<hex digest of the raw body>` |
|
||
|
|
| `[[webhooks]].events` | array of strings | `[]` | Events to receive: `post.created`, `post.updated`, `post.deleted`, `post.published`. Empty or absent receives every event |
|
||
|
|
| `[[webhooks]].enabled` | boolean | `true` | `false` keeps the entry but skips delivery |
|
||
|
|
|
||
|
|
Both scheduled-publishing mechanisms do the same work: a post whose
|
||
|
|
`publish_at` date is today or earlier loses that key and gains a `date` when
|
||
|
|
it carried none, and each published post fires the `post.published` webhook.
|
||
|
|
See [DEPLOYMENT.md](DEPLOYMENT.md#scheduled-publishing).
|
||
|
|
|
||
|
|
The optional `[[webhooks]]` array registers one outgoing webhook per entry.
|
||
|
|
When a post is created, updated, deleted or published, Volumen POSTs a signed
|
||
|
|
JSON body to every matching endpoint. Every request also carries
|
||
|
|
`X-Volumen-Event` (the event name), `X-Volumen-Delivery` (a unique delivery
|
||
|
|
id) and `User-Agent: volumen/<version>`. The body carries `event`,
|
||
|
|
`timestamp`, `version` and, for a post event, a `post` object holding the
|
||
|
|
post summary. Delivery runs in the background with up to three attempts. The
|
||
|
|
most recent deliveries are listed in the admin at **Settings, Webhooks**,
|
||
|
|
where a **Send test** button fires a `ping` event. Endpoints added in the
|
||
|
|
admin are not written into this file: they live in `webhooks.toml` beside the
|
||
|
|
users file, and a change applies there without a restart; the config-declared
|
||
|
|
entries are read-only in the admin and both sets deliver.
|
||
|
|
|
||
|
|
## Precedence
|
||
|
|
|
||
|
|
Sources, strongest first: the flags of `volumen serve`, then the configuration
|
||
|
|
file, then the built-in defaults. There is no environment variable and no
|
||
|
|
second file.
|
||
|
|
|
||
|
|
| Flag | Effect |
|
||
|
|
|---|---|
|
||
|
|
| `--config PATH` | Selects the file to read. Without it the server tries `/etc/volumen/config.toml`, then `~/.config/volumen/config.toml`, and uses the built-in defaults when neither exists |
|
||
|
|
| `--content DIR` | Overrides `content_dir` |
|
||
|
|
| `--host ADDR` | Overrides `server.host` |
|
||
|
|
| `--port N` | Overrides `server.port`, and must be `1` to `65535` |
|
||
|
|
|
||
|
|
```sh
|
||
|
|
volumen serve --config /etc/volumen/config.toml \
|
||
|
|
--content /var/lib/volumen/posts \
|
||
|
|
--host 127.0.0.1 \
|
||
|
|
--port 9000
|
||
|
|
```
|
||
|
|
|
||
|
|
A key absent from the file takes its default, and a key the loader does not
|
||
|
|
know is ignored, so a file left over from an older release still starts. A
|
||
|
|
missing file is not an error either: the defaults are used and one line is
|
||
|
|
logged saying so.
|
||
|
|
|
||
|
|
## Validation
|
||
|
|
|
||
|
|
`Config.Validate()` runs at startup, before the server binds.
|
||
|
|
|
||
|
|
The file is decoded into typed values, and a key whose TOML type does not match
|
||
|
|
its field is an error rather than a silent fallback to the default, because a
|
||
|
|
typo that quietly disables a setting is worse than a refusal to start. The
|
||
|
|
decoder lives in [`internal/config/config.go`](../internal/config/config.go)
|
||
|
|
and covers every key in the table above. A key the decoder does not know is
|
||
|
|
ignored, so a file written for another release still loads, and a root key
|
||
|
|
written below a table header is refused with the fix in the message, as the
|
||
|
|
[File](#file) section describes. The message names the key and the type it
|
||
|
|
found:
|
||
|
|
|
||
|
|
```text
|
||
|
|
volumen serve: parse config /etc/volumen/config.toml: interpres: server.port: cannot assign string to int
|
||
|
|
```
|
||
|
|
|
||
|
|
The remaining rules are value rules:
|
||
|
|
|
||
|
|
| Key | Rule |
|
||
|
|
|---|---|
|
||
|
|
| `server.host` | Non-empty, and an IP address |
|
||
|
|
| `server.port` | `1` to `65535` |
|
||
|
|
| `server.env` | `development` or `production` |
|
||
|
|
| `server.log_format` | `text` or `json` |
|
||
|
|
| `server.trusted_proxies` | Each entry an address or a CIDR prefix |
|
||
|
|
| `site.base_url` | Non-empty, and an absolute URL with a scheme and a host |
|
||
|
|
| `site.fediverse_creator` | `@user@host` when set |
|
||
|
|
| `admin.session_ttl` | `1` to `31536000` |
|
||
|
|
| `admin.min_password_length` | At least `1`, and at most `admin.max_password_length` |
|
||
|
|
| `admin.max_password_length` | At most `1024` |
|
||
|
|
| `admin.max_upload_bytes` | `1` to `1073741824` |
|
||
|
|
| `admin.session_key` | At least 64 bytes when `env = "production"` |
|
||
|
|
| `revision_limit` | Zero or greater |
|
||
|
|
| `api.rate_limit` | Zero or greater |
|
||
|
|
| `api.rate_limit_window` | `1` to `86400` while rate limiting is enabled |
|
||
|
|
| `scheduler.interval` | At least `1` while the scheduler is enabled |
|
||
|
|
| `[[webhooks]].url` | Absolute `http` or `https` URL |
|
||
|
|
| `content_dir` | The parent directory must be creatable and writable |
|
||
|
|
| `users_file` | The parent directory must be creatable and writable |
|
||
|
|
|
||
|
|
A failure stops the process with the message on standard error and exit code
|
||
|
|
`1`, as the example above shows. `volumen doctor --config PATH` reports the
|
||
|
|
same check without starting the server:
|
||
|
|
|
||
|
|
```text
|
||
|
|
config ok /etc/volumen/config.toml
|
||
|
|
config-validate ok
|
||
|
|
posts-readable ok
|
||
|
|
posts-render ok
|
||
|
|
users-file ok
|
||
|
|
password-hashes ok
|
||
|
|
```
|