Read the PKCS#11 token PIN from NB_TPM_PIN instead of the profile config

This commit is contained in:
Viktor Liu
2026-10-01 08:16:40 +02:00
parent 0641f0d7bb
commit ab2f8972be
6 changed files with 30 additions and 18 deletions
+12 -10
View File
@@ -18,7 +18,7 @@ to one WireGuard peer key and cannot be replayed by another peer.
| Windows | signed-in user's `CurrentUser\MY` | a helper launched with that session's token |
| Linux and others | PEM directory: `CertStoreDir` in the profile config, else `NB_CERT_STORE_DIR`, else `/etc/netbird/certs` | the daemon, directly |
| Linux | a `TSS2 PRIVATE KEY` file in that directory, signed by the TPM | the daemon, through `/dev/tpmrm0` |
| Linux | a PKCS#11 token, tpm2-pkcs11 for one, enabled by `CertPKCS11PIN` in the profile config | the daemon, through the token's module, in builds with the `pkcs11` tag |
| Linux | a PKCS#11 token, tpm2-pkcs11 for one, enabled by `NB_TPM_PIN` in the daemon's environment | the daemon, through the token's module, in builds with the `pkcs11` tag |
macOS and Windows both keep per-user certificates out of reach of a privileged daemon,
and both are handled the same way: the daemon reads the machine store itself and
@@ -147,11 +147,12 @@ NB_TPM_DEVICE=/tmp/swtpm.sock go test ./client/internal/certproof/ -run TestColl
Distributions that follow Red Hat's guidance reach the TPM through tpm2-pkcs11, a PKCS#11
module whose token holds both the key and, after `tpm2_ptool addcert`, the certificate.
The store reads that token when the profile config, `/etc/netbird/config.json` by default,
carries the token's user PIN:
The store reads that token when the daemon's environment carries the token's user PIN in
`NB_TPM_PIN`. The PIN is never read from the profile config or a command-line flag; set it
on the service instead:
```json
"CertPKCS11PIN": "1234"
```sh
netbird service install --service-env NB_TPM_PIN=1234
```
That alone opens the first token the p11-kit proxy exposes, which is tpm2-pkcs11 on a
@@ -166,7 +167,7 @@ on a host with several tokens or without p11-kit:
names the library to load; `module-name=tpm2_pkcs11` resolves to `libtpm2_pkcs11.so` on
the loader's search path, and with neither the p11-kit proxy is loaded, which exposes every
module the system has registered. The URI may carry the PIN itself, as `pin-value` inline
or `pin-source` naming a file, and `CertPKCS11PIN` takes precedence over both. Without any
or `pin-source` naming a file, and `NB_TPM_PIN` takes precedence over both. Without any
PIN no login happens, and tpm2-pkcs11 then shows no private keys at all. Every other
attribute is ignored.
@@ -184,9 +185,10 @@ Each operation opens a session, logs in, works, logs out and closes, so no token
outlives a call, and the PEM directory keeps working when the token does not: the two are
queried together and a failing token is logged rather than hiding file certificates.
Two consequences of the PIN are worth knowing. It is a secret on disk, which the profile
config already is: it holds the WireGuard private key and is written readable by root
alone, and the debug bundle's config dump leaves `CertPKCS11PIN` out. And a wrong PIN
Two consequences of the PIN are worth knowing. It lives in the service definition
(the systemd unit environment, for one), so it stays out of the profile config and the
debug bundle, which only records whether `CertPKCS11URI` is set because a URI may carry
`pin-value`. And a wrong PIN
counts against the TPM's dictionary-attack lockout, which is shared with everything else
on the machine that uses the TPM.
@@ -194,7 +196,7 @@ The module is loaded at runtime without cgo, through `purego`, which means the b
dynamically linked against libc. The store is therefore compiled in only with `-tags pkcs11`
on linux/amd64 and linux/arm64: the deb and rpm packages are built that way, since they
target glibc distributions, while the release tarballs and the Alpine-based container
images keep the fully static build. Without the tag, setting `CertPKCS11PIN` logs that
images keep the fully static build. Without the tag, setting `NB_TPM_PIN` logs that
the build lacks the support.
To exercise the path without hardware, initialise a SoftHSM token and run the end-to-end
+10
View File
@@ -8,12 +8,17 @@ import (
"errors"
"fmt"
"io"
"os"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/pkcs11"
)
// PINEnv carries the user PIN of the PKCS#11 token. It is read from the daemon's
// environment only, so the PIN never lands in the profile config or on a command line.
const PINEnv = "NB_TPM_PIN"
// PKCS11Config names the token whose certificates the store yields. URI is an RFC 7512
// PKCS#11 URI, or empty for the first token the p11-kit proxy exposes. PIN is the user
// PIN, and takes precedence over a pin-value or pin-source the URI carries.
@@ -22,6 +27,11 @@ type PKCS11Config struct {
PIN string
}
// PINFromEnv returns the token PIN set in NB_TPM_PIN, or empty when it is unset.
func PINFromEnv() string {
return os.Getenv(PINEnv)
}
// PKCS11Store yields the identities of a PKCS#11 token, which is how tpm2-pkcs11 exposes
// TPM-held keys on Linux. Certificates on the token are paired with keys by CKA_ID, the
// convention tpm2_ptool addcert and pkcs11-tool follow; certificate files in the PEM
+1 -1
View File
@@ -674,7 +674,7 @@ func createEngineConfig(key wgtypes.Key, config *profilemanager.Config, peerConf
CertStore: certproof.Config{
Dir: config.CertStoreDir,
PKCS11: certproof.PKCS11Config{URI: config.CertPKCS11URI, PIN: config.CertPKCS11PIN},
PKCS11: certproof.PKCS11Config{URI: config.CertPKCS11URI, PIN: certproof.PINFromEnv()},
},
MTU: selectMTU(config.MTU, peerConfig.Mtu),
+2
View File
@@ -743,6 +743,8 @@ func (g *BundleGenerator) addCommonConfigFields(configContent *strings.Builder)
configContent.WriteString(fmt.Sprintf("LocalMetricsEnabled: %v\n", g.internalConfig.LocalMetricsEnabled))
configContent.WriteString(fmt.Sprintf("LocalMetricsAddress: %s\n", g.internalConfig.LocalMetricsAddress))
configContent.WriteString(fmt.Sprintf("SyncMessageVersion: %v\n", g.internalConfig.SyncMessageVersion))
configContent.WriteString(fmt.Sprintf("CertStoreDir: %s\n", g.internalConfig.CertStoreDir))
configContent.WriteString(fmt.Sprintf("CertPKCS11URISet: %v\n", g.internalConfig.CertPKCS11URI != ""))
if g.internalConfig.DisableNotifications != nil {
configContent.WriteString(fmt.Sprintf("DisableNotifications: %v\n", *g.internalConfig.DisableNotifications))
+1
View File
@@ -846,6 +846,7 @@ func TestAddConfig_AllFieldsCovered(t *testing.T) {
"Name": "non-config: profile name is not needed for debug purposes",
"policy": "non-config: in-memory MDM policy snapshot, surfaced via Config.Policy() / GetConfigResponse.MDMManagedFields",
"DebugBundleUploadURL": "sensitive: MDM-provided upload URL may carry credentials or query tokens; kept out of the shared bundle",
"CertPKCS11URI": "sensitive: the URI may carry the token PIN as pin-value; only whether it is set is rendered",
}
mURL, _ := url.Parse("https://api.example.com:443")
+4 -7
View File
@@ -191,13 +191,10 @@ type Config struct {
// NB_CERT_STORE_DIR or /etc/netbird/certs; see client/internal/certproof/README.md.
CertStoreDir string
// CertPKCS11PIN is the user PIN of the PKCS#11 token, tpm2-pkcs11 for one, whose
// certificates answer certificate posture checks on Linux. Setting it enables the
// token store; see client/internal/certproof/README.md.
CertPKCS11PIN string
// CertPKCS11URI is the RFC 7512 URI selecting that token and its module. Empty means
// the first token the p11-kit proxy exposes.
// CertPKCS11URI is the RFC 7512 URI selecting the PKCS#11 token, tpm2-pkcs11 for one,
// and its module, whose certificates answer certificate posture checks on Linux.
// Empty means the first token the p11-kit proxy exposes. The token's user PIN comes
// from NB_TPM_PIN, never from this file; see client/internal/certproof/README.md.
CertPKCS11URI string
// LazyConnection is the MDM-managed lazy-connection override ("on"/"off"/"").