70.1.48. pepsi-helper-token-refresh

Refresh OAuth access tokens for the smarthost relay stage

Manual section:

1

70.1.48.1.1. Name

pepsi-helper-token-refresh - keep the smarthost SASL OAuth (XOAUTH2 / OAUTHBEARER) access tokens current.

70.1.48.1.2. Synopsis

pepsi-helper-token-refresh [GLOBAL-OPTIONS]

pepsi-helper-token-refresh [GLOBAL-OPTIONS] –once

70.1.48.1.3. Description

When a smarthost MTA is configured with AUTH = oauth (see pepsi-stage-relay-to-smarthost(1)), the relay stage authenticates with a SASL bearer token it reads — fresh on every delivery — from that MTA’s TOKEN_FILE. OAuth access tokens are short-lived (typically one hour), so something must replace the file’s contents before each token expires.

pepsi-helper-token-refresh is that something. For every [pepsi-stage-relay-to-smarthost-mta-<name>] section with AUTH = oauth it looks for a matching secret section [pepsi-helper-token-refresh-<name>] holding the OAuth client credentials, requests a fresh access token from the provider’s token endpoint, and writes it atomically to that MTA’s TOKEN_FILE. An OAuth MTA with no readable matching secret section is skipped with a warning — its token is assumed to be supplied by other means.

It is one implementation of an external refresh contract; a site may substitute its own equivalent (see the Extending the Pipeline chapter of the Pepsi manual). It is deliberately not part of pepsi.target and must be enabled explicitly.

70.1.48.1.4. Security model

The service runs as the dedicated unprivileged account pepsi-helper-token-refresh and is the only reader of the OAuth client secrets:

  • The per-target secret sections live in a separate file included into the main configuration with the taler @inline-secret@ directive (see pepsi.conf(5)). That file is readable only by this account, so the CLIENT_SECRET and REFRESH_TOKEN are never exposed to the relay stage (which runs as pepsi) or to other users. @inline-secret@ is silently skipped for a reader that cannot open the file, so the relay stage parses the same pepsi.conf without error.

  • The access-token files are written into a directory that is SGID the pepsi-token group (default /var/pepsi/tokens), so each token file inherits that group and is created mode 0640. The smarthost relay stage binary is installed SGID pepsi-token, giving the dispatcher’s pepsi worker just enough group membership to read the token — and nothing else.

  • Rotated refresh tokens (RFC 6749 §10.4; e.g. Microsoft) are persisted to a private state directory (default /var/pepsi/token-refresh, mode 0700, never group-readable) and take precedence over the configured REFRESH_TOKEN on the next run.

Started as root, the process first drops to the pepsi-helper-token-refresh account; it refuses to run as root. Credentials are sent only over HTTPS.

70.1.48.1.5. Modes

With no flag, pepsi-helper-token-refresh runs as a long-lived service. Each token is refreshed independently and proactively, ahead of its stated expiry by REFRESH_MARGIN. A target whose refresh fails is retried with exponential back-off while its previous token file is left in place, so a transient token-endpoint outage never blanks a working token. It runs until it receives SIGINT or SIGTERM.

With –once, every configured target is refreshed a single time and the process exits — the form for a manual run, a systemd timer, or cron. The exit status is non-zero if any target failed (after all were attempted).

70.1.48.1.6. Configuration

The targets are derived from the smarthost relay configuration; each pairs an MTA section with a secret section. See pepsi.conf(5) for the full option reference. In outline:

# main pepsi.conf (world-readable):
[pepsi-stage-relay-to-smarthost-mta-gmail]
HOST = smtp.gmail.com
PORT = 587
MODE = starttls
AUTH = oauth
USERNAME = me@example.com
TOKEN_FILE = /var/pepsi/tokens/gmail
CATCH_ALL = YES

@inline-secret@ pepsi-helper-token-refresh-gmail /etc/pepsi/secrets/token-refresh.conf

# /etc/pepsi/secrets/token-refresh.conf (readable only by the refresher):
[pepsi-helper-token-refresh-gmail]
TOKEN_ENDPOINT = https://oauth2.googleapis.com/token
GRANT = refresh_token
CLIENT_ID = ...
CLIENT_SECRET = ...
REFRESH_TOKEN = ...
# REFRESH_MARGIN = 5 m

For an app-only Microsoft 365 target use GRANT = client_credentials with SCOPE instead of a REFRESH_TOKEN.

70.1.48.1.7. Options

–once

Refresh every configured target once and exit, instead of running as a service that refreshes ahead of expiry.

70.1.48.1.8. Global Options

These options may appear before or after the other flags.

-c FILE, –config FILE

Read the configuration from FILE instead of searching the default locations.

-L LOGLEVEL, –log LOGLEVEL

Set the logging verbosity. LOGLEVEL is one of error, warn, info, debug or trace (default: info).

-v, –verbose

Show log messages from all sources, including third-party libraries.

-h, –help

Print a usage summary and exit.

-V, –version

Print the version and exit.

70.1.48.1.9. Exit Status

0

Successful completion (a --once run refreshed every target, or the service shut down cleanly on a signal).

1

An error occurred: a malformed configuration, a secret section missing a required option, at least one target failing in --once mode, or an attempt to run as root without the pepsi-helper-token-refresh account.

70.1.48.1.10. Examples

Run as a service (the typical deployment):

systemctl enable --now pepsi-helper-token-refresh.service

Refresh every token once, by hand:

pepsi-helper-token-refresh -c /etc/pepsi/pepsi.conf --once

70.1.48.1.11. See Also

pepsi-stage-relay-to-smarthost(1), pepsi-setup(1), pepsi.conf(5)

70.1.48.1.12. Bugs

Report bugs to the Pepsi issue tracker.