NVMe-oF Connection Configuration

August 28, 2026 · View on GitHub

This is the host-side configuration that specifies which NVMe over Fabrics (NVMe-oF) controllers a host should connect to, and with what parameters. The configuration file uses the INI format.

The format is parsed by libnvme, so every cooperating consumer reads one file format through one parser. nvme connect-all / nvme config in nvme-cli and the nvme-discoverd daemon are complementary tools that work together from this one shared file. nvme-stas is a separate orchestrator: it reads the same format through the same parser (via the Python bindings), but from its own file, so that the two do not act on each other's configuration. The format shares the transport-ID identity model (src/nvme/tid.h) of the two host-wide coordination files, the ownership registry and the exclusion list — which, unlike a connection configuration, are genuinely one per host.

This document describes the configuration format and intent. Once the parser lands, the header kdoc and the nvme-* man pages are the authoritative reference; the code is the source of truth.

Design goals

  • One shared format for every libnvme consumer — nvme-cli, nvme-discoverd, and (via the Python bindings) nvme-stas all read the same format through the same parser. The nvme-cli tools and nvme-discoverd share one file; nvme-stas keeps its own.
  • Human-editable, systemd-style[Section] / key = value, comments, and drop-ins, the same idiom as systemd's own .conf files.
  • Deterministic parsing and precedence — the same input always resolves to the same connections, with one well-defined merge order.
  • Validated before connecting — a dry-run entry point catches a bad configuration before it reaches the kernel.
  • Extensible without breaking existing configurations — unknown keys are ignored rather than rejected, so new keys can be added later without breaking older parsers.
  • Both the common case and advanced deployments — a small host needs only the main file; a host presenting several distinct identities gets one drop-in per identity.
  • No json-c dependency in libnvme — the previous format needed an optional third-party library to parse; this one does not.

Single vs. multiple host personalities

One host personality (the common case). The host has a single identity — one Host NVMe Qualified Name (NQN) and one Host Identifier — and connects to its fabrics under that identity. This is what most systems need, and the whole configuration fits in the single nvme-fabrics.conf main file: the per-type defaults plus the discovery controllers and subsystems to connect. The [Host] section is optional here — you may include one to state the identity explicitly, but if it is omitted the connections fall back to the default (legacy) identity in /etc/nvme/hostnqn and /etc/nvme/hostid.

Multiple host personalities. A single host can deliberately present different identities — different (Host NQN, Host Identifier) pairs — to different fabrics, as though it were several independent hosts. Each such identity is a personality. Each one gets its own file under nvme-fabrics.conf.d/, opening with its own [Host] block; keeping one persona per file is what stops them from getting tangled.

This distinction matters once authentication or Transport Layer Security (TLS) enter the picture. Authentication credentials, such as a TLS Pre-Shared Key (PSK) or a KX-HMAC-CHAP (key-exchange HMAC-CHAP) secret, are associated with a host identity and an NVMe subsystem, not with the machine itself.

Separate host identities let different credential sets be configured for different NVMe-oF deployments: a credential configured for one host identity cannot be used by another, and credentials for a single deployment can be updated or revoked by editing only that identity's configuration.

This matters most for hosts that connect to multiple independently administered or multi-tenant NVMe-oF deployments, each with its own access control policy and credentials.

The identity rules that make personalities valid (why each needs a distinct Host NQN and Host Identifier) are in Host identity and multiple personalities; the file mechanics follow next.

File layout

A configuration is a main file plus an optional drop-in directory, the same foo.conf + foo.conf.d/ pattern systemd uses. The default main file is /etc/nvme/nvme-fabrics.conf, and its drop-in directory is the main file's name with .d appended: /etc/nvme/nvme-fabrics.conf.d/.

/etc/nvme/
    nvme-fabrics.conf       # main file: per-type defaults (+ optionally connections)
    nvme-fabrics.conf.d/    # optl. drop-ins, 1 persona per file (or none = default)
        black-fabric.conf
        white-fabric.conf

