Configuration - phax/peppol-cli GitHub Wiki

Configuration

Status: proposed. The format question is an open decision — see Open Questions #2 — but the resolution mechanism below is a concrete plan built on ph-config, which has been checked against its source.

Requirements

  1. Zero configuration for read-only use: peppol smp lookup must work on a fresh install with nothing but --network.
  2. Named profiles, so an operator can keep test and production, or several customers, side by side.
  3. A documented precedence chain, with a way to ask the tool which value won and where it came from.
  4. Secrets separable from the rest, so the non-secret part can be committed to a repository.
  5. Environment variable overrides for every setting, for CI use.

Does ph-config suffice?

Mostly yes — it is the right base and should be reused rather than replaced. The following is verified against ph-commons/ph-config.

What ph-config already provides:

Capability Detail (verified)
Layered sources with numeric priorities EConfigSourceType: SYSTEM_PROPERTY = 400, ENVIRONMENT_VARIABLE = 300, RESOURCE = 200, APPLICATION = 100.
Automatic environment variable mapping EnvVarHelper.getUnifiedSysEnvName upper-cases the key and replaces every character that is not valid in an environment variable name with _. So peppol.cli.network is overridable as PEPPOL_CLI_NETWORK with no extra code.
Explicit external config file The config.file system property, added as a FileSystemResource at priority 200 (overridable via config.file.priority). Also config.resource / config.resources for classpath and config.url for a remote resource.
Built-in file name conventions private-application.properties at priority 190, application.properties at 180, reference.properties at 1 — the last being the natural home for our shipped defaults.
Pluggable file formats ConfigurationSourceResourceTypeRegistry resolves by file extension; .properties is built in, and .json is added by the ph-config-json module via an SPI registrar.
Key prefix views ConfigSubset exposes a prefixed subset of keys as an IConfig — exactly what profile handling needs.
Renamed/deprecated keys ConfigWithFallback plus IConfigKeyOutdatedNotifier handle a key being renamed while still honouring the old name with a warning.

What ph-config does not provide, and the CLI must add:

Gap Plan
Per-user config file discovery ph-config looks at the classpath, the working directory and whatever config.file points at. It has no notion of XDG or %APPDATA%. The CLI resolves the path itself (rules below) and feeds the result in — though a concrete design for solving this upstream in ph-config is proposed in Library Gaps, which would serve every CLI in the ecosystem.
--config <file> as a command line option Must be turned into the config.file system property, or into an explicitly constructed source.
Secret separation and permission checks No concept of a "secret" source, no file-permission warning, no redaction in diagnostics. All CLI responsibilities.
Provenance reporting for config show Values carry a priority, but presenting "this value came from this file" is our own output code. > TODO(verify): how much provenance ConfiguredValue / IConfigurationSource expose, to see how much of config show comes for free
Typed profile structures It is a flat key/value store, so profiles are expressed as a key naming convention, not as nested objects.

Implementation gotcha worth recording now: ConfigFactory holds its default instance in a private static final field initialised at class load, and it reads config.file from system properties at that moment. So --config has to be translated into the system property before anything touches ConfigFactory, or the CLI has to build its own MultiConfigurationValueProvider instead of using the default one. The second option is cleaner for a CLI and is the recommendation.

Resolution chain

Highest priority first, mapping onto ph-config priorities:

# Source Priority Notes
1 Command line option n/a Handled by picocli, applied on top of the resolved config.
2 System property 400 -Dpeppol.cli.network=prod.
3 Environment variable 300 PEPPOL_CLI_NETWORK=prod, mapped automatically.
4 File from --config <file> 250 Explicit file wins over discovered ones.
5 Per-user secret file 220 Restrictive permissions; see below.
6 Per-user config file 210 The discovered location.
7 Project-local config file 205 ./.peppol-cli.properties in the working directory, for repository-committed settings. > TODO(decide): is a project-local file wanted, or is it a footgun in shared checkouts?
8 Shipped defaults 1 reference.properties inside the JAR — network profile definitions and the like.

