commit 2d314ea1199dd8310984ddbc28f8704cba8ada67
parent a8b943313330695fb48a8ebb271784a64cbeaba8
Author: Christian Grothoff <christian@grothoff.org>
Date: Fri, 7 Aug 2026 14:03:33 +0200
clean up Debian package and especially hardening
Diffstat:
10 files changed, 136 insertions(+), 133 deletions(-)
diff --git a/README b/README
@@ -44,8 +44,16 @@ How it works
via libcurl and the response is streamed back.
The cookie is an HMAC over `(expiration time, website, client address)` keyed by a
-`paivana_secret` derived from the configured `SECRET`. If `SECRET` is absent,
-a random nonce is used and cookies do not survive a restart of `paivana-httpd`
+`paivana_secret` derived from the configured `SECRET`. `SECRET` is
+required whenever the paywall is on: it is the only input to that MAC
+the client does not supply, so a key invented afresh at each start would
+invalidate every access anyone had already paid for. Under `-n` no
+cookie is ever minted and none is needed.
+
+Keep the same `SECRET` on every `paivana-httpd` serving one site — a
+cookie issued by one has to verify on the next — and treat it as
+equivalent to the content itself: whoever holds it can mint access for
+any visitor, URL and expiry.
Dependencies
@@ -96,13 +104,17 @@ Paivana reads an INI-style `.conf` file. The only section used is
BASE_URL Public base URL of Paivana. Required unless `-f`
is given, in which case it is derived from the
forwarding headers (see below).
+ SECRET Key for the access-cookie MAC and the Paivana ID
+ derivation. Required unless `-n`; see above for
+ why there is no sensible default. The Debian
+ package generates one into
+ /etc/paivana/secrets/paivana.secret.conf, which
+ is not world-readable.
### Optional keys
Key Description
-------- ---------------------------------------------------------------
- SECRET Stable secret for cookie MAC and Paivana ID derivation.
- A random nonce is generated on every startup if absent.
WHITELIST POSIX extended regular expression; matching request paths
are forwarded without payment. Matched against the
*entire* path and anchored at both ends, so `/free/`
@@ -243,10 +255,16 @@ Apache (requires mod_proxy, mod_proxy_http and mod_headers):
RequestHeader set Forwarded \
"for=%{REMOTE_ADDR}e;proto=%{REQUEST_SCHEME}e;host=%{HTTP_HOST}e"
- ProxyPass "unix:/var/lib/paivana/httpd/paivana.sock|http://example.com/"
+ ProxyPass "unix:/run/paivana/httpd/paivana-http.sock|http://example.com/"
</Location>
-Ready-made versions of both are shipped in `debian/etc/`.
+Ready-made versions of both are shipped in `debian/examples/`, and
+installed by the Debian package into
+`/usr/share/doc/paivana-httpd/examples/`. They are examples rather
+than drop-ins on purpose: the package only *recommends* a web server,
+so it must not create `/etc/nginx/` or `/etc/apache2/` on a system
+that has neither (Debian Policy 9.1.1). Copy the one you want into
+place and enable it yourself.
Trusted proxies
diff --git a/debian/control b/debian/control
@@ -30,9 +30,7 @@ Pre-Depends:
Depends:
libtalerexchange (>= 1.6.10),
libtalermerchant (>= 1.6.0),
- libgnutls30 (>= 3.7.1),
adduser,
- lsb-base,
netbase,
${misc:Depends},
${shlibs:Depends}
diff --git a/debian/etc/apache2/sites-available/paivana.conf b/debian/etc/apache2/sites-available/paivana.conf
@@ -1,37 +0,0 @@
-<!--
- Make sure to enable the following Apache modules before
- integrating this into your configuration:
-
- # a2enmod proxy
- # a2enmod proxy_http
- # a2enmod headers
--->
-
-<Location "/">
-# paivana-httpd is started with -f (see paivana-httpd.service), so it
-# takes the client address for the access cookie from the forwarding
-# headers. mod_proxy's ProxyAddHeaders (on by default) *appends* the
-# real client to any X-Forwarded-For the client itself sent, which
-# would leave the client in control of the leftmost entry -- and thus
-# of its own identity. Drop the client's copies first so that what
-# mod_proxy adds is the only thing paivana-httpd sees.
-#
-# If this Apache is itself behind another proxy, remove these and
-# configure mod_remoteip (RemoteIPHeader / RemoteIPInternalProxy) for
-# that hop instead.
-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. "set" rather than "append": this is the outermost hop,
-# so a client-supplied element must not survive. Note that
-# %{REMOTE_ADDR}e yields an unbracketed IPv6 address where RFC 7239 §6
-# asks for for="[...]"; paivana-httpd accepts both.
-RequestHeader set Forwarded "for=%{REMOTE_ADDR}e;proto=%{REQUEST_SCHEME}e;host=%{HTTP_HOST}e"
-
-ProxyPass "unix:/var/lib/paivana/httpd/paivana.sock|http://example.com/"
-</Location>
diff --git a/debian/etc/nginx/sites-available/paivana b/debian/etc/nginx/sites-available/paivana
@@ -1,47 +0,0 @@
-# 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
-
- 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. That is only sound because this server is the
- # outermost hop and *overwrites* them: $remote_addr is the peer we
- # actually accepted, whereas $proxy_add_x_forwarded_for would
- # append it to whatever the client claimed, leaving the client in
- # control of the leftmost entry -- and thus of its own identity.
- #
- # If this nginx is itself behind another proxy, switch to
- # $proxy_add_x_forwarded_for and set real_ip_header /
- # set_real_ip_from for that hop.
- 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.
- # Again a plain "set": nginx offers no $proxy_add_forwarded, and at
- # the outermost hop we would not want one -- a client-supplied
- # element must not survive. 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";
- }
-}
diff --git a/debian/etc/paivana/paivana.conf b/debian/etc/paivana/paivana.conf
@@ -4,16 +4,10 @@
# after having already been handed the activated socket.
SERVE = systemd
-# The three settings below have no default and paivana-httpd refuses to
+# The two settings below have no default and paivana-httpd refuses to
# start without them.
# DESTINATION_BASE_URL = http://localhost:8080/
# MERCHANT_BACKEND_URL = http://localhost:9966/
-# MERCHANT_ACCESS_TOKEN = secret-token:CHANGE-ME
-
-# Key for the access cookie's MAC. Without it a fresh random key is
-# generated at every start, so every restart invalidates every access
-# a customer has paid for.
-# SECRET = CHANGE-ME
# Which HTTP port does the backend listen on? Only used if "SERVE" is 'tcp'.
# PORT = 9967
@@ -23,3 +17,7 @@ SERVE = systemd
# if left empty. Only used if "SERVE" is 'tcp'.
# BIND_TO =
+# MERCHANT_ACCESS_TOKEN and SECRET live in the file below rather than
+# here, because this file is world-readable and those two are not
+# things every local account should be able to read. Keep it that way.
+@inline-secret@ paivana secrets/paivana.secret.conf
diff --git a/debian/paivana-httpd.postinst b/debian/paivana-httpd.postinst
@@ -1,35 +1,52 @@
-#!/bin/bash
+#!/bin/sh
set -e
-if [ -d /run/systemd/system ]; then
- systemctl --system daemon-reload >/dev/null || true
-fi
-if [ "$1" = "remove" ]; then
- if [ -x "/usr/bin/deb-systemd-helper" ]; then
- deb-systemd-helper mask 'paivana-httpd.service' >/dev/null || true
- fi
-fi
-
-if [ "$1" = "purge" ]; then
- if [ -x "/usr/bin/deb-systemd-helper" ]; then
- deb-systemd-helper purge 'paivana-httpd.service' >/dev/null || true
- deb-systemd-helper unmask 'paivana-httpd.service' >/dev/null || true
- fi
-fi
-
-PAIVANA_HOME="/var/lib/paivana/"
-
-. /usr/share/debconf/confmodule
+# paivana-httpd keeps no state of its own -- no database, no cache, no
+# spool -- so it has no home to create. The account used to name
+# /var/lib/paivana/ with --no-create-home, and postrm purged a third
+# path again (/var/lib/paivana/httpd/); none of the three ever existed.
+PAIVANA_HOME="/nonexistent"
+SECRET_CONF="/etc/paivana/secrets/paivana.secret.conf"
case "${1}" in
configure)
- # Creating taler users if needed
- if ! getent passwd paivana-httpd >/dev/null;
- then
- adduser --quiet --system --ingroup www-data --no-create-home --home ${PAIVANA_HOME} paivana-httpd
+ if ! getent passwd paivana-httpd >/dev/null;
+ then
+ adduser --quiet --system --ingroup www-data --no-create-home \
+ --home ${PAIVANA_HOME} paivana-httpd
fi
+ # The secrets file carries the merchant bearer token and the key for
+ # the access-cookie MAC. Either is enough to take the instance over:
+ # the token is authority over its orders and templates, and the key
+ # lets an attacker mint access cookies for any visitor. Neither
+ # belongs in a 0644 file that every local account -- a compromised
+ # www-data process included -- can read. Same treatment the merchant
+ # package gives its own secrets.
+ if ! dpkg-statoverride --list ${SECRET_CONF} >/dev/null 2>&1
+ then
+ dpkg-statoverride \
+ --add \
+ --update \
+ paivana-httpd root 0460 \
+ ${SECRET_CONF}
+ fi
+
+ # Generate the cookie key on first install. paivana-httpd refuses to
+ # start without one, and leaving the operator to invent it invites
+ # either a weak value or a per-start random key that logs everyone out
+ # on every restart. Only ever written when the placeholder is still
+ # in place, so an upgrade never disturbs a configured system -- and
+ # never regenerates, which would invalidate every outstanding cookie.
+ if [ -f ${SECRET_CONF} ] && grep -q '^# SECRET = CHANGE-ME' ${SECRET_CONF};
+ then
+ SECRET="$(head -c 32 /dev/urandom | base64 | tr -d '\n')"
+ sed -i "s|^# SECRET = CHANGE-ME|SECRET = ${SECRET}|" ${SECRET_CONF}
+ SECRET=""
+ echo "paivana-httpd: generated a random SECRET in ${SECRET_CONF}." >&2
+ echo "paivana-httpd: copy it to every other host serving this site." >&2
+ fi
;;
abort-upgrade | abort-remove | abort-deconfigure) ;;
diff --git a/debian/paivana-httpd.postrm b/debian/paivana-httpd.postrm
@@ -2,14 +2,18 @@
set -e
-if [ -f /usr/share/debconf/confmodule ]; then
- . /usr/share/debconf/confmodule
-fi
+SECRET_CONF="/etc/paivana/secrets/paivana.secret.conf"
case "${1}" in
purge)
- rm -rf /var/lib/paivana/httpd/
- rm -f /etc/paivana/override.conf
+ # Added by postinst; dpkg does not clean up statoverrides itself, and
+ # leaving one behind makes a later reinstall inherit a mode nothing
+ # in the package accounts for.
+ if dpkg-statoverride --list ${SECRET_CONF} >/dev/null 2>&1
+ then
+ dpkg-statoverride --remove ${SECRET_CONF}
+ fi
+ rm -f ${SECRET_CONF}
deluser --system --quiet paivana-httpd || true
;;
remove | upgrade | failed-upgrade | abort-install | abort-upgrade | disappear)
diff --git a/debian/paivana-httpd.service b/debian/paivana-httpd.service
@@ -31,6 +31,11 @@ RestartMaxDelaySec=300s
# unparseable TRUSTED_PROXIES only fills the journal.
RestartPreventExitStatus=6 9
+# Recycle hourly. This is only tolerable because `SECRET' is now
+# mandatory (paivana-httpd exits 6 without it) and the package
+# generates one: with a per-start random key, every restart would
+# invalidate every access cookie, so an hourly restart meant a customer
+# paying at 10:59 was shown the paywall again at 11:01.
RuntimeMaxSec=3600s
# -f: we are served over a Unix socket by nginx/Apache (see the
# shipped site configs), so the client address has to come from the
@@ -43,9 +48,30 @@ ExecStart=/usr/bin/paivana-httpd -c /etc/paivana/paivana.conf -f -L INFO
StandardOutput=journal
StandardError=journal
+
+# Hardening. paivana-httpd needs a listening socket handed to it,
+# outbound TCP to the merchant backend and the origin, and read access
+# to its own configuration -- nothing else. DynamicUser is deliberately
+# NOT used: paivana-httpd.socket names SocketUser=paivana-httpd, which
+# needs a stable account.
PrivateTmp=yes
PrivateDevices=yes
-ProtectSystem=full
+ProtectSystem=strict
+ProtectHome=yes
+ProtectKernelTunables=yes
+ProtectKernelModules=yes
+ProtectControlGroups=yes
+NoNewPrivileges=yes
+RestrictSUIDSGID=yes
+RestrictNamespaces=yes
+LockPersonality=yes
+MemoryDenyWriteExecute=yes
+RestrictRealtime=yes
+RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
+CapabilityBoundingSet=
+SystemCallArchitectures=native
+SystemCallFilter=@system-service
+SystemCallErrorNumber=EPERM
[Install]
WantedBy=multi-user.target
diff --git a/debian/rules b/debian/rules
@@ -15,7 +15,12 @@ override_dh_auto_configure-indep:
override_dh_auto_build-indep:
override_dh_auto_test:
- # Disabling test suite, incomplete
+ # The reverse_proxy suite needs a completed `make install' -- it
+ # resolves paivana-httpd and its templates through the prefix -- and
+ # starts listeners, neither of which belongs in a build chroot. The
+ # two unit tests have no such requirement, so run those rather than
+ # nothing at all.
+ dh_auto_test -- client_address cookie_header
override_dh_auto_install-arch:
# paivana-httpd is the only binary package, so dh_auto_install would
diff --git a/src/backend/paivana-httpd.c b/src/backend/paivana-httpd.c
@@ -624,9 +624,30 @@ run (void *cls,
"SECRET",
&secret))
{
- GNUNET_log_config_missing (GNUNET_ERROR_TYPE_WARNING,
- "paivana",
- "SECRET");
+ if (! PH_no_check)
+ {
+ /* The key is the only input to the access-cookie MAC that the
+ client does not supply, so a fresh random one invalidates
+ every access anyone has paid for. That used to be a warning
+ and a random key, which is survivable only if restarts are
+ rare -- and the shipped unit restarts hourly (RuntimeMaxSec),
+ so a customer paying at 10:59 was shown the paywall again at
+ 11:01. Refuse to start instead: the packaging generates one
+ at install time, and an operator configuring by hand needs to
+ be told rather than silently sold a paywall that forgets. `-n'
+ mints no cookies at all and so needs no key. */
+ GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
+ "paivana",
+ "SECRET");
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Refusing to start without `SECRET': every restart"
+ " would invalidate all access already paid for."
+ " Generate one with `gpg --gen-random 0 32 | base64'"
+ " (or pass -n to serve without a paywall).\n");
+ PH_global_ret = EXIT_NOTCONFIGURED;
+ GNUNET_SCHEDULER_shutdown ();
+ return;
+ }
GNUNET_CRYPTO_random_block (&paivana_secret,
sizeof (paivana_secret));
}