mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-10 15:39:07 +02:00
Both platform collectors wrapped the helper launch in a 30 second deadline, but the context they wrapped was already capped at 10 seconds by the collector that calls them, and CollectProofs has no other caller. The inner deadline could never be reached, so it described a budget the code does not have. Leave the collector as the single owner of the deadline rather than picking a smaller inner value: on macOS one budget has to cover the System keychain read and the helper launch that follows it, and how to divide it is a question about the budget as a whole, not about the helper alone.
97 lines
3.6 KiB
Go
97 lines
3.6 KiB
Go
//go:build !ios
|
|
|
|
package certproof
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/shared/management/certposture"
|
|
"github.com/netbirdio/netbird/shared/management/proto"
|
|
)
|
|
|
|
// CollectProofs answers the certificate challenges in checks from every store this Mac
|
|
// can reach. The root daemon reads the System keychain itself, which is where MDM
|
|
// 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, cfg Config) []certposture.Proof {
|
|
challenges := certificateChallenges(checks)
|
|
if len(challenges) == 0 {
|
|
logNoChallenges(checks)
|
|
return nil
|
|
}
|
|
|
|
// A helper already runs inside the user's session, so it reads its own keychain
|
|
// directly and must never launch another one.
|
|
if os.Geteuid() != 0 {
|
|
return CollectChallenges(ctx, DefaultStore(), challenges, peerKey)
|
|
}
|
|
|
|
proofs := CollectChallenges(ctx, DefaultStore(), challenges, peerKey)
|
|
|
|
userProofs, err := collectAsConsoleUser(ctx, cfg.ProfileOwner, challenges, peerKey)
|
|
if err != nil {
|
|
log.Debugf("certificate posture: console user keychain unavailable: %v", err)
|
|
}
|
|
return mergeProofs(proofs, userProofs)
|
|
}
|
|
|
|
// UserContext identifies the user whose keychain a collection would include: the console
|
|
// user when it owns the active profile, or empty when no user keychain would be asked. A
|
|
// change means a collection made earlier no longer reflects what this Mac can prove.
|
|
func UserContext(cfg Config) string {
|
|
if os.Geteuid() != 0 {
|
|
return ""
|
|
}
|
|
user, ok := CurrentConsoleUser()
|
|
if !ok || !user.isOwner(cfg.ProfileOwner) {
|
|
return ""
|
|
}
|
|
return strconv.FormatUint(uint64(user.UID), 10) + ":" + user.Name
|
|
}
|
|
|
|
// collectAsConsoleUser runs the helper inside the desktop session of the logged-in
|
|
// user. Dropping to their uid is not enough: keychain access is an XPC call to a
|
|
// per-session securityd, so the helper has to enter their Mach bootstrap namespace,
|
|
// which is what launchctl asuser does.
|
|
func collectAsConsoleUser(ctx context.Context, owner string, challenges []*proto.CertificateChallenge, peerKey []byte) ([]certposture.Proof, error) {
|
|
user, ok := CurrentConsoleUser()
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
if !user.isOwner(owner) {
|
|
log.Debugf("certificate posture: console user %s does not own the active profile, no user keychain is asked", user.Name)
|
|
return nil, nil
|
|
}
|
|
|
|
binary, err := os.Executable()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve own binary: %w", err)
|
|
}
|
|
|
|
// Absolute paths, because the daemon's PATH is configurable through the service
|
|
// environment, and sudo selects the user by uid so the name never has to round-trip.
|
|
uid := strconv.FormatUint(uint64(user.UID), 10)
|
|
cmd := exec.CommandContext(ctx, "/bin/launchctl", "asuser", uid, "/usr/bin/sudo", "-u", "#"+uid, "-H", binary, "posture", "cert-proof")
|
|
|
|
log.Debugf("certificate posture: asking the desktop session of uid %s to answer %d challenges", uid, len(challenges))
|
|
proofs, err := runHelperCmd(cmd, helperRequest(challenges, peerKey))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("run helper as uid %s: %w", uid, err)
|
|
}
|
|
log.Debugf("certificate posture: desktop session of uid %s returned %d proofs", uid, len(proofs))
|
|
return proofs, nil
|
|
}
|
|
|
|
// helperStore is the store the helper reads. On macOS the keychain search list of the
|
|
// user's own session already is that user's keychain, so the platform default is right.
|
|
func helperStore() Store {
|
|
return DefaultStore()
|
|
}
|