peppol config show prints every effective key, its value and its source, with secrets masked.

File location

Discovery order per platform, first existing file wins (and peppol config init creates the first entry):

Platform Config Secrets
Linux / BSD $XDG_CONFIG_HOME/peppol-cli/config.properties, falling back to ~/.config/peppol-cli/config.properties same directory, credentials.properties, mode 0600
macOS ~/.config/peppol-cli/config.properties ~/.config/peppol-cli/credentials.properties
Windows %APPDATA%\peppol-cli\config.properties %APPDATA%\peppol-cli\credentials.properties

Cache directory: $XDG_CACHE_HOME/peppol-cli or ~/.cache/peppol-cli on Linux, ~/Library/Caches/peppol-cli on macOS, %LOCALAPPDATA%\peppol-cli\cache on Windows — overridable with peppol.cli.cache.dir.

The cache directory also holds the optional Mozilla NSS trust list override, in <cache-dir>/nss/. Each list is named mozilla-nss-root-certs-<version>-<yyyy-mm-dd>.p12 — the version and date in the name are what the CLI reads — accompanied by <same name>.p12.sha256. The directory is scanned, the highest version wins, and a list is used only when its version is strictly newer than the one bundled in ph-nss; see the TLS rows in the Feature Catalogue. The CLI never downloads or builds such a list — it is provisioned out of band. Note this is the one cache entry that is a trust input rather than a convenience copy, which is why it is version-gated, hash-checked, and why a hash mismatch stops the command instead of falling back.

> TODO(decide): macOS convention. Apple's own location is ~/Library/Application Support, but developer tools on macOS overwhelmingly use ~/.config. The table proposes ~/.config for consistency with Linux; confirm.

Format

Two candidates, both already supported by ph-config, so neither adds a new dependency of its own:

Format For Against
.properties (proposed) Built into ph-config with no extra module; supports comments, which a hand-edited file needs; the ecosystem's existing convention; trivially diffable. Flat, so nesting is expressed through dotted key names; no typed lists.
.json (via ph-config-json) Real nesting and types. No comments — which rules it out for a file we want users to read and edit.

TOML and YAML are not supported by ph-config and would mean writing a new IConfigurationSourceResourceTypeRegistrarSPI plus taking on a parser dependency. That cost is not justified by the benefit here.

Proposal: .properties, with dotted keys. Recorded as Open Questions #2 until confirmed.

Contents

Profiles are key prefixes, read through ConfigSubset. peppol.cli.profile selects the active one, --profile overrides it, and the two built-in profiles test and prod come from the shipped reference.properties so that the common case needs no user configuration at all.

