Configuration - phax/peppol-cli GitHub Wiki
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.
- Zero configuration for read-only use:
peppol smp lookupmust work on a fresh install with nothing but--network. - Named profiles, so an operator can keep test and production, or several customers, side by side.
- A documented precedence chain, with a way to ask the tool which value won and where it came from.
- Secrets separable from the rest, so the non-secret part can be committed to a repository.
- Environment variable overrides for every setting, for CI use.
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.
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.
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.
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.
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 usesverification.phorm.urlandverification.phorm.tokenfor the same two values — worth staying consistent with in spirit. -
Phorm authenticates with a shared secret sent in the
X-TokenHTTP header, sopeppol.cli.phorm.tokenis a plain secret and belongs in the credentials file. -
The built-in network profiles should not be hard-coded here at all.
peppol-commonsalready models them:EPeppolNetwork.TESTandEPeppolNetwork.PRODUCTIONeach pair a Directory URL with anESMLentry, andESMLcarries the DNS zone, the management service URL and a "requires client certificate" flag.--network test|prodshould map straight ontoEPeppolNetwork, so the values track the library rather than this wiki. The values below are quoted fromESMLandEPeppolNetworkin peppol-commons for review only:Network SML DNS zone SML management URL Directory URL testparticipant.sml.test.tech.peppol.org.https://api.sml.test.tech.peppol.org/edelivery-smlhttps://test-directory.peppol.euprodparticipant.sml.prod.tech.peppol.org.https://api.sml.prod.tech.peppol.org/edelivery-smlhttps://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 companionSMLInfois builder-based and supports custom SMLs. -
Note a real collision — three libraries bring their own
ph-configconfiguration:Library Its own configuration Keys smp-client-baseSMPClientConfiguration— readsapplication.propertieswith ansmp-client.propertiesfallbacktrust store type/path/password peppol-directory-client1.0.0PDClientConfigurationpdclient.keystore.path/.type/.password/.key.alias/.key.password, plus HTTP timeouts and proxyphorm(server side)its own properties phorm.api.requiredtokenetc. — not the CLI's concern, but the token must matchSince all of them sit on
ph-config, they will happily read the sameapplication.propertiesthe 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 ownpeppol.cli.*keys rather than letting these mechanisms discover files on their own — one visible configuration surface, onepeppol 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.Notepeppol-directory-client1.0.0 ships anexample-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.
Rules, all of them deliberate:
-
Two files. Non-secret settings in
config.properties, secrets incredentials.properties. The first can be committed or shared; the second must not be. -
Environment variables override everything, and are the recommended way to supply secrets in CI — nothing
needs to be written to disk.
PEPPOL_CLI_PHORM_TOKENandPEPPOL_CLI_KEYSTORE_PASSWORDcome for free fromph-config's env var mapping. -
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 600fix.peppol config initcreates it with0600.> TODO(decide): warn only, or refuse to read a world-readable secrets file? - 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.
-
Redaction.
--verbose,--debugandconfig showmask every value whose key matches a password/token/secret pattern, and--debugmust not print theX-TokenorAuthorizationheaders. -
No keyring integration in the first version.
> TODO(decide): is macOS Keychain / libsecret / Windows Credential Manager support wanted later? - 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.
~/.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>| 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. |