Skip to content

Commit

Permalink
implement redaction filters
Browse files Browse the repository at this point in the history
Implement a new field filter type, the refaction filter. Redaction filters use regular
expressions to suppress sensitive information in string fields in Tetragon events. When
a regular expression in a redcation filter matches a string, everything inside of its
capture groups is replaced with `*****`, effectively censoring the output. For example,
the regular expression `(?:--password|-p)(?:\s+|=)(\S*)` will convert the string
"--password=foo" into "--password=*****".

In some cases, it is not desirable to apply a redaction filter to all events. For this use
case, redaction filters also include an event filter which can be used to select events to
redact. This event filter is configured with the same syntax as an export filter. As
a more concrete example:

    {"match": {"binary_regex": ["^foo$"]}, "redact": ["\W(qux)\W"]}

The above filter would redact any occurrences of the word "qux" in events with the binary
name "foo".

Due to the sensitive nature of redaction, these filters are applied as configured in the
agent, regardless of whether an event is exported via gRPC or the JSON exporter. In other
words, redaction filter configuration always happens at the agent config level, not in the
gRPC client CLI.

Signed-off-by: William Findlay <[email protected]>
  • Loading branch information
willfindlay committed Mar 20, 2024
1 parent 2978456 commit 319340d
Show file tree
Hide file tree
Showing 24 changed files with 1,883 additions and 379 deletions.
17 changes: 17 additions & 0 deletions api/v1/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

434 changes: 255 additions & 179 deletions api/v1/tetragon/events.pb.go

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions api/v1/tetragon/events.pb.json.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions api/v1/tetragon/events.proto
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ message CapFilterSet {
repeated CapabilitiesType none = 4;
}

message RedactionFilter {
// Match events that the redaction filter will apply to.
repeated Filter match = 1;
// Regular expressions to use for redaction. Strings inside capture groups are redacted.
repeated string redact = 2;
}

