control/controlclient, ipn: add client audit logging (#14950)

updates tailscale/corp#26435

Adds client support for sending audit logs to control via /machine/audit-log.
Specifically implements audit logging for user initiated disconnections.

This will require further work to optimize the peristant storage and exclusion
via build tags for mobile:
tailscale/corp#27011
tailscale/corp#27012

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
This commit is contained in:
Jonathan Nobels
2025-03-12 10:37:03 -04:00
committed by GitHub
parent 06ae52d309
commit 52710945f5
13 changed files with 1204 additions and 13 deletions

View File

@@ -57,10 +57,12 @@ import (
"tailscale.com/health/healthmsg"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/auditlog"
"tailscale.com/ipn/conffile"
"tailscale.com/ipn/ipnauth"
"tailscale.com/ipn/ipnstate"
"tailscale.com/ipn/policy"
memstore "tailscale.com/ipn/store/mem"
"tailscale.com/log/sockstatlog"
"tailscale.com/logpolicy"
"tailscale.com/net/captivedetection"
@@ -450,6 +452,12 @@ type LocalBackend struct {
// Each callback is called exactly once in unspecified order and without b.mu held.
// Returned errors are logged but otherwise ignored and do not affect the shutdown process.
shutdownCbs set.HandleSet[func() error]
// auditLogger, if non-nil, manages audit logging for the backend.
//
// It queues, persists, and sends audit logs
// to the control client. auditLogger has the same lifespan as b.cc.
auditLogger *auditlog.Logger
}
// HealthTracker returns the health tracker for the backend.
@@ -1679,6 +1687,15 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
b.logf("Failed to save new controlclient state: %v", err)
}
}
// Update the audit logger with the current profile ID.
if b.auditLogger != nil && prefsChanged {
pid := b.pm.CurrentProfile().ID()
if err := b.auditLogger.SetProfileID(pid); err != nil {
b.logf("Failed to set profile ID in audit logger: %v", err)
}
}
// initTKALocked is dependent on CurrentProfile.ID, which is initialized
// (for new profiles) on the first call to b.pm.SetPrefs.
if err := b.initTKALocked(); err != nil {
@@ -2386,6 +2403,27 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
debugFlags = append([]string{"netstack"}, debugFlags...)
}
var auditLogShutdown func()
// Audit logging is only available if the client has set up a proper persistent
// store for the logs in sys.
store, ok := b.sys.AuditLogStore.GetOK()
if !ok {
b.logf("auditlog: [unexpected] no persistent audit log storage configured. using memory store.")
store = auditlog.NewLogStore(&memstore.Store{})
}
al := auditlog.NewLogger(auditlog.Opts{
Logf: b.logf,
RetryLimit: 32,
Store: store,
})
b.auditLogger = al
auditLogShutdown = func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
al.FlushAndStop(ctx)
}
// TODO(apenwarr): The only way to change the ServerURL is to
// re-run b.Start, because this is the only place we create a
// new controlclient. EditPrefs allows you to overwrite ServerURL,
@@ -2411,6 +2449,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
C2NHandler: http.HandlerFunc(b.handleC2N),
DialPlan: &b.dialPlan, // pointer because it can't be copied
ControlKnobs: b.sys.ControlKnobs(),
Shutdown: auditLogShutdown,
// Don't warn about broken Linux IP forwarding when
// netstack is being used.
@@ -4263,6 +4302,21 @@ func (b *LocalBackend) MaybeClearAppConnector(mp *ipn.MaskedPrefs) error {
return err
}
var errNoAuditLogger = errors.New("no audit logger configured")
func (b *LocalBackend) getAuditLoggerLocked() ipnauth.AuditLogFunc {
logger := b.auditLogger
return func(action tailcfg.ClientAuditAction, details string) error {
if logger == nil {
return errNoAuditLogger
}
if err := logger.Enqueue(action, details); err != nil {
return fmt.Errorf("failed to enqueue audit log %v %q: %w", action, details, err)
}
return nil
}
}
// EditPrefs applies the changes in mp to the current prefs,
// acting as the tailscaled itself rather than a specific user.
func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (ipn.PrefsView, error) {
@@ -4288,9 +4342,8 @@ func (b *LocalBackend) EditPrefsAs(mp *ipn.MaskedPrefs, actor ipnauth.Actor) (ip
unlock := b.lockAndGetUnlock()
defer unlock()
if mp.WantRunningSet && !mp.WantRunning && b.pm.CurrentPrefs().WantRunning() {
// TODO(barnstar,nickkhyl): replace loggerFn with the actual audit logger.
loggerFn := func(action, details string) { b.logf("[audit]: %s: %s", action, details) }
if err := actor.CheckProfileAccess(b.pm.CurrentProfile(), ipnauth.Disconnect, loggerFn); err != nil {
if err := actor.CheckProfileAccess(b.pm.CurrentProfile(), ipnauth.Disconnect, b.getAuditLoggerLocked()); err != nil {
b.logf("check profile access failed: %v", err)
return ipn.PrefsView{}, err
}
@@ -5874,6 +5927,15 @@ func (b *LocalBackend) requestEngineStatusAndWait() {
func (b *LocalBackend) setControlClientLocked(cc controlclient.Client) {
b.cc = cc
b.ccAuto, _ = cc.(*controlclient.Auto)
if b.auditLogger != nil {
if err := b.auditLogger.SetProfileID(b.pm.CurrentProfile().ID()); err != nil {
b.logf("audit logger set profile ID failure: %v", err)
}
if err := b.auditLogger.Start(b.ccAuto); err != nil {
b.logf("audit logger start failure: %v", err)
}
}
}
// resetControlClientLocked sets b.cc to nil and returns the old value. If the