mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-22 06:39:08 +02:00
add tpm pin to netbird config
This commit is contained in:
@@ -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, `NB_CERT_STORE_DIR` or `/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 named by `NB_CERT_PKCS11_URI`, such as tpm2-pkcs11 | the daemon, through the token's module, in builds with the `pkcs11` tag |
|
||||
| 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 |
|
||||
|
||||
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,18 +147,28 @@ 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 `NB_CERT_PKCS11_URI` names it with an RFC 7512 URI:
|
||||
The store reads that token when the profile config, `/etc/netbird/config.json` by default,
|
||||
carries the token's user PIN:
|
||||
|
||||
```json
|
||||
"CertPKCS11PIN": "1234"
|
||||
```
|
||||
NB_CERT_PKCS11_URI='pkcs11:token=netbird?module-path=/usr/lib/x86_64-linux-gnu/libtpm2_pkcs11.so&pin-source=file:/etc/netbird/pkcs11.pin'
|
||||
|
||||
That alone opens the first token the p11-kit proxy exposes, which is tpm2-pkcs11 on a
|
||||
stock setup that has registered it. `CertPKCS11URI`, an RFC 7512 URI, narrows that down
|
||||
on a host with several tokens or without p11-kit:
|
||||
|
||||
```json
|
||||
"CertPKCS11URI": "pkcs11:token=netbird?module-path=/usr/lib/x86_64-linux-gnu/libtpm2_pkcs11.so"
|
||||
```
|
||||
|
||||
`token` selects the token by label, or the first token present when absent. `module-path`
|
||||
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. `pin-source` points at a file holding the user PIN and
|
||||
`pin-value` carries it inline; without either no login happens, and tpm2-pkcs11 then shows
|
||||
no private keys at all. Every other attribute is ignored.
|
||||
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
|
||||
PIN no login happens, and tpm2-pkcs11 then shows no private keys at all. Every other
|
||||
attribute is ignored.
|
||||
|
||||
Certificates and private keys are paired by `CKA_ID`, which is what `tpm2_ptool addcert`
|
||||
and `pkcs11-tool` set. Chains are completed from the other certificates on the token. Each
|
||||
@@ -166,15 +176,17 @@ operation opens a session, logs in, works, logs out and closes, so no token hand
|
||||
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, so the PIN file
|
||||
should be root-only. 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.
|
||||
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
|
||||
counts against the TPM's dictionary-attack lockout, which is shared with everything else
|
||||
on the machine that uses the TPM.
|
||||
|
||||
The module is loaded at runtime without cgo, through `purego`, which means the binary is
|
||||
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 `NB_CERT_PKCS11_URI` logs that
|
||||
images keep the fully static build. Without the tag, setting `CertPKCS11PIN` logs that
|
||||
the build lacks the support.
|
||||
|
||||
To exercise the path without hardware, initialise a SoftHSM token and run the end-to-end
|
||||
|
||||
@@ -24,7 +24,7 @@ const helperTimeout = 30 * time.Second
|
||||
// installs device identities, and reaches the console user's login keychain only by
|
||||
// launching a helper into that user's session. A Mac sitting at the login window
|
||||
// therefore yields device proofs alone.
|
||||
func CollectProofs(ctx context.Context, checks []*proto.Checks, peerKey []byte) []certposture.Proof {
|
||||
func CollectProofs(ctx context.Context, checks []*proto.Checks, peerKey []byte, _ PKCS11Config) []certposture.Proof {
|
||||
challenges := certificateChallenges(checks)
|
||||
if len(challenges) == 0 {
|
||||
logNoChallenges(checks)
|
||||
|
||||
@@ -9,11 +9,12 @@ import (
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
|
||||
// CollectProofs answers the certificate challenges in checks from the platform store.
|
||||
// Only macOS and Windows keep per-user certificates out of reach of a privileged
|
||||
// daemon, so every other platform reads its store in the daemon itself.
|
||||
func CollectProofs(ctx context.Context, checks []*proto.Checks, peerKey []byte) []certposture.Proof {
|
||||
return Collect(ctx, DefaultStore(), checks, peerKey)
|
||||
// CollectProofs answers the certificate challenges in checks from the platform store,
|
||||
// joined by the PKCS#11 token that token names when it names one. Only macOS and
|
||||
// Windows keep per-user certificates out of reach of a privileged daemon, so every
|
||||
// other platform reads its store in the daemon itself.
|
||||
func CollectProofs(ctx context.Context, checks []*proto.Checks, peerKey []byte, token PKCS11Config) []certposture.Proof {
|
||||
return Collect(ctx, storeWithToken(token), checks, peerKey)
|
||||
}
|
||||
|
||||
// helperStore is the store the helper reads. Nothing launches a helper on these
|
||||
|
||||
@@ -25,7 +25,7 @@ const helperTimeout = 30 * time.Second
|
||||
// Intune enrol device certificates, and reaches the signed-in user's store by launching
|
||||
// a helper with that session's token. A machine at the sign-in screen therefore proves
|
||||
// device certificates alone.
|
||||
func CollectProofs(ctx context.Context, checks []*proto.Checks, peerKey []byte) []certposture.Proof {
|
||||
func CollectProofs(ctx context.Context, checks []*proto.Checks, peerKey []byte, _ PKCS11Config) []certposture.Proof {
|
||||
challenges := certificateChallenges(checks)
|
||||
if len(challenges) == 0 {
|
||||
logNoChallenges(checks)
|
||||
|
||||
@@ -14,22 +14,32 @@ import (
|
||||
"github.com/netbirdio/netbird/client/internal/pkcs11"
|
||||
)
|
||||
|
||||
const PKCS11URIEnv = "NB_CERT_PKCS11_URI"
|
||||
// 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.
|
||||
type PKCS11Config struct {
|
||||
URI string
|
||||
PIN string
|
||||
}
|
||||
|
||||
// PKCS11Store yields the identities of a PKCS#11 token, which is how tpm2-pkcs11 exposes
|
||||
// TPM-held keys on Linux. Certificates and keys are paired by CKA_ID, the convention
|
||||
// tpm2_ptool addcert and pkcs11-tool follow, and every signature happens on the token.
|
||||
type PKCS11Store struct {
|
||||
uri *pkcs11.URI
|
||||
pin string
|
||||
}
|
||||
|
||||
// NewPKCS11Store reads the token, module and PIN source from an RFC 7512 PKCS#11 URI.
|
||||
func NewPKCS11Store(uri string) (*PKCS11Store, error) {
|
||||
parsed, err := pkcs11.ParseURI(uri)
|
||||
// NewPKCS11Store parses cfg.URI, standing in the bare defaults when it is empty.
|
||||
func NewPKCS11Store(cfg PKCS11Config) (*PKCS11Store, error) {
|
||||
if cfg.URI == "" {
|
||||
return &PKCS11Store{uri: &pkcs11.URI{}, pin: cfg.PIN}, nil
|
||||
}
|
||||
parsed, err := pkcs11.ParseURI(cfg.URI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PKCS11Store{uri: parsed}, nil
|
||||
return &PKCS11Store{uri: parsed, pin: cfg.PIN}, nil
|
||||
}
|
||||
|
||||
func (s *PKCS11Store) Candidates(_ context.Context) ([]Candidate, error) {
|
||||
@@ -74,13 +84,20 @@ func (s *PKCS11Store) open() (*pkcs11.Session, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pin, err := s.uri.PIN()
|
||||
pin, err := s.userPIN()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return module.OpenSession(s.uri.Token, pin)
|
||||
}
|
||||
|
||||
func (s *PKCS11Store) userPIN() ([]byte, error) {
|
||||
if s.pin != "" {
|
||||
return []byte(s.pin), nil
|
||||
}
|
||||
return s.uri.PIN()
|
||||
}
|
||||
|
||||
type tokenCertificate struct {
|
||||
cert *x509.Certificate
|
||||
id []byte
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestCollect_PKCS11TokenEndToEnd(t *testing.T) {
|
||||
if uri == "" {
|
||||
t.Skipf("set %s to a PKCS#11 URI with a PIN to run", testPKCS11URIEnv)
|
||||
}
|
||||
store, err := NewPKCS11Store(uri)
|
||||
store, err := NewPKCS11Store(PKCS11Config{URI: uri})
|
||||
require.NoError(t, err)
|
||||
if _, err := pkcs11.Load(store.uri.Module()); errors.Is(err, pkcs11.ErrUnsupported) {
|
||||
t.Skip(err)
|
||||
@@ -187,3 +187,30 @@ func keyAttributes(t *testing.T, key crypto.Signer) []pkcs11.Attribute {
|
||||
func attr(typ uint, value []byte) pkcs11.Attribute {
|
||||
return pkcs11.Attribute{Type: typ, Value: value}
|
||||
}
|
||||
|
||||
func TestNewPKCS11Store_PIN(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg PKCS11Config
|
||||
wantPIN []byte
|
||||
wantModule string
|
||||
}{
|
||||
{"pin alone opens the first p11-kit token", PKCS11Config{PIN: "1234"}, []byte("1234"), pkcs11.DefaultModule},
|
||||
{"pin field wins over pin-value", PKCS11Config{URI: "pkcs11:?module-path=/lib/x.so&pin-value=0000", PIN: "1234"}, []byte("1234"), "/lib/x.so"},
|
||||
{"uri pin-value stands in for a missing field", PKCS11Config{URI: "pkcs11:?pin-value=0000"}, []byte("0000"), pkcs11.DefaultModule},
|
||||
{"no pin at all means no login", PKCS11Config{URI: "pkcs11:token=netbird"}, nil, pkcs11.DefaultModule},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
store, err := NewPKCS11Store(tt.cfg)
|
||||
require.NoError(t, err)
|
||||
pin, err := store.userPIN()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantPIN, pin, "PIN, nil meaning no login")
|
||||
assert.Equal(t, tt.wantModule, store.uri.Module(), "module to load")
|
||||
})
|
||||
}
|
||||
|
||||
_, err := NewPKCS11Store(PKCS11Config{URI: "not-a-pkcs11-uri", PIN: "1234"})
|
||||
assert.Error(t, err, "a malformed URI must not be silently replaced by the defaults")
|
||||
}
|
||||
|
||||
@@ -2,23 +2,22 @@
|
||||
|
||||
package certproof
|
||||
|
||||
import (
|
||||
"os"
|
||||
import log "github.com/sirupsen/logrus"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// DefaultStore is the PEM directory named by NB_CERT_STORE_DIR, or /etc/netbird/certs,
|
||||
// joined by the PKCS#11 token named by NB_CERT_PKCS11_URI when that is set.
|
||||
// DefaultStore is the PEM directory named by NB_CERT_STORE_DIR, or /etc/netbird/certs.
|
||||
func DefaultStore() Store {
|
||||
files := NewFileStore(StoreDir())
|
||||
uri := os.Getenv(PKCS11URIEnv)
|
||||
if uri == "" {
|
||||
return NewFileStore(StoreDir())
|
||||
}
|
||||
|
||||
// storeWithToken joins DefaultStore with the PKCS#11 token cfg names, when it names one.
|
||||
func storeWithToken(cfg PKCS11Config) Store {
|
||||
files := DefaultStore()
|
||||
if cfg.URI == "" && cfg.PIN == "" {
|
||||
return files
|
||||
}
|
||||
token, err := NewPKCS11Store(uri)
|
||||
token, err := NewPKCS11Store(cfg)
|
||||
if err != nil {
|
||||
log.Warnf("ignoring %s: %v", PKCS11URIEnv, err)
|
||||
log.Warnf("ignoring PKCS#11 URI: %v", err)
|
||||
return files
|
||||
}
|
||||
return Stores{files, token}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//go:build !darwin && !windows
|
||||
|
||||
package certproof
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStoreWithToken(t *testing.T) {
|
||||
assert.IsType(t, &FileStore{}, storeWithToken(PKCS11Config{}), "nothing configured reads the PEM directory alone")
|
||||
assert.IsType(t, &FileStore{}, storeWithToken(PKCS11Config{URI: "not-a-pkcs11-uri"}), "an invalid URI must not hide the PEM directory")
|
||||
|
||||
for name, cfg := range map[string]PKCS11Config{
|
||||
"pin alone": {PIN: "1234"},
|
||||
"uri alone": {URI: "pkcs11:token=netbird?pin-value=1234"},
|
||||
} {
|
||||
store, ok := storeWithToken(cfg).(Stores)
|
||||
if assert.True(t, ok, "%s joins the token to the PEM directory", name) {
|
||||
assert.Len(t, store, 2, name)
|
||||
assert.IsType(t, &PKCS11Store{}, store[1], name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/netbirdio/netbird/client/iface"
|
||||
"github.com/netbirdio/netbird/client/iface/device"
|
||||
"github.com/netbirdio/netbird/client/iface/netstack"
|
||||
"github.com/netbirdio/netbird/client/internal/certproof"
|
||||
"github.com/netbirdio/netbird/client/internal/dns"
|
||||
"github.com/netbirdio/netbird/client/internal/lazyconn"
|
||||
"github.com/netbirdio/netbird/client/internal/listener"
|
||||
@@ -671,6 +672,8 @@ func createEngineConfig(key wgtypes.Key, config *profilemanager.Config, peerConf
|
||||
|
||||
LazyConnection: lazyconn.ParseState(config.LazyConnection),
|
||||
|
||||
CertPKCS11: certproof.PKCS11Config{URI: config.CertPKCS11URI, PIN: config.CertPKCS11PIN},
|
||||
|
||||
MTU: selectMTU(config.MTU, peerConfig.Mtu),
|
||||
LogPath: logPath,
|
||||
|
||||
|
||||
@@ -171,6 +171,8 @@ type EngineConfig struct {
|
||||
|
||||
MTU uint16
|
||||
|
||||
CertPKCS11 certproof.PKCS11Config
|
||||
|
||||
// for debug bundle generation
|
||||
ProfileConfig *profilemanager.Config
|
||||
|
||||
@@ -1296,7 +1298,7 @@ func (e *Engine) applyInfoFlags(info *system.Info) {
|
||||
// certificates reachable on this device, signing each challenge nonce for our peer key.
|
||||
func (e *Engine) attachCertificateProofs(info *system.Info, checks []*mgmProto.Checks) {
|
||||
peerKey := e.config.WgPrivateKey.PublicKey()
|
||||
info.CertificateProofs = certproof.CollectProofs(e.ctx, checks, peerKey[:])
|
||||
info.CertificateProofs = certproof.CollectProofs(e.ctx, checks, peerKey[:], e.config.CertPKCS11)
|
||||
}
|
||||
|
||||
func (e *Engine) currentSystemInfo(ctx context.Context) *system.Info {
|
||||
|
||||
@@ -186,6 +186,15 @@ type Config struct {
|
||||
|
||||
ClientCertKeyPair *tls.Certificate `json:"-"`
|
||||
|
||||
// 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 string
|
||||
|
||||
// LazyConnection is the MDM-managed lazy-connection override ("on"/"off"/"").
|
||||
// Runtime-only: re-derived from MDM policy on each load, never persisted.
|
||||
LazyConnection string `json:"-"`
|
||||
|
||||
Reference in New Issue
Block a user