// Determines the behavior of a field filter
enum FieldFilterAction {
INCLUDE = 0;
Expand Down
20 changes: 19 additions & 1 deletion cmd/tetragon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,17 @@ func getFieldFilters() ([]*tetragon.FieldFilter, error) {
return filters, nil
}

func getRedactionFilters() ([]*fieldfilters.RedactionFilter, error) {
redactionFilters := viper.GetString(option.KeyRedactionFilters)

protoFilters, err := fieldfilters.ParseRedactionFilterList(redactionFilters)
if err != nil {
return nil, err
}

return fieldfilters.RedactionFilterListFromProto(protoFilters)
}

// Save daemon information so it is used by client cli but
// also by bugtool
func saveInitInfo() error {
Expand Down Expand Up @@ -401,11 +412,17 @@ func tetragonExecute() error {

hookRunner := rthooks.GlobalRunner().WithWatcher(k8sWatcher)

redactionFilters, err := getRedactionFilters()
if err != nil {
return err
}

pm, err := tetragonGrpc.NewProcessManager(
ctx,
&cleanupWg,
observer.GetSensorManager(),
hookRunner)
hookRunner,
redactionFilters)
if err != nil {
return err
}
Expand Down Expand Up @@ -706,6 +723,7 @@ func startExporter(ctx context.Context, server *server.Server) error {
}
req := tetragon.GetEventsRequest{AllowList: allowList, DenyList: denyList, AggregationOptions: aggregationOptions, FieldFilters: fieldFilters}
log.WithFields(logrus.Fields{"fieldFilters": fieldFilters}).Debug("Configured field filters")
log.WithFields(logrus.Fields{"redactionFilters": fieldFilters}).Debug("Configured redaction filters")
log.WithFields(logrus.Fields{"logger": writer, "request": &req}).Info("Starting JSON exporter")
exporter := exporter.NewExporter(ctx, &req, server, encoder, writer, rateLimiter)
return exporter.Start()
Expand Down
9 changes: 9 additions & 0 deletions docs/content/en/docs/reference/grpc-api.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion pkg/bench/bench.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/cilium/tetragon/pkg/cilium"
"github.com/cilium/tetragon/pkg/defaults"
"github.com/cilium/tetragon/pkg/exporter"
"github.com/cilium/tetragon/pkg/fieldfilters"
"github.com/cilium/tetragon/pkg/grpc"
"github.com/cilium/tetragon/pkg/logger"
"github.com/cilium/tetragon/pkg/observer"
Expand Down Expand Up @@ -227,7 +228,8 @@ func startBenchmarkExporter(ctx context.Context, obs *observer.Observer, summary
ctx,
&wg,
observer.GetSensorManager(),
hookRunner)
hookRunner,
[]*fieldfilters.RedactionFilter{})
if err != nil {
return err
}
Expand Down
5 changes: 3 additions & 2 deletions pkg/exporter/exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/cilium/tetragon/api/v1/tetragon"
"github.com/cilium/tetragon/pkg/encoder"
"github.com/cilium/tetragon/pkg/fieldfilters"
"github.com/cilium/tetragon/pkg/ratelimit"
"github.com/cilium/tetragon/pkg/rthooks"
"github.com/cilium/tetragon/pkg/server"
Expand Down Expand Up @@ -85,7 +86,7 @@ func TestExporter_Send(t *testing.T) {
eventNotifier := newFakeNotifier()
ctx, cancel := context.WithCancel(context.Background())
dr := rthooks.DummyHookRunner{}
grpcServer := server.NewServer(ctx, &wg, eventNotifier, &server.FakeObserver{}, dr)
grpcServer := server.NewServer(ctx, &wg, eventNotifier, &server.FakeObserver{}, dr, []*fieldfilters.RedactionFilter{})
numRecords := 2
results := newArrayWriter(numRecords)
encoder := encoder.NewProtojsonEncoder(results)
Expand Down Expand Up @@ -190,7 +191,7 @@ func Test_rateLimitExport(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
eventNotifier := newFakeNotifier()
dr := rthooks.DummyHookRunner{}
grpcServer := server.NewServer(ctx, &wg, eventNotifier, &server.FakeObserver{}, dr)
grpcServer := server.NewServer(ctx, &wg, eventNotifier, &server.FakeObserver{}, dr, []*fieldfilters.RedactionFilter{})
results := newArrayWriter(tt.totalEvents)
encoder := encoder.NewProtojsonEncoder(results)
request := &tetragon.GetEventsRequest{}
Expand Down
159 changes: 159 additions & 0 deletions pkg/fieldfilters/redaction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Tetragon

package fieldfilters

import (
"context"
"encoding/json"
"fmt"
"io"
"regexp"
"strings"

"github.com/cilium/tetragon/api/v1/tetragon"
"github.com/cilium/tetragon/pkg/filters"
v1 "github.com/cilium/tetragon/pkg/oldhubble/api/v1"
hubbleFilters "github.com/cilium/tetragon/pkg/oldhubble/filters"
"google.golang.org/protobuf/reflect/protopath"
"google.golang.org/protobuf/reflect/protorange"
"google.golang.org/protobuf/reflect/protoreflect"
)

const REDACTION_STR = "*****"

type RedactionFilter struct {
match hubbleFilters.FilterFuncs
redact []*regexp.Regexp
}

func ParseRedactionFilterList(filters string) ([]*tetragon.RedactionFilter, error) {
if filters == "" {
return nil, nil
}
dec := json.NewDecoder(strings.NewReader(filters))
var results []*tetragon.RedactionFilter
for {
var result tetragon.RedactionFilter
if err := dec.Decode(&result); err != nil {
if err == io.EOF {
break
}
return nil, fmt.Errorf("failed to parse redaction filter list: %w", err)
}
results = append(results, &result)
}
return results, nil
}

func RedactionFilterListFromProto(protoFilters []*tetragon.RedactionFilter) ([]*RedactionFilter, error) {
var filters []*RedactionFilter
for _, f := range protoFilters {
filter, err := RedactionFilterFromProto(f)
if err != nil {
return nil, err
}
filters = append(filters, filter)
}

return filters, nil
}

// RedactionFilterFromProto constructs a new RedactionFilter from a Tetragon API redaction filter.
func RedactionFilterFromProto(protoFilter *tetragon.RedactionFilter) (*RedactionFilter, error) {
var err error
filter := &RedactionFilter{}

// Construct match funcs
filter.match, err = filters.BuildFilterList(context.TODO(), protoFilter.Match, filters.Filters)
if err != nil {
return nil, fmt.Errorf("failed to construct match for redaction filter: %w", err)
}

if len(protoFilter.Redact) == 0 {
return nil, fmt.Errorf("refusing to construct redaction filter with no redactions")
}

// Compile regex
for _, re := range protoFilter.Redact {
compiled, err := regexp.Compile(re)
if err != nil {
return nil, fmt.Errorf("failed to compile redaction regex `%s`: %w", re, err)
}
filter.redact = append(filter.redact, compiled)
}

return filter, nil
}

// Redact resursively checks any string fields in the event for matches to
// redaction regexes and replaces any capture groups with `*****`.
func (f *RedactionFilter) Redact(event *tetragon.GetEventsResponse) {
if f.match != nil {
if !f.match.MatchOne(&v1.Event{Event: event}) {
return
}
}

f.doRedact(event.ProtoReflect())
}

func (f *RedactionFilter) doRedact(msg protoreflect.Message) {
protorange.Range(msg, func(p protopath.Values) error {
last := p.Index(-1)
s, ok := last.Value.Interface().(string)
if !ok {
return nil
}

for _, re := range f.redact {
s = redactString(re, s)
}

beforeLast := p.Index(-2)
switch last.Step.Kind() {
case protopath.FieldAccessStep:
m := beforeLast.Value.Message()
fd := last.Step.FieldDescriptor()
m.Set(fd, protoreflect.ValueOfString(s))
case protopath.ListIndexStep:
ls := beforeLast.Value.List()
i := last.Step.ListIndex()
ls.Set(i, protoreflect.ValueOfString(s))
case protopath.MapIndexStep:
ms := beforeLast.Value.Map()
k := last.Step.MapIndex()
ms.Set(k, protoreflect.ValueOfString(s))
}

return nil
})
}

func redactString(re *regexp.Regexp, s string) string {
s = re.ReplaceAllStringFunc(s, func(s string) string {
var redacted strings.Builder

idx := re.FindStringSubmatchIndex(s)
if len(idx) < 2 {
return s
}

// Skip first idx pair which is entire string
lastOffset := 0
for i := 2; i < len(idx); i += 2 {
// Handle nested capture groups that have already been redacted
if idx[i] < lastOffset {
continue
}
redacted.WriteString(s[lastOffset:idx[i]])
redacted.WriteString(REDACTION_STR)
lastOffset = idx[i+1]
}
// Write the rest of the string
redacted.WriteString(s[lastOffset:])

return redacted.String()
})
return s
}
Loading

0 comments on commit 319340d

Please sign in to comment.