Group Keys (proposed)
Active selection peppol.cli.profile, peppol.cli.network
Network profile peppol.cli.network.<net>.sml.zone, .sml.management.url, .directory.url, .truststore.path, .truststore.password. Defaults come from peppol-commons: the SMP trust store (PeppolTrustStores.Config2025.TRUSTSTORE_SMP_PRODUCTION / TRUSTSTORE_SMP_TEST) is used for SMP, SML and Directory connections alike — one current G3/PKCS12 trust store rather than several. These keys exist only to override it.
Own key material peppol.cli.keystore.path, .type, .password, .key.alias, .key.password — used for SML/SMP write operations and TLS client authentication
Truststore override peppol.cli.truststore.path, .type, .password
NSS trust list peppol.cli.tls.nss.file — an explicit path to a Mozilla NSS PKCS#12 trust list. It is held to the same newer-than-bundled test as a discovered list and refused if it fails, so the filename must carry a readable version. Without it the CLI scans <cache-dir>/nss/.
CSR generation peppol.cli.csr.format (default both), peppol.cli.csr.generation (default g3)
Revocation checking peppol.cli.revocation.mode (default crl-before-ocsp, matching CertificateRevocationCheckerDefaults), peppol.cli.revocation.soft.fail (default false), peppol.cli.revocation.cache.enabled (default true), peppol.cli.revocation.cache.duration (default 6h)
phoss-SMP write peppol.cli.phoss.smp.url, .auth.type, .username, .password. > TODO(decide): keep the neutral peppol.cli.smp.* key names, or rename to match the phoss-smp area? Renaming is clearer but the keys also configure nothing else, so it may be needless churn.
Phorm peppol.cli.phorm.url, peppol.cli.phorm.token
Directory peppol.cli.directory.url (defaults from the network profile). Indexing additionally needs a client certificate — reuse peppol.cli.keystore.* rather than introducing a second keystore, unless a separate Directory certificate is genuinely required. > TODO(verify): whether the Directory indexer accepts the same SMP certificate used for SML operations.
Cache peppol.cli.cache.dir, peppol.cli.cache.enabled, peppol.cli.codelist.ttl
HTTP peppol.cli.http.connect.timeout (default 10s), peppol.cli.http.read.timeout (default 2m), peppol.cli.http.retries
DNS peppol.cli.dns.timeout (default 5s, matching DNSConfig.DEFAULT_RESOLVER_TIMEOUT), peppol.cli.dns.servers (override the system resolvers), peppol.cli.dns.cache.enabled, peppol.cli.dns.dnssec.enabled (default true — deliberately overriding the library default of false, see ADR-009), peppol.cli.dns.dnssec.key.cache.ttl (library default 1 hour, which matches the observed 36–60 minute DNSKEY TTLs on the chain)
Bulk concurrency peppol.cli.bulk.parallel.per.smp (default 2; values above 4 warn), peppol.cli.bulk.parallel.dns, peppol.cli.bulk.parallel.directory. Separate keys because the three target different things: per-participant SMP infrastructure, the caller's own resolver, and one shared Directory service.
Proxy peppol.cli.proxy.host, .port, .username, .password, .nonproxyhosts
Output peppol.cli.output.format, peppol.cli.output.color
Update check updatecheck.enabled (default true), updatecheck.url (default https://<host>/peppol-cli/latest.json). Disabled also by PEPPOL_NO_UPDATE_CHECK=1 or DO_NOT_TRACK=1 in the environment. See Requirements — UPD-02 and UPD-09.

Notes grounded in verified source:

  • The Phorm keys mirror the existing precedent in phoss-ap, which uses verification.phorm.url and verification.phorm.token for the same two values — worth staying consistent with in spirit.

  • Phorm authenticates with a shared secret sent in the X-Token HTTP header, so peppol.cli.phorm.token is a plain secret and belongs in the credentials file.

  • The built-in network profiles should not be hard-coded here at all. peppol-commons already models them: EPeppolNetwork.TEST and EPeppolNetwork.PRODUCTION each pair a Directory URL with an ESML entry, and ESML carries the DNS zone, the management service URL and a "requires client certificate" flag. --network test|prod should map straight onto EPeppolNetwork, so the values track the library rather than this wiki. The values below are quoted from ESML and EPeppolNetwork in peppol-commons for review only:

    Network SML DNS zone SML management URL Directory URL
    test participant.sml.test.tech.peppol.org. https://api.sml.test.tech.peppol.org/edelivery-sml https://test-directory.peppol.eu
    prod participant.sml.prod.tech.peppol.org. https://api.sml.prod.tech.peppol.org/edelivery-sml https://directory.peppol.eu

    > TODO(verify): the ESML source comments mark both entries as "valid from March 19th, 2026". Confirm these are the currently correct values and that no transition period or legacy zone still needs to be supported. > TODO(verify): ESML also has a DEVELOPMENT_LOCAL entry (zone smj.peppolcentral.org., http://localhost:8080). Should the CLI expose a third built-in profile for it, or is a user-defined profile the right way?

  • The user-defined profile keys stay in place for private test environments, since ESML's companion SMLInfo is builder-based and supports custom SMLs.

  • Note a real collision — three libraries bring their own ph-config configuration:

    Library Its own configuration Keys
    smp-client-base SMPClientConfiguration — reads application.properties with an smp-client.properties fallback trust store type/path/password
    peppol-directory-client 1.0.0 PDClientConfiguration pdclient.keystore.path / .type / .password / .key.alias / .key.password, plus HTTP timeouts and proxy
    phorm (server side) its own properties phorm.api.requiredtoken etc. — not the CLI's concern, but the token must match

    Since all of them sit on ph-config, they will happily read the same application.properties the CLI might use, which means a key set in one place could silently configure something else. The CLI should configure each client programmatically from its own peppol.cli.* keys rather than letting these mechanisms discover files on their own — one visible configuration surface, one peppol config show. > TODO(decide): confirm the programmatic approach, and check whether each client actually allows full programmatic configuration or falls back to its own config in places. Note peppol-directory-client 1.0.0 ships an example-config/application.properties, which is the reference for the keys it reads.

  • > TODO(decide): which authentication schemes the SMP write commands must support. Different SMP implementations differ here, and it drives the .auth.type key.

Secrets

Rules, all of them deliberate:

  1. Two files. Non-secret settings in config.properties, secrets in credentials.properties. The first can be committed or shared; the second must not be.
  2. Environment variables override everything, and are the recommended way to supply secrets in CI — nothing needs to be written to disk. PEPPOL_CLI_PHORM_TOKEN and PEPPOL_CLI_KEYSTORE_PASSWORD come for free from ph-config's env var mapping.
  3. Permission check. On a POSIX platform, if the secrets file is group- or world-readable, the tool prints a warning naming the file and the chmod 600 fix. peppol config init creates it with 0600. > TODO(decide): warn only, or refuse to read a world-readable secrets file?
  4. No plaintext secrets in any example, ever. Documentation uses either an environment variable reference or an obvious placeholder. No example in this wiki contains a usable password.
  5. Redaction. --verbose, --debug and config show mask every value whose key matches a password/token/secret pattern, and --debug must not print the X-Token or Authorization headers.
  6. No keyring integration in the first version. > TODO(decide): is macOS Keychain / libsecret / Windows Credential Manager support wanted later?
  7. Passwords are never accepted as command line arguments — they are visible in the process list. Environment variable, config file, or an interactive prompt on a TTY.

Example configuration

~/.config/peppol-cli/config.properties — non-secret, safe to commit:

# ---------------------------------------------------------------------------
# peppol-cli configuration
# Every key below can be overridden by an environment variable: upper-case the
# key and replace every non-alphanumeric character with an underscore, e.g.
#   peppol.cli.network  ->  PEPPOL_CLI_NETWORK
# Precedence (highest first): command line option, system property,
# environment variable, --config file, credentials file, this file,
# shipped defaults.
# ---------------------------------------------------------------------------

# Which network to use when --network is not given: test or prod.
# The built-in default is prod; set this to test to flip it for this machine.
peppol.cli.network = prod

# Default output format for every command: text or json
peppol.cli.output.format = text

# --- Update check -----------------------------------------------------------
# Checks at most once per 24h, runs concurrently with the command, 2s timeout,
# fails silently, and writes only to stderr when stderr is a TTY. The request
# carries nothing but the User-Agent - no identifiers of any kind.
# Also disabled by PEPPOL_NO_UPDATE_CHECK=1 or DO_NOT_TRACK=1.
updatecheck.enabled = true
# updatecheck.url = https://<host>/peppol-cli/latest.json

# --- Own key material -------------------------------------------------------
# Required only for SML/SMP write operations and TLS client authentication.
# The password belongs in credentials.properties, NOT here.
peppol.cli.keystore.path = /home/me/peppol/ap-keystore.p12
peppol.cli.keystore.type = PKCS12
peppol.cli.keystore.key.alias = my-ap-key

# --- SMP management ---------------------------------------------------------
# The SMP that the "peppol phoss-smp ..." administration commands target.
# Note: SMP write is not standardised, so this is the phoss-SMP write API.
peppol.cli.smp.url = https://smp.example.org
peppol.cli.smp.auth.type = basic
peppol.cli.smp.username = smp-admin
# password -> credentials.properties

# --- Phorm validation service ----------------------------------------------
# Documents sent here leave your machine. Point this at an instance you trust.
# Authentication is a shared secret sent in the X-Token header.
peppol.cli.phorm.url = https://phorm.example.org
# token -> credentials.properties

# --- Caching ----------------------------------------------------------------
peppol.cli.cache.enabled = true
# peppol.cli.cache.dir = /home/me/.cache/peppol-cli

# --- HTTP -------------------------------------------------------------------
# Time to establish a connection - a host that is down should fail fast
peppol.cli.http.connect.timeout = 10s
# Time to wait for a response once connected - generous, because a large
# document validation or a slow SMP can legitimately take a while
peppol.cli.http.read.timeout = 2m

# --- DNS ---------------------------------------------------------------------
# Overall budget for a DNS lookup. Note this is divided across the resolvers
# that are tried, so each single resolver gets roughly half of it.
peppol.cli.dns.timeout = 5s
# DNSSEC validation of the discovery chain. ON by default: the Peppol SML
# zones are signed, so this works - but your resolver must forward DNSSEC
# records, otherwise every lookup fails. Set to false, or pass --no-dnssec,
# if your resolver cannot do it.
peppol.cli.dns.dnssec.enabled = true
# Override the system resolvers, e.g. to query an authoritative server directly
# peppol.cli.dns.servers = 8.8.8.8,1.1.1.1
peppol.cli.http.retries = 2

# --- Bulk concurrency ------------------------------------------------------
# Concurrent requests PER resolved SMP, not in total. Values above 4 are
# accepted but produce a warning - an SMP operator cannot tell aggressive
# tooling from a small attack.
peppol.cli.bulk.parallel.per.smp = 2

# --- Proxy ------------------------------------------------------------------
# peppol.cli.proxy.host = proxy.example.org
# peppol.cli.proxy.port = 3128
# peppol.cli.proxy.nonproxyhosts = localhost,127.0.0.1,*.internal.example.org

# --- Network profile overrides ---------------------------------------------
# The built-in "test" and "prod" profiles come from peppol-commons
# (EPeppolNetwork + ESML) and need no configuration. Override these keys only
# for a private test environment.
# peppol.cli.network.mine.sml.zone            = participant.sml.example.org.
# peppol.cli.network.mine.sml.management.url  = https://sml.example.org/edelivery-sml
# peppol.cli.network.mine.directory.url       = https://directory.example.org

~/.config/peppol-cli/credentials.properties — secrets only, mode 0600:

# ---------------------------------------------------------------------------
# Secrets. chmod 600 this file. Do not commit it.
# In CI, set the equivalent environment variables instead and do not ship this
# file at all:
#   PEPPOL_CLI_KEYSTORE_PASSWORD, PEPPOL_CLI_SMP_PASSWORD, PEPPOL_CLI_PHORM_TOKEN
# ---------------------------------------------------------------------------

peppol.cli.keystore.password = <your keystore password>
peppol.cli.keystore.key.password = <your key password>
peppol.cli.smp.password = <your SMP API password>
peppol.cli.phorm.token = <your Phorm X-Token value>

Related commands

Command Purpose
peppol config init Interactively create the config and credentials files with correct permissions.
peppol config show Print every effective setting with its source; secrets masked.
peppol config get <key> / set <key> <value> Read or write a single key without an editor.
peppol config path Print the resolved file locations, useful when discovery picked an unexpected file.
peppol config validate Check the config for unknown keys, unreadable keystores and bad permissions, and probe whether the configured resolver returns DNSSEC-authenticated data. The resolver probe is the one network request any config command makes, and it happens only here — never implicitly.
⚠️ **GitHub.com Fallback** ⚠️