mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-01 21:52:02 +08:00
* Move configuration from lib/service to lib/service/servicecfg The new servicecfg package will hold only configuration for services. This will allow other packages (like tctl and tsh) to depend on servicecfg without pulling in all of lib/service (which has a number of platform-specific details). This is the first step towards being able to build tctl for Windows. * Move PAM and BPF config into servicecfg This breaks a compile-time dependency on BPF/PAM for tctl.
345 lines
7.9 KiB
Go
345 lines
7.9 KiB
Go
//go:build bpf && !386
|
|
// +build bpf,!386
|
|
|
|
/*
|
|
Copyright 2020 Gravitational, Inc.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package restrictedsession
|
|
|
|
import (
|
|
"bytes"
|
|
"embed"
|
|
"encoding/binary"
|
|
"os"
|
|
"sync"
|
|
"unsafe"
|
|
|
|
"github.com/aquasecurity/libbpfgo"
|
|
"github.com/gravitational/trace"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/lib/bpf"
|
|
"github.com/gravitational/teleport/lib/service/servicecfg"
|
|
)
|
|
|
|
var log = logrus.WithFields(logrus.Fields{
|
|
trace.Component: teleport.ComponentRestrictedSession,
|
|
})
|
|
|
|
var (
|
|
lostRestrictedEvents = prometheus.NewCounter(
|
|
prometheus.CounterOpts{
|
|
Name: teleport.MetricLostRestrictedEvents,
|
|
Help: "Number of lost restricted events.",
|
|
},
|
|
)
|
|
)
|
|
|
|
//go:embed bytecode
|
|
var embedFS embed.FS
|
|
|
|
func init() {
|
|
prometheus.MustRegister(lostRestrictedEvents)
|
|
}
|
|
|
|
var unit = make([]byte, 1)
|
|
|
|
// sessionMgr implements restrctedsession.Manager interface
|
|
// by enforcing the rules via LSM BPF hooks
|
|
type sessionMgr struct {
|
|
// mod is the handle to the BPF loaded module
|
|
mod *libbpfgo.Module
|
|
|
|
// watch keeps the set of cgroups being enforced
|
|
watch bpf.SessionWatch
|
|
|
|
// cgroups for which enforcement is active
|
|
restrictedCGroups *libbpfgo.BPFMap
|
|
|
|
// network blocking subsystem
|
|
nw *network
|
|
|
|
// eventLoop pumps the audit messages from the kernel
|
|
// to the audit subsystem
|
|
eventLoop *auditEventLoop
|
|
|
|
// updateLoop listens for restriction updates and applies them
|
|
// to the audit subsystem
|
|
updateLoop *restrictionsUpdateLoop
|
|
}
|
|
|
|
// New creates a RestrictedSession service.
|
|
func New(config *servicecfg.RestrictedSessionConfig, wc RestrictionsWatcherClient) (Manager, error) {
|
|
err := config.CheckAndSetDefaults()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// If BPF-based auditing is not enabled, don't configure anything
|
|
// right away.
|
|
if !config.Enabled {
|
|
log.Debugf("Restricted session is not enabled, skipping.")
|
|
return &NOP{}, nil
|
|
}
|
|
|
|
// Before proceeding, check that eBPF based LSM is enabled in the kernel
|
|
if err = checkBpfLsm(); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
log.Debugf("Starting restricted session.")
|
|
|
|
restrictedBPF, err := embedFS.ReadFile("bytecode/restricted.bpf.o")
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
mod, err := libbpfgo.NewModuleFromBuffer(restrictedBPF, "restricted")
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// Load into the kernel
|
|
if err = mod.BPFLoadObject(); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
nw, err := newNetwork(mod)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
cgroups, err := mod.GetMap("restricted_cgroups")
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
m := &sessionMgr{
|
|
mod: mod,
|
|
watch: bpf.NewSessionWatch(),
|
|
restrictedCGroups: cgroups,
|
|
nw: nw,
|
|
}
|
|
|
|
m.eventLoop, err = newAuditEventLoop(mod, &m.watch)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
m.updateLoop, err = newRestrictionsUpdateLoop(nw, wc)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
log.Info("Started restricted session management")
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// Close will stop any running BPF programs. Note this is only for a graceful
|
|
// shutdown, from the man page for BPF: "Generally, eBPF programs are loaded
|
|
// by the user process and automatically unloaded when the process exits."
|
|
func (m *sessionMgr) Close() {
|
|
// Close the updater loop
|
|
m.updateLoop.close()
|
|
|
|
// Signal the loop pulling events off the perf buffer to shutdown.
|
|
m.eventLoop.close()
|
|
}
|
|
|
|
// OpenSession inserts the cgroupID into the BPF hash map to enable
|
|
// enforcement by the kernel
|
|
func (m *sessionMgr) OpenSession(ctx *bpf.SessionContext, cgroupID uint64) {
|
|
m.watch.Add(cgroupID, ctx)
|
|
|
|
key := make([]byte, 8)
|
|
binary.LittleEndian.PutUint64(key, cgroupID)
|
|
|
|
m.restrictedCGroups.Update(unsafe.Pointer(&key[0]), unsafe.Pointer(&unit[0]))
|
|
|
|
log.Debugf("CGroup %v registered", cgroupID)
|
|
}
|
|
|
|
// CloseSession removes the cgroupID from the BPF hash map to enable
|
|
// enforcement by the kernel
|
|
func (m *sessionMgr) CloseSession(ctx *bpf.SessionContext, cgroupID uint64) {
|
|
key := make([]byte, 8)
|
|
binary.LittleEndian.PutUint64(key, cgroupID)
|
|
|
|
m.restrictedCGroups.DeleteKey(unsafe.Pointer(&key[0]))
|
|
|
|
m.watch.Remove(cgroupID)
|
|
|
|
log.Debugf("CGroup %v unregistered", cgroupID)
|
|
}
|
|
|
|
type restrictionsUpdateLoop struct {
|
|
nw *network
|
|
|
|
watcher *RestrictionsWatcher
|
|
|
|
// Notifies that loop goroutine is done
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
func newRestrictionsUpdateLoop(nw *network, wc RestrictionsWatcherClient) (*restrictionsUpdateLoop, error) {
|
|
w, err := NewRestrictionsWatcher(RestrictionsWatcherConfig{
|
|
Client: wc,
|
|
RestrictionsC: make(chan *NetworkRestrictions, 10),
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
l := &restrictionsUpdateLoop{
|
|
nw: nw,
|
|
watcher: w,
|
|
}
|
|
|
|
l.wg.Add(1)
|
|
go l.loop()
|
|
|
|
return l, nil
|
|
}
|
|
|
|
func (l *restrictionsUpdateLoop) close() {
|
|
l.watcher.Close()
|
|
l.wg.Wait()
|
|
}
|
|
|
|
func (l *restrictionsUpdateLoop) loop() {
|
|
defer l.wg.Done()
|
|
|
|
for r := range l.watcher.RestrictionsC {
|
|
l.nw.update(r)
|
|
}
|
|
}
|
|
|
|
type auditEventLoop struct {
|
|
// Maps the cgroup to the session
|
|
watch *bpf.SessionWatch
|
|
|
|
// BPF ring buffer for reported audit (blocked) events
|
|
events *bpf.RingBuffer
|
|
|
|
// Keeps track of the number of lost audit events
|
|
lost *bpf.Counter
|
|
|
|
// Notifies that loop goroutine is done
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
// loop pulls events off the perf ring buffer, parses them, and emits them to
|
|
// the audit log.
|
|
func newAuditEventLoop(mod *libbpfgo.Module, w *bpf.SessionWatch) (*auditEventLoop, error) {
|
|
events, err := bpf.NewRingBuffer(mod, "audit_events")
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
lost, err := bpf.NewCounter(mod, "lost", lostRestrictedEvents)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
l := &auditEventLoop{
|
|
watch: w,
|
|
events: events,
|
|
lost: lost,
|
|
}
|
|
|
|
l.wg.Add(1)
|
|
go l.loop()
|
|
|
|
return l, nil
|
|
}
|
|
|
|
func (l *auditEventLoop) loop() {
|
|
defer l.wg.Done()
|
|
|
|
for eventBytes := range l.events.EventCh {
|
|
buf := bytes.NewBuffer(eventBytes)
|
|
hdr, err := parseAuditEventHeader(buf)
|
|
if err != nil {
|
|
log.Error(err.Error())
|
|
continue
|
|
}
|
|
|
|
ctx, ok := l.watch.Get(hdr.CGroupID)
|
|
if !ok {
|
|
log.Errorf("Blocked event for unknown cgroup ID (%v)", hdr.CGroupID)
|
|
continue
|
|
}
|
|
|
|
event, err := parseAuditEvent(buf, &hdr, ctx)
|
|
if err != nil {
|
|
log.WithError(err).Error("Failed to parse network event.")
|
|
continue
|
|
}
|
|
|
|
if err = ctx.Emitter.EmitAuditEvent(ctx.Context, event); err != nil {
|
|
log.WithError(err).Warn("Failed to emit network event.")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (l *auditEventLoop) close() {
|
|
// Cleanup
|
|
l.events.Close()
|
|
l.lost.Close()
|
|
|
|
l.wg.Wait()
|
|
}
|
|
|
|
// checkBpfLsm checks that eBPF is one of the enabled
|
|
// LSM "modules".
|
|
func checkBpfLsm() error {
|
|
const lsmInfo = "/sys/kernel/security/lsm"
|
|
|
|
csv, err := os.ReadFile(lsmInfo)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
for _, mod := range bytes.Split(csv, []byte(",")) {
|
|
if bytes.Equal(mod, []byte("bpf")) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return trace.Errorf(`%s does not contain bpf entry, indicating that the kernel
|
|
is not enabled for eBPF based LSM enforcement. Make sure the kernel is compiled with
|
|
CONFIG_BPF_LSM=y and enabled via CONFIG_LSM or lsm= boot option`, lsmInfo)
|
|
}
|
|
|
|
// attachLSM attaches the LSM programs in the module to
|
|
// kernel hook points.
|
|
func attachLSM(mod *libbpfgo.Module, name string) error {
|
|
prog, err := mod.GetProgram(name)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
_, err = prog.AttachLSM()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|