The drop-in directory name is derived mechanically from whatever file you point at, so the convention generalizes to any base name. If bob keeps his own config, it is nvme-for-bob.conf + nvme-for-bob.conf.d/*.conf.

Both parts are optional, read independently:

  • The main file may hold a complete configuration on its own — the per-type defaults and connection sections — which is all most single-personality hosts need; nvme-fabrics.conf.d/ need not exist at all.
  • An empty or absent main file with a populated .conf.d/ is fully valid: read the main file if it exists, then read <file>.d/*.conf if that directory exists. This is the layout for a host that wants only explicit, non-default personalities and no default config.
  • If neither exists, the configuration is empty (no connections) — a gentle no-op, not an error.

A consumer daemon may keep its own configuration file for daemon-specific behavior — for example nvme-discoverd's nvme-discoverd.conf carries its discovery-source toggles (nbft, zeroconf, …) and log level. Those keys are specific to that consumer and documented by it; they are not part of the shared connectivity format described here.

Pointing at a configuration — --config and the .d/ rule

--config FILE names the main file, and its drop-in directory is always <FILE>.d/. Because the directory is derived from the filename, one option reroots the whole tree coherently and there is never a second path to keep in sync:

  • --config /etc/nvme/nvme-fabrics.conf → reads that file and /etc/nvme/nvme-fabrics.conf.d/.
  • --config /home/bob/nvme-for-bob.conf → reads that file and /home/bob/nvme-for-bob.conf.d/ — rerooted to any base directory for free.
  • Omitting --config → the default /etc/nvme/nvme-fabrics.conf (+ its .d/).
  • --config /etc/nvme/config.json (a legacy JSON file, recognized by its .json extension) → a hard error naming nvme config-convert; libnvme's reader never reads JSON.

Because the config is reached by an explicit path (--config FILE, or libnvmf_config_read(ctx, file) at the API level), unit testing is straightforward: point at a throwaway file under /tmp (and its derived .d/) instead of the default /etc/nvme/nvme-fabrics.conf.

Top-level config file (nvme-fabrics.conf)

[Discovery Controller Defaults] and [I/O Controller Defaults] hold the default connection parameters (ctrl-loss-tmo, reconnect-delay, keep-alive-tmo, digests, queue counts, …) for connections of that type, unless something more specific overrides them (see Precedence). There is deliberately no third, type-agnostic defaults section: some parameters do want a different default per controller class — most clearly the keep-alive timeout: keep-alive-tmo = 30 for a Discovery Controller, keep-alive-tmo = 5 (the kernel default) for an I/O Controller — and a value that happens to apply to both is simply written in both sections. That's a deliberate choice, not an oversight: checking one *Defaults section shows the whole effective default set for that class, rather than requiring a reader to also mentally merge in a separate global scope; the occasional duplicated line is a small price for that directness.

Two more scopes are available for controllers that are discovered, not configured — it is up to the orchestrator (nvme-discoverd, nvme-stas) to apply them when connecting to what it finds:

  • For a controller with no configured origin (e.g. an mDNS-found DC), the top-level file's [Discovery Controller Defaults] / [I/O Controller Defaults] pair.
  • For an I/O Controller or a referral Discovery Controller learned at runtime from a Discovery Log Page (DLP), the scope of the file the discovering DC belongs to — that DC's identity (hostnqn / hostid) and host binding (host-traddr / host-iface), plus the file's defaults for its own type: [I/O Controller Defaults] for a discovered IOC, [Discovery Controller Defaults] for a referral DC, never the discovering DC's own resolved tunables.

A fabric's drop-in therefore supplies defaults for both its configured controllers and the ones an orchestrator discovers through its DCs, each keyed to what it is. (A drop-in may carry its own copy of either *Defaults section; see Precedence for how the overlay works.)

# /etc/nvme/nvme-fabrics.conf — a complete single-personality configuration

[Discovery Controller Defaults]
keep-alive-tmo = 30                  # DC keep-alive default
ctrl-loss-tmo  = 600                 # duplicated in both *Defaults sections

[I/O Controller Defaults]
keep-alive-tmo = 5                   # IOC keep-alive default (kernel default)
ctrl-loss-tmo  = 600                 # duplicated in both *Defaults sections

[Host]
# hostnqn omitted → /etc/nvme/hostnqn
# hostid omitted → /etc/nvme/hostid
hostsymname = lab-host-01

[Discovery Controller]
controller = transport=tcp;traddr=192.168.1.10;trsvcid=8009

[Discovery Controller]
nqn        = nqn.2014-08.org.example:cdc.main
controller = transport=tcp;traddr=192.168.1.11;trsvcid=8009

[Subsystem]
nqn        = nqn.2024-01.com.example:data.vol1
controller = transport=tcp;traddr=192.168.1.20;trsvcid=4420

[Subsystem]
nqn           = nqn.2024-01.com.example:data.vol2
ctrl-loss-tmo = 1800            # override the [I/O Controller Defaults] default for this subsystem
controller    = transport=tcp;traddr=192.168.1.21;trsvcid=4420
controller    = transport=tcp;traddr=192.168.1.22;trsvcid=4420

Drop-in config files (nvme-fabrics.conf.d/)

A drop-in typically adds a new host persona — opening with its own [Host] block naming that persona's hostnqn — plus the connections made under that identity. (A drop-in may also omit [Host] entirely, adding more connections under the default persona instead.) It may also carry its own copy of either *Defaults section, each a scoped overlay limited to that drop-in's own connections (see Precedence).

# /etc/nvme/nvme-fabrics.conf.d/prod-fabric.conf
[Host]
hostnqn     = nqn.2014-08.org.nvmexpress:uuid:1111…-A
hostid      = 46ba5037-7ce5-41fa-9452-48477bf00080
hostsymname = Hydra

# This file's own defaults: overlay the top-level ones, for these connections only

[Discovery Controller Defaults]
keep-alive-tmo = 45    # this fabric's DCs are remote — longer than the top-level 30

[I/O Controller Defaults]
reconnect-delay = 20   # slower reconnect for this fabric's IOCs

# --- Discovery Controllers: list as many as needed ---

[Discovery Controller]
nqn        = nqn.2014-08.org.example:cdc.prod
tls        = true
tls-key    = NVMeTLSkey-1:01:…cdc
controller = transport=tcp;traddr=10.0.0.5;trsvcid=8009

[Discovery Controller]
# nqn omitted → the well-known discovery NQN
controller = transport=tcp;traddr=10.0.0.6;trsvcid=8009

# --- I/O Subsystems: list as many as needed ---

[Subsystem]
nqn           = nqn.2024-01.com.example:prod.vol1
tls           = true
tls-key       = NVMeTLSkey-1:01:…vol1     # bound to (this host, this subsysnqn)
ctrl-loss-tmo = 1800                       # overrides the top-level 600 default
controller    = transport=tcp;traddr=10.0.0.9;trsvcid=4420;host-iface=eth0
controller    = transport=tcp;traddr=10.0.0.10;trsvcid=4420;host-iface=eth1

[Subsystem]
nqn           = nqn.2024-01.com.example:prod.vol2
controller    = transport=tcp;traddr=10.0.0.9;trsvcid=4420

Connections

A connection is described by an optional [Host] block plus one or more endpoint sections. By convention [Host] is written first, but its placement carries no meaning — a file's [Host] applies to every endpoint section in that file (see Singleton vs repeatable sections).

  • [Host] — the host identity (hostnqn, hostid, hostsymname, kxchap-secret, …) used for every connection in that file. A file with no [Host] connects as the system default identity (/etc/nvme/hostnqn / hostid) — see Host identity and multiple personalities.
  • [Discovery Controller] — a Discovery Controller to connect to. Its NQN is the section's nqn =; if omitted it defaults to the well-known discovery NQN (nqn.2014-08.org.nvmexpress.discovery).
  • [Subsystem] — an I/O subsystem to connect to, named by its nqn =.

The role is the section name — there is no type = key. Both [Discovery Controller] and [Subsystem] may be repeated — list one section per controller or subsystem you want to connect. Within an endpoint section, each controller = line is one path to that endpoint; repeating it expresses multipath. Per-endpoint keys (tls, tls-key, kxchap-*, ctrl-loss-tmo, …) override the type defaults for that endpoint and all its paths.

controller = address syntax

Each path is a controller = line whose value is a ;-separated key=value list using the nvme connect option names. The addressing keys — transport, traddr, trsvcid, and optionally host-traddr / host-iface for per-path host binding (the two paths above are pinned to eth0 and eth1) — identify the path; this is the same address form nvme-stas uses (and the legacy discovery.conf), minus the subsystem nqn, which now comes from the section header.

Beyond addressing, any nvme connect option may appear on the line as a per-path override (keep-alive-tmo, ctrl-loss-tmo, reconnect-delay, …) — fine-tuning down to a single controller — with the sole exception of the security parameters, which stay on the section (see Precedence).

A URL form (tcp://10.0.0.10:4420) was considered and rejected: it carries only the target triple (no host-traddr / host-iface, which are genuinely per-path in multi-NIC multipath), and it is ambiguous for Fibre Channel (FC), whose World Wide Name (WWN) traddr is full of colons and has no port. One uniform, transport-agnostic form is preferred over a URL-plus-extension hybrid.

Multipath

A [Subsystem] (or [Discovery Controller]) names one endpoint; each controller = line under it is one path = one controller the host instantiates via nvme connect. The subsystem's namespaces are reachable through every path — native NVMe multipath groups all controllers reporting that subsysnqn. The parser accepts any number of controller = lines: 1 is normal (single path), and there is no upper bound and no power-of-two rule (2 is merely the common dual-fabric deployment; 3, 4, … are equally valid). Security stays at the section level across all paths — see Precedence.

Precedence and the merge model

A connection parameter is resolved most-specific-first:

controller = line > endpoint section ([Discovery Controller] / [Subsystem]) > [Host] > type defaults ([Discovery Controller Defaults] / [I/O Controller Defaults]) > kernel default

The type-defaults level is selected by the controller's class — a DC connection draws from [Discovery Controller Defaults], an IOC from [I/O Controller Defaults]. There is no third, type-agnostic rung beneath them: a value that applies to both classes is simply set in both sections (see Top-level config file). So a keep-alive-tmo on a controller = line wins over its [Subsystem] section, which wins over the file's [Host], which wins over the type default, which wins over the kernel built-in.

The one non-obvious rung is [Host] sitting above the type defaults: a keep-alive-tmo in a persona's [Host] beats even the same file's [Discovery Controller Defaults]. That is deliberate — the ladder orders by ownership, not category. The *Defaults sections are baseline plumbing for a whole class of controllers; a key in [Host] was written for that persona specifically, so the persona's value follows its connections.

Drop-ins are additive, with file-scoped defaults. A drop-in mostly adds connections, but may also carry its own copy of [Discovery Controller Defaults] or [I/O Controller Defaults], each a scoped overlay limited to that drop-in's own connections:

  • a key set there overrides the top-level default for that file only;
  • a key left unset inherits from the top-level section;
  • nothing leaks to sibling drop-ins or back to the top level.

The type-default rungs are computed per file this way, so the result never depends on the order drop-ins are read in.

Exception — per-link security stays at the section level. The TLS and KX-HMAC-CHAP parameters (see Security parameters) are not overridable on a controller = line: the PSK and authentication secrets are bound to the (hostnqn, subsysnqn) pair, so they are constant across all paths to an endpoint and belong on [Host] or an endpoint section. Everything else — ctrl-loss-tmo, keep-alive-tmo, digests, queue counts, reconnect-delay, … — can be overridden per path on the controller = line.

Security parameters

A connection's security settings fall into two families — authentication (KX-HMAC-CHAP, which proves the host and controller identities in-band) and encryption (TLS, which protects the data on the wire) — plus one parameter that bridges them. Because they are bound to the host+subsystem relationship, they live on [Host] or an endpoint section, never on a controller = path (the exception above): set on [Host] when a persona uses the same credential for everything, on [Discovery Controller]/[Subsystem] when one endpoint needs its own.

Authentication — KX-HMAC-CHAP

KeyMeaning
kxchap-secretthe host's secret — authenticates the host to the controller
kxchap-ctrl-secretthe controller's secret — adds bidirectional (mutual) authentication

Bidirectional auth for one specific subsystem, rather than the whole persona:

[Subsystem]
nqn                = nqn.2024-01.com.example:secure.vol1
kxchap-secret      = DHHC-1:00:…    # this host, proving itself to the controller
kxchap-ctrl-secret = DHHC-1:00:…    # this controller, proving itself back
controller         = transport=tcp;traddr=10.0.0.9;trsvcid=4420

kxchap-secret/kxchap-ctrl-secret are always literal in this file. Unlike tls-key below, KX-HMAC-CHAP has no keyring-reference form, so the secret sits in the clear in whatever reads nvme-fabrics.conf — typically world-readable, like the rest of /etc/nvme/. Fine for testing, not for production; a real secret-at-rest story for KX-HMAC-CHAP is still an open item.

Encryption — TLS

KeyMeaning
tlsenable TLS for the connection
tls-keythe Pre-Shared Key (PSK): a keyring key id, or the key in interchange format (NVMeTLSkey-1:01:…)
tls-key-identitythe identity string bound to the PSK
keyringthe keyring the PSK is looked up in (default .nvme); usually set once as a [Host] default, or duplicated in both *Defaults sections

Bridging the two

KeyMeaning
concatsecure concatenation — derive the TLS PSK from a successful KX-HMAC-CHAP authentication rather than a pre-provisioned key, chaining the two steps (so it requires the KX-HMAC-CHAP secrets to be set)

The key material lives in the kernel keyring, not in this file: tls-key names a key, it does not store one (except in the interchange-format spelling). libnvme provisions and looks those keys up — nvme gen-tls-key / nvme tls-key, default keyring .nvme — and the kernel's TLS handshake daemon (tlshd, from ktls-utils) reads the PSK from the keyring when the connection is made. This file only says which key and keyring to use and whether TLS and authentication are on.

Reverting a parameter to the kernel default

Because the cascade only ever sets values, a more-specific level needs a way to un-set one an outer level imposed — for instance a drop-in that wants the kernel's built-in ctrl-loss-tmo even though the top-level [I/O Controller Defaults] pinned it to 450. Hardcoding the number is both inconvenient (the admin must dig it out of the kernel) and wrong (it pins a value, not "the default", and silently diverges if the kernel default ever changes).

An empty assignment expresses this, exactly as a systemd drop-in resets a setting:

FormMeaning
key absentinherit through the cascade (normal)
key = (empty)reset → omit the parameter from nvme connect, let the kernel apply its built-in default
key = valueset to that value

An empty value is used rather than a default keyword on purpose: default is unambiguous for a number (it can never be a valid integer) but ambiguous for a string — host-iface = default could not be told apart from binding to an interface literally named default. The empty form carries one meaning regardless of the value's type, and it reads cleanly for strings too (host-iface = means "no interface binding", i.e. the kernel default).

Because keep-alive-tmo is special — nvme connect only applies the 30 s discovery default when it recognizes the well-known discovery NQN — keep-alive-tmo = hands the choice back to the kernel, which for a unique-NQN Discovery Controller means the 5 s I/O default. That is exactly what the empty form promises (let the kernel decide); set an explicit keep-alive-tmo = 30 if you want a DC's keep-alive guaranteed — pinning it in [Discovery Controller Defaults] is exactly what that section is for.

Singleton vs repeatable sections

  • Repeatable[Subsystem], [Discovery Controller]. Multiple are normal; that is how you list many endpoints.
  • Singleton — the two *Defaults sections. Conceptually one of each in scope. A repeat within one file is almost always a mistake, so it merges (key-by-key, last wins) and emits a warning rather than failing. A *Defaults section appearing across several files is expected — that is the file-scoped overlay above — and is silent.
  • [Host] is a hard singleton — one per file, position-independent. A file's [Host] applies to every endpoint section in that file, wherever it sits — before or after them; there is no positional scoping. A second [Host] in the same file is an error (Tier 1) — see Single vs. multiple host personalities for why personas never merge.

How sections expand into connections

The runtime unit is the controller, and a connection is a controller. At parse time the files resolve into a flat set of controllers: a [Discovery Controller] is one DC connection, and a [Subsystem] with N controller = lines expands into N I/O-controller connections, one per path — the grouping is an authoring convenience that then evaporates. Controllers learned at runtime from a Discovery Log Page or an Asynchronous Event Notification (AEN) are discovered, not configured, and are not part of this file format.

Host identity and multiple personalities

Getting personalities right (see Single vs. multiple host personalities) depends on how the NVMe specification identifies a host, which turns on two identifiers that drive different subsystem mechanisms (Base 2.3 §5.2.26.1.32; TP4110, TP4126):

  • Host NQN — the host's name. The subsystem uses it for access-control lists and to filter the discovery log. It is "who you are" at the identity level.
  • Host Identifier (HostID) — a 128-bit value the subsystem uses to decide whether multiple controllers belong to the same host, for reservations and registrations. Per Base 2.3 §5.2.26.1.32, "controllers … that have the same Host Identifier are assumed to be associated with the same host and have the same reservation and registration rights." Mandatory and extended 128-bit for NVMe-oF (§5.2.26.1.32.2); a zero HostID forfeits host association, so this format treats zero as invalid. TP4126 pairs HostNQN and HostID from one System UUID — they're meant to travel together.

The consequences for configuration:

  • A distinct personality is a distinct (HostNQN, HostID) pair, and a hostnqn belongs to exactly one file. The specification permits several HostIDs under one HostNQN, but the Linux kernel does not currently support that, so this format is deliberately stricter: reusing a hostnqn across files — the top-level file and a drop-in, or two drop-ins — is an error, regardless of hostid (revisit if the kernel ever supports it). Reusing a hostid across two distinct personas (different hostnqn) is a separate collision, independent of that kernel limitation: it makes the subsystem treat them as the same host (merged reservation/registration rights) — a real functional collision, not a style nit.
  • The default persona — the top-level [Host], or no [Host] at all — falls back to the system identity: hostnqn from /etc/nvme/hostnqn, hostid from /etc/nvme/hostid. If a fallback file does not exist, the parameter is simply omitted from the connect and the kernel generates one (see the caveat below). This is the single legacy default; an explicit value overrides it.
  • A drop-in [Host] must state its hostnqn — the persona's name is what distinguishes it, so leaving it unset is an error (Tier 1). Its hostid is optional: if unset, it is omitted and the kernel generates one. There is deliberately no /etc/nvme/hostid fallback for drop-ins — inheriting the system HostID is precisely the same-host collision described above. (hostsymname, keys, etc. remain optional throughout — they do not discriminate identity.)

Kernel-generated HostID caveat. When hostid is omitted, the kernel supplies a random one. That satisfies the spec, but the value is not stable: it changes on every reboot, and can change again when the identity's last controller disconnects and the host is re-created. Persistent reservations and registrations are keyed on HostID, so a host that relies on them across reboots must pin hostid explicitly (or, for the default persona, in /etc/nvme/hostid). Hosts that do not use reservations can safely let the kernel generate.

Validation

The shared parser validates a configuration when it is read and reports problems in two tiers. Validation lives in libnvme so every consumer gets identical checks; relational checks (which compare two entries) fail the configuration as a unit and name the offending entries, rather than silently connecting a partial set. A dry-run/validate entry point in nvme-cli runs both tiers and reports without connecting (the natural home is nvme config — or a dedicated command if that would overload it). For a daemon, a validation failure on SIGHUP reload rejects the new configuration and keeps the last-good one running — a fat-fingered edit never tears down working connections.

Tier 1 — spec violations → error. The configuration is non-compliant, not merely sloppy:

  • hostid explicitly set but zero or malformed (not a valid 128-bit / UUID value) — see Host identity.
  • A drop-in [Host] with no hostnqn — the persona has no name (see Host identity).
  • More than one [Host] section in a single file — see Singleton vs repeatable sections.
  • Duplicate hostid across distinct personas (different hostnqn, same explicit hostid) — see Host identity.
  • A hostnqn reused across files (the top-level file and a drop-in, or two drop-ins) — one persona, one file (see Host identity).
  • Malformed hostnqn / subsysnqn (NQN syntax, Base 2.3 §4.7).
  • An empty value (key =) on a required field (nqn, traddr, transport) — there is no kernel default to fall back to, so it is a missing required value.

Tier 2 — config hygiene → warning (loud, non-blocking):

  • A repeated singleton section within one file (a *Defaults section).
  • A persona with no explicit hostid — legal, but the kernel-generated value is unstable across reboots, which breaks persistent reservations (see the caveat in Host identity).
  • Unknown keys — ignored, same fail-safe posture as the exclusion list.

Parser conventions

The parser follows systemd conventions. Boolean values accept 1/yes/y/true/t/on and 0/no/n/false/f/off (all case-insensitive), matching systemd's parse_boolean(). Lines beginning with # are comments; blank lines are ignored. An empty value (key =) is meaningful — it resets a cascade-able tunable to the kernel default (above) — so the parser distinguishes a key that is absent from one present with an empty value. Every key has exactly one spelling: no hidden aliases, no underscore variants, and the subsystem NQN is written nqn (not subsysnqn). This is greenfield work — there are no legacy INI files to stay compatible with, so there is nothing ambiguous to parse or document.

Connection-parameter keys are the same names as the nvme connect command-line optionskeep-alive-tmo, ctrl-loss-tmo, reconnect-delay, host-iface, kxchap-secret, and so on (hyphenated, not the underscored spellings the legacy config.json used). What you write here is exactly what you would pass on the command line, which is the rule to reach for when in doubt about a key's name. The one key with no connect option is hostsymname: the host's symbolic name, sent to a Discovery Controller by the Discovery Information Management (DIM) command (nvme dim, TP8010). It is carried in this format for nvme-stas (which reads it through the Python bindings, from its own file) and for eventual TP8010 support in nvme-discoverd.

Relationship to the registry and exclusion list

These three files cover complementary questions for the same goal — letting independent actors share a host's NVMe-oF connections. Two of them are genuinely host-wide, one registry and one exclusion list for the whole machine. A connection configuration is not: it states one orchestrator's intent, and each orchestrator names its own file.

This configRegistryExclusion list
QuestionWhat should be connected?Who owns this controller?May anyone connect this controller?
Locationper orchestrator (persistent config); /etc/nvme/nvme-fabrics.conf + .conf.d/ for the nvme-cli tools and nvme-discoverd/run/nvme/registry/ (runtime, per-boot)/etc/nvme/exclusions.conf + .conf.d/ (persistent config)
Authorthe administratorlibnvme, automatically on connectthe administrator

All three rest on the same transport-ID identity (src/nvme/tid.h) and the same principle: cooperation by convention, not enforcement.

Glossary

TermMeaning
AENAsynchronous Event Notification — a controller-initiated event, e.g. a Discovery Controller signalling that its log page changed.
DCDiscovery Controller — a controller whose Discovery Log Page lists the controllers a host may connect; it exposes no namespaces.
DLPDiscovery Log Page — the list of connectable controllers a host retrieves from a Discovery Controller.
FCFibre Channel — an NVMe-oF transport.
HostIDHost Identifier — a 128-bit value identifying a host for reservations and registrations; the same HostID implies the same host.
IOCI/O Controller — a controller that exposes namespaces (storage), as opposed to a Discovery Controller.
katoKeep Alive Time-Out — the spec/kernel term for the keep-alive-tmo connection parameter (named --keep-alive-tmo on the CLI and keep-alive-tmo here).
KX-HMAC-CHAPKey-exchange HMAC-CHAP — the NVMe in-band authentication protocol; its secret is bound to a (host, subsystem) pair. Named DH-HMAC-CHAP before TP4201 added post-quantum key-exchange groups alongside classic Diffie-Hellman; the secret string format (DHHC-1:…) is unchanged by the rename.
mDNSmulticast DNS — zero-configuration discovery used to find Discovery Controllers (a future feature).
NQNNVMe Qualified Name — the name identifying a host (Host NQN) or a subsystem (Subsystem NQN) on a fabric.
NVMeNon-Volatile Memory Express.
NVMe-oFNVMe over Fabrics — NVMe carried over a network transport (TCP, RDMA, Fibre Channel).
PSKPre-Shared Key — the TLS key material that secures a fabric connection.
TIDtransport ID — the stable per-connection identity (src/nvme/tid.h) shared with the registry and exclusion list.
TLSTransport Layer Security — encrypts NVMe/TCP connections.
UUIDUniversally Unique Identifier.
WWNWorld Wide Name — a Fibre Channel address.

References

Specification documents this design cites (section numbers refer to the revisions below):

  • NVM Express Base Specification, Revision 2.3 (2025-08-01, Ratified) — NVMe Qualified Names (§4.7); Host Identifier feature (§5.2.26.1.32), its NVMe-oF requirements (§5.2.26.1.32.2)
  • TP4110 — Align PCIe and Fabrics HOSTID Management (Ratified 2022-01-11)
  • TP4126 — NVMe-oF Boot HostNQN and HostID (Ratified 2023-01-25)
  • TP8010 — NVMe-oF Centralized Discovery Controller (Ratified 2022-01-12) — the Discovery Information Management (DIM) command that carries hostsymname
  • TP8009 — Automated Discovery of NVMe-oF Discovery Controllers in IP Networks (Ratified 2022-01-11) and TP8024 — mDNS Discovery update (Ratified 2024-06-11) — the mDNS discovery behind auto-discovered DCs
  • TP4201 — Enhancements for Post-Quantum Crypto Algorithms (Ratified) — renamed DH-HMAC-CHAP to KX-HMAC-CHAP; the secret string format is unchanged

Further reading

Notes

nvme-cli has historically stored saved connections in /etc/nvme/config.json, via the optional json-c library. That format is superseded by the INI format described here: it removes libnvme's dependency on json-c, it supports comments (JSON does not), and it matches the hand-edit style of systemd's own .conf / .network / .service files.

libnvme itself never reads config.json — INI is the only format it understands. The transition is staged entirely in nvme-cli, above the library: nvme config-convert reads the legacy config.json / discovery.conf once and writes the INI equivalent, then renames each converted legacy file to <name>.converted so a repeat run does not read it again. Routine operation only reads the INI — the tool never rewrites it.

discovery.conf is also superseded. By its own man page it is "a list of connect-all commands to run": limited to Discovery Controllers by construction, with no way to express an I/O Controller or global defaults, and no [Host]-style persona (the hostnqn/hostid pair has to be repeated on every line for one identity, with nothing validating the result). The format here covers the full set, so it replaces discovery.conf too.

libnvme's own JSON dump of the live topology (libnvme_dump_tree()) was retired, not ported: nvme list -vvv -o json already walks the same host/subsystem/controller/namespace tree and renders it as JSON.