taler-docs

Documentation for GNU Taler components, APIs and protocols
Log | Files | Refs | README | LICENSE

commit 1d09311a4cbcd24f5734cb875b26e3d1adab17d3
parent d566f16157971f6d424514325a5518389c0a20f1
Author: Christian Grothoff <christian@grothoff.org>
Date:   Fri,  7 Aug 2026 11:03:41 +0200

update paivana docs

Diffstat:
Mdesign-documents/076-paywall-proxy.rst | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mfrags/paivana-httpd-manual.rst | 303++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mmanpages/paivana-httpd.1.rst | 79++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mmanpages/paivana.conf.5.rst | 75++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
4 files changed, 456 insertions(+), 116 deletions(-)

diff --git a/design-documents/076-paywall-proxy.rst b/design-documents/076-paywall-proxy.rst @@ -221,6 +221,121 @@ Problems: * Solution A: Don't care, unlikely to happen in the first place. * Solution B: Rate-limit template instantiation on a per-IP basis. +Accepted risks: +--------------- + +Three properties of this design were examined and deliberately kept as they +are. Each is recorded here together with the assumption that makes it +acceptable, because a deployment that does not satisfy the assumption does +not get the property. + +Only one price per URL +~~~~~~~~~~~~~~~~~~~~~~ + +paivana-httpd quotes a price by taking the first configured template whose +anchored ``website_regex`` matches the requested URL; a template configured +without a regex matches every URL. At redemption, the checks listed above +are all that can be made: the contract carries a fulfillment URL and a +``max_pickup_time``, and it does not carry the identity of the template it +was instantiated from. paivana-httpd therefore cannot tell an order created +under one template from an order created under another, and in particular +cannot check that the amount paid is the amount its own template search +would have quoted for the URL being unlocked. + +Where an instance carries more than one paivana template — or one template +without a ``website_regex``, which matches everything — this is exploitable +in the obvious way. A client that wants an expensive URL instantiates the +cheap template with that URL as its fulfillment URL, pays the cheap price, +and posts the result for redemption; both orders name the same fulfillment +URL, which is all the redemption check inspects. The exposure is wider than +the configured regular expressions suggest, because the merchant backend +matches ``website_regex`` unanchored where paivana-httpd anchors it: the set +of URLs the backend will sell a template for is a superset of the set +paivana-httpd paywalls with it. + +The mitigation is a property of the configuration rather than of the code. +A merchant instance used by paivana-httpd carries exactly one paivana +template, so that every URL it paywalls has exactly one price and there is +nothing to substitute; differentiated pricing across a site is then a matter +of separate instances, each with its own template and its own paivana-httpd. +**The risk is accepted on the assumption that a deployment presents a single +price for every URL it paywalls.** A deployment that puts two paivana +templates on one instance is selling its expensive articles at the cheaper +price. + +Closing the gap properly requires the merchant backend to record the +instantiating ``template_id`` in the contract terms and to report it with +the order status; paivana-httpd could then re-run its own template search +for the posted ``website`` and require the two to agree. That is a +merchant-side change in a separate upstream, and this document does not +assume it. + +Payment buys access, not a seat +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The redemption POST is idempotent and unmetered, and nothing records that an +order or a paivana ID has already been redeemed. The cookie it returns is +bound to the address of whoever posted the redemption, not to the address +that paid. Anyone holding the four posted fields — order ID, ``nonce``, +``expiration`` and ``website`` — can therefore obtain their own cookie, for +their own address, at any point until ``expiration``, and a buyer who +publishes those four fields has given the article to everyone who reads +them. + +This is intended behaviour and not a defect. What is sold is access to one +resource until one moment, and the buyer may pass that on, in the same way +and for the same reasons that the buyer of a newspaper may hand it to the +next reader. The purchase stays bounded by what was bought: sharing extends +a payment to more readers, never to more URLs and never past ``expiration``, +so a client that wants the whole site still pays for the whole site. That +bound is what lets the design's actual goal — making bulk automated +retrieval expensive — survive the sharing. **The risk is accepted on the +assumption that deployments price access per resource and per unit of time, +and that none of them requires per-seat licensing**, which this design +cannot provide and must not be configured as though it could. + +One consequence has to be stated plainly, because the construction of the +cookie invites the opposite reading: binding the cookie to the client +address is a cookie-theft mitigation and nothing else. It ensures that a +cookie which leaks — from a log, a shared machine, a proxy — is useless to +whoever picks it up. It provides no anti-sharing property whatsoever, since +the redemption that mints cookies is open to every address. + +The redemption endpoint is thus unmetered by intent. Metering it would not +restore any property this design claims; the rate-limiting question raised +above for template instantiation is a question about load, it applies to +this endpoint in the same form, and it is open in the same way. + +Entropy of the server secret +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``paivana_server_secret`` is derived from the configured secret by a single +unsalted SHA-512. There is no stretching and no salt, so the cost of +guessing that secret offline from one observed cookie is one hash per +candidate: the strength of every cookie the deployment will ever issue is +the entropy of the configured string, and nothing more. + +The requirement that follows is placed on the operator. The configured +secret must carry at least 128 bits drawn from a cryptographic random +source, and must never be a passphrase, a hostname, a token reused from +elsewhere, or the placeholder that ships in the sample configuration — which +is an example rather than a secret, and leaves a deployment that keeps it +with no secret at all. Where no secret is configured, paivana-httpd uses a +fresh random value per process, which is safe but invalidates every +outstanding cookie whenever the service restarts. + +Absorbing the requirement into the construction was considered and rejected. +A memory-hard KDF exists to make human-chosen, low-entropy secrets expensive +to guess; it buys a fixed factor, no fixed factor rescues a guessable +phrase, and against 128 genuine bits it buys nothing that is needed. +``paivana_server_secret`` is a machine-generated configuration value that +nobody has to remember or type, so the situation a KDF defends against is +one the deployment can simply not be in. **The risk is accepted on the +assumption that the secret is produced by a random generator and never +chosen by a person**; where a person chooses it, the cookies are forgeable +and the paywall is decorative. How to generate such a secret is operator +guidance and belongs with the manual rather than here. + Implementation: --------------- diff --git a/frags/paivana-httpd-manual.rst b/frags/paivana-httpd-manual.rst @@ -236,10 +236,10 @@ Configuring paivana-httpd The main configuration file is ``/etc/paivana/paivana.conf``. Its syntax follows the standard GNUnet configuration file format and is -documented in full in :manpage:`paivana.conf(5)`. Default values -shipped with the package live under -``/usr/share/paivana/config.d/``; values in ``paivana.conf`` -override those defaults. +documented in full in :manpage:`paivana.conf(5)`. Files under +``$PREFIX/share/paivana/config.d/`` are read first and overridden by +it, but Paivana installs none: every option is either required or has +the default stated with it in :manpage:`paivana.conf(5)`. All Paivana-specific keys live in the ``[paivana]`` section. At a minimum, the file must specify three things: @@ -274,21 +274,23 @@ like this: MERCHANT_BACKEND_URL = http://localhost:9966/ MERCHANT_ACCESS_TOKEN = secret-token:CHANGE-ME - # Stable secret used to MAC the access cookie. - # If unset, a random value is generated at every startup, + # Stable key material used to MAC the access cookie. Must be + # generated from a random source, not invented; see below. If + # unset, a random value is generated at every startup, # invalidating all previously issued cookies. - SECRET = please-change-this-to-a-long-random-value + SECRET = REPLACE-ME-WITH-GENERATED-RANDOMNESS # Resources that should never trigger the paywall, e.g. # logos, stylesheets or favicons. WHITELIST = ^/(favicon\.ico|assets/.*|robots\.txt)$ The exhaustive list of supported keys (``SERVE``, ``PORT``, -``BIND_TO``, ``UNIXPATH``, ``UNIXPATH_MODE``, ``BASE_URL``, -``DESTINATION_BASE_URL``, ``MERCHANT_BACKEND_URL``, -``MERCHANT_BACKEND_UNIX_PATH``, ``MERCHANT_ACCESS_TOKEN``, -``SECRET``, ``WHITELIST``) is documented in -:manpage:`paivana.conf(5)`. +``BIND_TO``, ``UNIXPATH``, ``UNIXPATH_MODE``, ``CONNECTION_LIMIT``, +``PER_IP_CONNECTION_LIMIT``, ``BASE_URL``, +``DESTINATION_BASE_URL``, ``DESTINATION_UNIXPATH``, +``MERCHANT_BACKEND_URL``, ``MERCHANT_BACKEND_UNIX_PATH``, ``MERCHANT_ACCESS_TOKEN``, +``SECRET``, ``WHITELIST``, ``TRUSTED_PROXIES``, +``TRUSTED_PROXIES6``) is documented in :manpage:`paivana.conf(5)`. If you reach the merchant backend over a UNIX domain socket on the same host (recommended for a single-machine deployment), replace @@ -308,8 +310,13 @@ the ``MERCHANT_BACKEND_URL`` block with: When ``paivana-httpd`` runs behind a trusted reverse proxy (Nginx/Apache), pass ``-f`` / ``--respect-forwarded-headers`` in the -systemd unit's ``ExecStart=`` so the real client address is taken -from ``X-Forwarded-For``. See :manpage:`paivana-httpd(1)` for the +systemd unit's ``ExecStart=`` so the real client address is taken from +the ``Forwarded`` and ``X-Forwarded-For`` headers. ``-f`` is a +statement that the server in front writes those headers itself, so +configure that server accordingly before you pass it; set +``TRUSTED_PROXIES`` / ``TRUSTED_PROXIES6`` as well if there is more +than one proxy in the path. :ref:`Paivana-ReverseProxy` has the +details and working configurations. See :manpage:`paivana-httpd(1)` for the remaining command-line flags (in particular ``-g`` to require only a single payment per site and ``-n`` to disable the paywall for debugging). @@ -376,8 +383,8 @@ A few details worth knowing: expression, which a client can arrange deliberately: with a substring rule, a request for ``/premium/article?x=/robots.txt`` would sail past the paywall. -- Paths longer than 16 kb are rejected outright with HTTP ``414 URI - Too Long``, before any expression is evaluated. The cap bounds the +- Paths longer than 16384 bytes are rejected outright with HTTP ``414 + URI Too Long``, before any expression is evaluated. The cap bounds the cost of a pathological expression on a path an unpaying client controls; no legitimate URL comes close to it. - An invalid regular expression is a fatal configuration error: @@ -403,9 +410,9 @@ A few details worth knowing: Access control: per-page vs. site-wide payment ---------------------------------------------- -Once a client has paid, ``paivana-httpd`` issues an HMAC-protected -access cookie (``Paivana-Cookie``) instead of asking the merchant -backend again on every subsequent request. The cookie is a keyed hash +Once a client has paid, ``paivana-httpd`` issues a cryptographically +protected access cookie (``Paivana-Cookie``) instead of asking the +merchant backend again on every subsequent request. The cookie is a keyed hash over *(expiration time, website, client address)*; it carries no server-side state, so any of the (possibly several) ``paivana-httpd`` processes can validate it. @@ -435,9 +442,12 @@ simply be copied to another machine. When ``paivana-httpd`` runs behind a TLS-terminating reverse proxy, the socket peer is always that proxy, so **every** client would appear to share one address. Run the daemon with ``-f`` / ``--respect-forwarded-headers`` in that setup so -the real client address is taken from ``X-Forwarded-For`` (only do this -behind a proxy you trust to set that header — see -:ref:`Paivana-ReverseProxy`). +the real client address is taken from the ``Forwarded`` and +``X-Forwarded-For`` headers instead. Only do this behind a proxy that +writes *both* of those headers itself; a header it merely passes +through is the client's own, and then the client chooses the address +its cookie is bound to. :ref:`Paivana-ReverseProxy` has the +configurations that get this right. Cookie lifetime is derived from the order's ``max_pickup_delay``; configure it on the template (``max_pickup_duration``) to control how @@ -446,6 +456,24 @@ again. Because the cookie is keyed by ``SECRET``, leaving ``SECRET`` unset means a new random key is chosen at every startup and all outstanding cookies are invalidated on restart. +``SECRET`` is hashed once to derive that key — no salt, no stretching +— so its own entropy is the only thing protecting it. An attacker who +has seen a single ``Paivana-Cookie`` can test candidate secrets +offline as fast as it can compute SHA-512, which on ordinary hardware +is billions of guesses per second. Give ``SECRET`` **at least 128 +bits of entropy from a random source**; do not invent a passphrase, +and do not ship the placeholder from the sample configuration above. +Generate one with: + +.. code-block:: shell-session + + # head -c 32 /dev/urandom | base64 + +Whoever recovers a weak ``SECRET`` can mint access cookies for any URL +and any client address, and you will not see it happen: the forged +cookies never touch the merchant backend, so nothing distinguishes +them in your logs from paid traffic. + .. _Paivana-Redemption: @@ -611,7 +639,7 @@ whole URL, ``https://example\.com/premium/.*``, or with a leading wildcard, ``.*/premium/.*``, if the same template should apply regardless of the host the site is served under. -A URL longer than 16 kb is answered with HTTP ``414 URI Too Long`` +A URL longer than 16384 bytes is answered with HTTP ``414 URI Too Long`` and no template is evaluated for it. The cap bounds the cost of a pathological expression on a URL an unpaying client controls. @@ -820,49 +848,123 @@ that ``paivana-httpd`` is socket-activated by the shipped ``/run/paivana/httpd/paivana-http.sock``) and that TLS termination happens at the reverse proxy. +In this setup ``paivana-httpd`` has to run with ``-f`` / +``--respect-forwarded-headers``: a UNIX-domain peer has no address of +its own, so without ``-f`` there is no client address at all, the +access cookie cannot be bound to one, and +``POST /.well-known/paivana`` fails. What ``-f`` means, though, is +that the client address becomes whatever the forwarding headers say it +is — and that address is what the cookie is bound to and what every +address-based decision uses. So it is worth being precise about which +part of those headers is believed. + +``paivana-httpd`` reads the chain from the **right**. The rightmost +element was written by the peer it accepted the connection from, which +``-f`` asserts is a proxy of yours; every step further leftwards is +hearsay written by the node to its right, and is permitted only if +that node is listed in ``TRUSTED_PROXIES`` / ``TRUSTED_PROXIES6``. +With no such list configured the walk stops on its first step: trust +extends exactly one hop, and the element the front server itself wrote +is the client. Anything a client prepends to the header sits to the +left of that and can never be promoted. + +The failure mode is therefore not a client that prepends, but a header +the front server never writes at all — because then the rightmost +element is the client's own, and it is believed. Both Nginx and +Apache forward a request header they were not told to touch, and +neither generates an RFC 7239 ``Forwarded`` header on its own. +``paivana-httpd`` *prefers* ``Forwarded`` over ``X-Forwarded-For`` +where both are present, so a front server that carefully sets +``X-Forwarded-For`` and ignores ``Forwarded`` is still handing every +client its choice of address: the client sends one ``Forwarded`` +element, that element is the whole chain, it is the rightmost, and it +wins over everything the proxy set. A paid cookie is then worth +nothing, since anyone can present the address it was issued to. + +Deal with **both** headers, on every request. Whether you overwrite +or append matters less than that you write: appending — Nginx's +``$proxy_add_x_forwarded_for``, Apache's ``ProxyAddHeaders`` — also +puts the real peer rightmost and is what you want when your own proxy +sits behind another one. At the outermost hop there is nothing to +preserve, so the configurations below simply set. + +They are the ones shipped with the Debian package, with TLS +termination added; their comments explain each choice. + .. tab-set:: .. tab-item:: Nginx - Place the snippet below in - ``/etc/nginx/sites-available/example.com`` (the - Debian package installs a starter template under - ``/etc/nginx/sites-available/paivana``), then enable it via - ``ln -s ../sites-available/example.com - /etc/nginx/sites-enabled/`` and reload Nginx - (``systemctl reload nginx``). + The package installs this at + ``/etc/nginx/sites-available/paivana``. Adapt ``server_name`` + and the certificate paths, then enable it via ``ln -s + ../sites-available/paivana /etc/nginx/sites-enabled/`` and + reload Nginx (``systemctl reload nginx``). .. code-block:: nginx - server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name example.com; - - ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; - - location / { - proxy_pass http://unix:/run/paivana/httpd/paivana-http.sock; - proxy_redirect off; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Forwarded-Host $host; - proxy_set_header X-Forwarded-Proto https; - } + # RFC 7239 node identifier for the peer we accepted from. nginx has + # no built-in variable for this: an IPv6 address has to be bracketed + # and therefore quoted (RFC 7239 §6), and a peer with no address is + # "unknown" (§6.3). + map $remote_addr $paivana_forwarded_elem { + ~^[0-9.]+$ "for=$remote_addr"; + ~^[0-9A-Fa-f:.]+$ "for=\"[$remote_addr]\""; + default "for=unknown"; } server { - listen 80; - listen [::]:80; - server_name example.com; - return 301 https://$host$request_uri; + listen 443 ssl; + listen [::]:443 ssl; + server_name example.com; + + ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; + + location / { + proxy_pass http://unix:/run/paivana/httpd/paivana-http.sock; + proxy_redirect off; + proxy_set_header Host $host; + + # paivana-httpd is started with -f (see paivana-httpd.service), so + # it takes the client address for the access cookie from the + # headers set here. What makes that sound is that we set every + # one of them: paivana-httpd reads the chain from the right, so + # the element that counts is the last one written -- ours. A + # header we do not set is forwarded as the client sent it, and + # then the element that counts is the client's own. + # + # $remote_addr rather than $proxy_add_x_forwarded_for because + # this is the outermost hop: the peer we accepted from IS the + # client, and there is no earlier chain worth preserving. If + # this nginx is itself behind another proxy, switch to + # $proxy_add_x_forwarded_for -- appending is equally sound, the + # peer still ends up rightmost -- and list that hop in + # TRUSTED_PROXIES so paivana-httpd may step over it. + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + # RFC 7239. paivana-httpd PREFERS this over the X-Forwarded-* + # headers above, which are kept for origins that only speak those. + # Setting it is not optional: nginx neither generates a Forwarded + # header nor strips the client's, so without this line a client + # can send one element of its own, have it be the whole chain, + # and outrank everything set above. Behind another proxy, + # replace this with the appending form from nginx.org's "Using + # the Forwarded header", which validates $http_forwarded before + # extending it. + proxy_set_header Forwarded "$paivana_forwarded_elem;proto=$scheme;host=$host"; + } } - Make sure ``paivana-httpd`` is started with - ``--respect-forwarded-headers`` (see - :manpage:`paivana-httpd(1)`) so the ``X-Forwarded-For`` - header set above is honoured. + server { + listen 80; + listen [::]:80; + server_name example.com; + return 301 https://$host$request_uri; + } .. tab-item:: Apache @@ -873,11 +975,10 @@ happens at the reverse proxy. # a2enmod proxy proxy_http headers ssl # systemctl reload apache2 - Then drop the following into - ``/etc/apache2/sites-available/example.com.conf`` - (the Debian package installs a starter template at - ``/etc/apache2/sites-available/paivana.conf``), enable it - with ``a2ensite example.com`` and reload Apache. + The package installs the ``<Location>`` block below at + ``/etc/apache2/sites-available/paivana.conf``; it is shown here + inside a TLS-terminating virtual host. Enable it with + ``a2ensite paivana`` and reload Apache. .. code-block:: apacheconf @@ -894,16 +995,86 @@ happens at the reverse proxy. SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem <Location "/"> - ProxyPass "unix:/run/paivana/httpd/paivana-http.sock|http://example.com/" - ProxyPassReverse "unix:/run/paivana/httpd/paivana-http.sock|http://example.com/" - RequestHeader set X-Forwarded-Proto "https" - RequestHeader set X-Forwarded-Host "example.com" + # paivana-httpd is started with -f (see paivana-httpd.service), so it + # takes the client address for the access cookie from the forwarding + # headers, reading the chain from the right. mod_proxy's + # ProxyAddHeaders (on by default) appends the real client to + # X-Forwarded-For, which puts it rightmost -- but mod_proxy does not + # touch Forwarded, and paivana-httpd prefers Forwarded. A + # client-supplied one would arrive whole and be believed. Drop the + # client's copies of all of them first, so that only what Apache and + # the line below produce reaches paivana-httpd. + # + # If this Apache is itself behind another proxy, remove these and + # configure mod_remoteip (RemoteIPHeader / RemoteIPInternalProxy) for + # that hop instead, listing it in TRUSTED_PROXIES. + RequestHeader unset X-Forwarded-For + RequestHeader unset X-Forwarded-Proto + RequestHeader unset X-Forwarded-Host + RequestHeader unset X-Forwarded-Port + RequestHeader unset Forwarded + + # RFC 7239, which paivana-httpd prefers over the X-Forwarded-* headers + # mod_proxy adds. Apache emits no Forwarded of its own, so build the + # element here. It must be the "expr=" form: mod_headers' %{NAME}e + # reads subprocess_env, which is still empty when RequestHeader runs, + # so the %{REMOTE_ADDR}e spelling silently yields "for=(null)". That + # is not a weaker paywall but a dead one -- an element naming no + # address sends paivana-httpd back to the socket peer, which over a + # UNIX socket is no address at all, and no cookie can be issued. + # Note that %{REMOTE_ADDR} yields an unbracketed IPv6 address where + # RFC 7239 §6 asks for for="[...]"; paivana-httpd accepts both. + RequestHeader set Forwarded "expr=for=%{REMOTE_ADDR};proto=%{REQUEST_SCHEME};host=%{HTTP_HOST}" + + ProxyPass "unix:/run/paivana/httpd/paivana-http.sock|http://example.com/" + ProxyPassReverse "unix:/run/paivana/httpd/paivana-http.sock|http://example.com/" </Location> </VirtualHost> - As with Nginx, run ``paivana-httpd`` with - ``--respect-forwarded-headers`` so that the client IP is - taken from ``X-Forwarded-For``. + There is deliberately no ``RequestHeader set X-Forwarded-Proto`` + here. ``mod_proxy`` does not add that header itself, and the + scheme ``paivana-httpd`` uses comes from the ``proto=`` + parameter of the ``Forwarded`` element built above, which it + prefers anyway. + +Neither configuration needs ``TRUSTED_PROXIES`` to be safe — with one +proxy in front, the one hop of trust that ``-f`` grants is exactly the +hop there is. ``TRUSTED_PROXIES`` and ``TRUSTED_PROXIES6`` are what +you need when the chain is **longer** than that: a CDN or a load +balancer in front of your Nginx, whose element your Nginx appends to. +Listing those hops is what permits the walk to keep stepping leftwards +past them and reach the client that is genuinely at the far end: + +.. code-block:: ini + + [paivana] + TRUSTED_PROXIES = 10.0.0.0/8;192.168.0.0/16; + TRUSTED_PROXIES6 = 2001:db8::/32; + +The walk still stops at the first element written by a node you did +not list, so a client that prepends entries of its own gains nothing +by it however long the list gets. Listing too much is the real risk: +every network you add is a network whose members may name any client +address they like. Keep the list to the proxies you operate, and note +that a UNIX-domain peer needs no entry at all — it has no address to +match, is by construction on this host, and access to it is governed +by ``UNIXPATH_MODE``. The syntax has sharp edges (entries are +terminated as well as separated by ``;``, and ``TRUSTED_PROXIES6`` +tolerates no spaces); :manpage:`paivana.conf(5)` documents them. + +One thing the walk does not cover, and the reason the front server's +configuration still matters after you have set all this: ``-f`` also +lets the forwarding headers supply the scheme and authority that +``paivana-httpd`` builds its own URLs from, when ``BASE_URL`` is not +set. Those are read as the **leftmost** ``proto=`` / ``host=`` (or +``X-Forwarded-Proto`` / ``-Host`` / ``-Port``) value, which is not the +element the trusted-proxy walk selects and is not vetted against it. +Setting ``BASE_URL`` to your public HTTPS URL closes that off entirely +and is worth doing whether or not you also list your proxies. + +Conversely, do not pass ``-f`` to a ``paivana-httpd`` that clients can +reach directly. There it is the client, not a proxy, that is setting +those headers, and ``-f`` is the assertion that this is not so. If you operate both Paivana and the merchant backend on the same host, you typically expose them under two different hostnames (e.g. diff --git a/manpages/paivana-httpd.1.rst b/manpages/paivana-httpd.1.rst @@ -13,9 +13,12 @@ Synopsis ======== **paivana-httpd** +[**-c** *FILENAME* | **--config=**\ \ *FILENAME*] [**-f** | **--respect-forwarded-headers**] -[**-h** | **--help**] [**-g** | **--global-payment**] +[**-h** | **--help**] +[**-L** *LOGLEVEL* | **--log=**\ \ *LOGLEVEL*] +[**-l** *FILENAME* | **--logfile=**\ \ *FILENAME*] [**-n** | **--no-payment**] [**-u** *BYTES* | **--max-upload=**\ \ *BYTES*] [**-v** | **--version**] @@ -36,47 +39,73 @@ them to the respective webserver configured. Its options are as follows: +**-c** *FILENAME* | **--config=**\ \ *FILENAME* + Use the configuration in *FILENAME*. Without this option + ``$XDG_CONFIG_HOME/paivana.conf`` is read if that variable is set, + and ``~/.config/paivana.conf`` otherwise. See paivana.conf(5). + **-f** | **--respect-forwarded-headers** - If set, derive the client address from the "Forwarded" (RFC 7239) - or "X-Forwarded-For" request header, falling back to the socket - address when neither is present, and forward upstream the chain - that was received rather than replacing it. "Forwarded" is - preferred where both are present. - - Only enable this when paivana-httpd is itself behind a reverse - proxy that **overwrites** those headers rather than appending to - whatever the client sent - otherwise clients can spoof their - address and so choose the identity their access cookie is bound to. - Note that nginx does not set "X-Forwarded-For" at all unless told - to, and that Apache's ``ProxyAddHeaders`` appends to a - client-supplied value; example configurations for both are shipped - with Paivana. - - Which entry of the chain names the client depends on - ``TRUSTED_PROXIES`` and ``TRUSTED_PROXIES6``; see paivana.conf(5). - Without those options every hop is believed, including the leftmost - entry, and correctness rests entirely on the server in front. + Assert that the peer connecting to paivana-httpd is a reverse proxy + whose forwarding headers may be believed. The client address is + then derived from the "Forwarded" (RFC 7239) or "X-Forwarded-For" + request header, and the chain that was received is forwarded + upstream rather than replaced. "Forwarded" is preferred where both + are present. The socket address is used when neither header is + present, and when the element the chain resolves to names no + address at all - RFC 7239's ``unknown``, an obfuscated identifier, + a host name. + + The flag extends trust exactly one hop, to that peer, and by itself + no further: the client is then the **rightmost** element of the + chain, the only one the peer wrote itself. ``TRUSTED_PROXIES`` and + ``TRUSTED_PROXIES6`` extend the walk leftwards, one element per + trusted hop; see paivana.conf(5). + + Only enable this when the server in front either replaces the + client's forwarding headers or appends its own element to them. + One that passes "Forwarded" or "X-Forwarded-For" through unchanged + - which nginx does with "Forwarded" unless told otherwise - leaves + the rightmost element under the client's control, and with it the + identity the access cookie is bound to. Example configurations for + nginx and Apache are shipped with Paivana. + + When ``BASE_URL`` is unset, the scheme and authority of our own + base URL are taken from the same headers. From "Forwarded" they + come from the element the walk stopped at; from + "X-Forwarded-Proto", "X-Forwarded-Host" and "X-Forwarded-Port" they + are the leftmost values received, which the walk does not vet. This option is required when ``SERVE`` is ``unix``: a UNIX domain peer has no address of its own, so without it there is no client address at all, the access cookie cannot be bound to a client, and payment callbacks fail. -**-h** | **--help** - Print short help on options. - **-g** | **--global-payment** Disable per-page payment, a single payment is always valid for the entire site. +**-h** | **--help** + Print short help on options. + +**-L** *LOGLEVEL* | **--log=**\ \ *LOGLEVEL* + Specifies the log level to use. Accepted values are: ``DEBUG``, + ``INFO``, ``WARNING``, ``ERROR``. + +**-l** *FILENAME* | **--logfile=**\ \ *FILENAME* + Send logging output to *FILENAME*. + **-n** | **--no-payment** Disable payment, only act as a reverse proxy that does nothing. Useful to temporarily disable the paywall or to test the reverse proxy itself. **-u** *BYTES* | **--max-upload=**\ \ *BYTES* - Set the maximum upload size allowed by the reverse proxy. - Only applies to the body size (the HTTP header has a separate limit). + Set the maximum upload size allowed by the reverse proxy; the + default is 1048576. Requests with a larger body are rejected with + HTTP 413. Only applies to the body size (the HTTP header has a + separate limit). The body is buffered in memory before it is + forwarded and the implementation caps that buffer at 40 MiB, so a + value above 41943040 has no effect. **-v** | **--version** Print version number. diff --git a/manpages/paivana.conf.5.rst b/manpages/paivana.conf.5.rst @@ -14,10 +14,12 @@ Description .. include:: ../frags/common-conf-syntax.rst -Files containing default values for many of the options described below -are installed under ``$PREFIX/share/paivana/config.d/``. -The configuration file given with **-c** to Paivana binaries -overrides these defaults. +Paivana is configured from the file named with **-c**; without that +option, ``$XDG_CONFIG_HOME/paivana.conf`` is read if that variable is +set, and ``~/.config/paivana.conf`` otherwise. Files under +``$PREFIX/share/paivana/config.d/`` are read first and overridden by +it, but Paivana installs none: every option below is either required +or has the default stated with it. A configuration file may include another, by using the ``@INLINE@`` directive, for example, in ``main.conf``, you could write ``@INLINE@ sub.conf`` to @@ -52,28 +54,43 @@ UNIXPATH_MODE = 660 What should be the file access permissions for ``UNIXPATH``? Only used if ``SERVE`` is ``unix``. +CONNECTION_LIMIT + Total number of concurrent client connections to accept, default + 512. Divided evenly over the listen sockets that come up - one per + address bound, typically two when ``BIND_TO`` is empty - so this is + the process-wide total. Outbound requests spend file descriptors + from the same table, so leave headroom below the process limit. + This setting is optional. + +PER_IP_CONNECTION_LIMIT + Concurrent connections accepted from any one peer address, default + 32; 0 disables the check. This is the address of the peer that + connected, not the client address recovered from the forwarding + headers, so set it to 0 wherever the two differ: under ``SERVE`` + ``unix`` or ``systemd`` every client shares one peer, and behind a + reverse proxy or a NAT many clients do. This setting is optional. + TRUSTED_PROXIES IPv4 networks whose members are reverse proxies trusted to report - the client address in the ``X-Forwarded-For`` or ``Forwarded`` + the client address in the ``Forwarded`` or ``X-Forwarded-For`` request headers. Only consulted if paivana-httpd is started with **-f**; see paivana-httpd(1). - Without this option, **-f** believes every hop of the forwarding - chain, including its leftmost entry — which is whatever the client - itself wrote, unless the server in front overwrites the header. A - client could then choose the address its access cookie is bound to, - and share one paid cookie with arbitrarily many others. Naming the - proxies here removes that dependency on the front server's - configuration: the chain is walked from the right with listed - proxies skipped, and the first entry that is not one of them is the - client. Entries a client prepended cannot be promoted this way, - because the walk stops at the address the trusted proxy actually - reported. If the peer that connected is not itself listed, the - forwarding headers are ignored entirely. - - A peer on a UNIX domain socket is always trusted: it has no address - to match, is by construction on the local machine, and access to the - socket is governed by ``UNIXPATH_MODE``. + **-f** trusts the peer that connected, and by itself nothing beyond + it: the client is then the rightmost element of the chain, the only + one that peer wrote itself. Each network named here lets the walk + step one element further left, past a node it matches. The chain is + walked from the right and the first element that is not a listed + proxy is the client, so entries a client prepended cannot be + promoted: the walk stops at the address the trusted proxy actually + reported. This is what makes a chain longer than one hop usable + without trusting whatever the client wrote at its head. + + The peer itself is not matched against these lists; **-f** is the + statement that it may be believed. A peer on a UNIX domain socket + is therefore trusted like any other: it has no address to match, is + by construction on the local machine, and access to the socket is + governed by ``UNIXPATH_MODE``. The syntax is that of GNUnet's network policies, which has three properties worth stating: @@ -107,8 +124,12 @@ TRUSTED_PROXIES6 This setting is optional. BASE_URL - Our own Base URL, used if we cannot learn our own base - URL from "Host" or other HTTP headers. Optional but recommended. + Our own base URL. Required unless **-f** or **-n** is given: + paivana-httpd refuses to start without it, because a client's own + "Host" header cannot identify this site unless a reverse proxy + vouches for it, and with **-n** there is no paywall to identify it + for. When it is unset, the base URL is reconstructed per request + from the forwarding headers and "Host"; see paivana-httpd(1). Write it with a trailing '/', as usual; the trailing slashes are removed before the request path is appended, so the resulting URL never contains a doubled '/'. @@ -140,7 +161,11 @@ SECRET Secret used to determine the Paivana cookie for access control. Should be set to ensure cookies remain valid across restarts. Optional, generated at random at every startup if not set. - This is a secret value. + This is a secret value. It is hashed once, unsalted and without + stretching, to derive the cookie key, so a short or guessable value + can be recovered offline from a single observed cookie: use at least + 128 bits from a cryptographic random source, as shown in the Paivana + manual. WHITELIST Posix extended regular expression whitelisting some paths. @@ -151,7 +176,7 @@ WHITELIST it is anchored at both ends before it is compiled, so ``/assets/`` whitelists nothing and ``/assets/.*`` is needed to whitelist that subtree. Paths longer than - 16 kb are rejected with HTTP 414 rather than matched. + 16384 bytes are rejected with HTTP 414 rather than matched. This setting is optional. SEE ALSO