diff --git a/client/internal/certproof/README.md b/client/internal/certproof/README.md index 9f7831bc0..2c63c78a9 100644 --- a/client/internal/certproof/README.md +++ b/client/internal/certproof/README.md @@ -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 diff --git a/client/internal/certproof/pkcs11store.go b/client/internal/certproof/pkcs11store.go index 24d0a3b5f..fc5234171 100644 --- a/client/internal/certproof/pkcs11store.go +++ b/client/internal/certproof/pkcs11store.go @@ -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 diff --git a/client/internal/connect.go b/client/internal/connect.go index 1c278f315..a8485a994 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -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), diff --git a/client/internal/debug/debug.go b/client/internal/debug/debug.go index b362ae293..fa1b43e68 100644 --- a/client/internal/debug/debug.go +++ b/client/internal/debug/debug.go @@ -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)) diff --git a/client/internal/debug/debug_test.go b/client/internal/debug/debug_test.go index 17d520358..323da25c9 100644 --- a/client/internal/debug/debug_test.go +++ b/client/internal/debug/debug_test.go @@ -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") diff --git a/client/internal/profilemanager/config.go b/client/internal/profilemanager/config.go index 18dd978c0..f4fed05e1 100644 --- a/client/internal/profilemanager/config.go +++ b/client/internal/profilemanager/config.go @@ -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"/"").