You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailscale/ssh/tailssh/auditd_linux.go

177 lines
4.7 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !android
package tailssh
import (
"bytes"
"encoding/binary"
"fmt"
"os"
"syscall"
"golang.org/x/sys/unix"
"tailscale.com/types/logger"
)
const (
auditUserLogin = 1112 // audit message type for user login (from linux/audit.h)
netlinkAudit = 9 // AF_NETLINK protocol number for audit (from linux/netlink.h)
nlmFRequest = 0x01 // netlink message flag: request (from linux/netlink.h)
// maxAuditMessageLength is the maximum length of an audit message payload.
// This is derived from MAX_AUDIT_MESSAGE_LENGTH (8970) in the Linux kernel
// (linux/audit.h), minus overhead for the netlink header and safety margin.
maxAuditMessageLength = 8192
)
// hasAuditWriteCap checks if the process has CAP_AUDIT_WRITE in its effective capability set.
func hasAuditWriteCap() bool {
var hdr unix.CapUserHeader
var data [2]unix.CapUserData
hdr.Version = unix.LINUX_CAPABILITY_VERSION_3
hdr.Pid = int32(os.Getpid())
if err := unix.Capget(&hdr, &data[0]); err != nil {
return false
}
const capBit = uint32(1 << (unix.CAP_AUDIT_WRITE % 32))
const capIdx = unix.CAP_AUDIT_WRITE / 32
return (data[capIdx].Effective & capBit) != 0
}
// buildAuditNetlinkMessage constructs a netlink audit message.
// This is separated from sendAuditMessage to allow testing the message format
// without requiring CAP_AUDIT_WRITE or a netlink socket.
func buildAuditNetlinkMessage(msgType uint16, message string) ([]byte, error) {
msgBytes := []byte(message)
if len(msgBytes) > maxAuditMessageLength {
msgBytes = msgBytes[:maxAuditMessageLength]
}
msgLen := len(msgBytes)
totalLen := syscall.NLMSG_HDRLEN + msgLen
alignedLen := (totalLen + syscall.NLMSG_ALIGNTO - 1) & ^(syscall.NLMSG_ALIGNTO - 1)
nlh := syscall.NlMsghdr{
Len: uint32(totalLen),
Type: msgType,
Flags: nlmFRequest,
Seq: 1,
Pid: uint32(os.Getpid()),
}
buf := bytes.NewBuffer(make([]byte, 0, alignedLen))
if err := binary.Write(buf, binary.NativeEndian, nlh); err != nil {
return nil, err
}
buf.Write(msgBytes)
for buf.Len() < alignedLen {
buf.WriteByte(0)
}
return buf.Bytes(), nil
}
// sendAuditMessage sends a message to the audit subsystem using raw netlink.
// It logs errors but does not return them.
func sendAuditMessage(logf logger.Logf, msgType uint16, message string) {
if !hasAuditWriteCap() {
return
}
fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_RAW, netlinkAudit)
if err != nil {
logf("auditd: failed to create netlink socket: %v", err)
return
}
defer syscall.Close(fd)
bindAddr := &syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK,
Pid: uint32(os.Getpid()),
Groups: 0,
}
if err := syscall.Bind(fd, bindAddr); err != nil {
logf("auditd: failed to bind netlink socket: %v", err)
return
}
kernelAddr := &syscall.SockaddrNetlink{
Family: syscall.AF_NETLINK,
Pid: 0,
Groups: 0,
}
msgBytes, err := buildAuditNetlinkMessage(msgType, message)
if err != nil {
logf("auditd: failed to build audit message: %v", err)
return
}
if err := syscall.Sendto(fd, msgBytes, 0, kernelAddr); err != nil {
logf("auditd: failed to send audit message: %v", err)
return
}
}
// logSSHLogin logs an SSH login event to auditd with whois information.
func logSSHLogin(logf logger.Logf, c *conn) {
if c == nil || c.info == nil || c.localUser == nil {
return
}
exePath := c.srv.tailscaledPath
if exePath == "" {
exePath = "tailscaled"
}
srcIP := c.info.src.Addr().String()
srcPort := c.info.src.Port()
dstIP := c.info.dst.Addr().String()
dstPort := c.info.dst.Port()
tailscaleUser := c.info.uprof.LoginName
tailscaleUserID := c.info.uprof.ID
tailscaleDisplayName := c.info.uprof.DisplayName
nodeName := c.info.node.Name()
nodeID := c.info.node.ID()
localUser := c.localUser.Username
localUID := c.localUser.Uid
localGID := c.localUser.Gid
hostname, err := os.Hostname()
if err != nil {
hostname = "unknown"
}
// use principally the same format as ssh / PAM, which come from the audit userspace, i.e.
// https://github.com/linux-audit/audit-userspace/blob/b6f8c208435038df113a9795e3e202720aee6b70/lib/audit_logging.c#L515
msg := fmt.Sprintf(
"op=login acct=%s uid=%s gid=%s "+
"src=%s src_port=%d dst=%s dst_port=%d "+
"hostname=%q exe=%q terminal=ssh res=success "+
"ts_user=%q ts_user_id=%d ts_display_name=%q ts_node=%q ts_node_id=%d",
localUser, localUID, localGID,
srcIP, srcPort, dstIP, dstPort,
hostname, exePath,
tailscaleUser, tailscaleUserID, tailscaleDisplayName, nodeName, nodeID,
)
sendAuditMessage(logf, auditUserLogin, msg)
logf("audit: SSH login: user=%s uid=%s from=%s ts_user=%s node=%s",
localUser, localUID, srcIP, tailscaleUser, nodeName)
}
func init() {
hookSSHLoginSuccess.Set(logSSHLogin)
}