Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement audit log #503

Merged
merged 1 commit into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cmd/omni/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -543,4 +543,11 @@ func init() {
config.Config.EnableBreakGlassConfigs,
"Allows downloading admin Talos and Kubernetes configs.",
)

rootCmd.Flags().StringVar(
&config.Config.AuditLogDir,
"audit-log-dir",
config.Config.AuditLogDir,
"Directory for audit log storage",
)
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ require (
github.com/jxskiss/base62 v1.1.0
github.com/mattn/go-shellwords v1.0.12
github.com/prometheus/client_golang v1.19.1
github.com/prometheus/common v0.55.0
github.com/siderolabs/crypto v0.4.4
github.com/siderolabs/discovery-api v0.1.4
github.com/siderolabs/discovery-client v0.1.9
Expand Down Expand Up @@ -199,7 +200,6 @@ require (
github.com/planetscale/vtprotobuf v0.6.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rs/cors v1.11.0 // indirect
github.com/russellhaering/goxmldsig v1.4.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/jsimonetti/rtnetlink/v2 v2.0.2 h1:ZKlbCujrIpp4/u3V2Ka0oxlf4BCkt6ojkvpy3nZoCBY=
github.com/jsimonetti/rtnetlink/v2 v2.0.2/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
Expand Down Expand Up @@ -334,6 +336,8 @@ github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/
github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
Expand Down
2 changes: 2 additions & 0 deletions hack/compose/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ services:
- logs:/_out/logs
- secondary-storage:/_out/secondary-storage
- etcd-backup:/tmp/omni-data/etcd-backup
- audit-logs:/tmp/omni-data/audit-logs
- ../generate-certs/certs:/etc/ssl/omni-certs:ro
container_name: local-omni
restart: on-failure
Expand Down Expand Up @@ -127,3 +128,4 @@ volumes:
minio:
secondary-storage:
etcd-backup:
audit-logs:
1 change: 1 addition & 0 deletions hack/generate-certs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ services:
--debug
--etcd-embedded-unsafe-fsync=true
--etcd-backup-s3
--audit-log-dir /tmp/omni-data/audit-logs
{{- range $key, $value := .RegistryMirrors }}
--registry-mirror {{ $key }}={{ $value }}
{{- end }}
Expand Down
23 changes: 23 additions & 0 deletions internal/backend/grpc/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ import (
"github.com/siderolabs/omni/client/api/omni/specs"
"github.com/siderolabs/omni/client/pkg/omni/resources"
authres "github.com/siderolabs/omni/client/pkg/omni/resources/auth"
"github.com/siderolabs/omni/internal/backend/runtime/omni/audit"
"github.com/siderolabs/omni/internal/backend/runtime/omni/controllers/omni"
"github.com/siderolabs/omni/internal/pkg/auth"
"github.com/siderolabs/omni/internal/pkg/auth/actor"
"github.com/siderolabs/omni/internal/pkg/auth/role"
"github.com/siderolabs/omni/internal/pkg/config"
"github.com/siderolabs/omni/internal/pkg/ctxstore"
)

const (
Expand Down Expand Up @@ -143,6 +145,17 @@ func (s *authServer) RegisterPublicKey(ctx context.Context, request *authpb.Regi

newPubKey := authres.NewPublicKey(resources.DefaultNamespace, pubKey.id)

auditData, ok := ctxstore.Value[*audit.Data](ctx)
if !ok {
return nil, errors.New("audit data not found")
}

auditData.UserID = userID
auditData.Fingerprint = pubKey.id
auditData.PublicKeyExpiration = pubKey.expiration.Unix()
auditData.Role = pubKeyRole
auditData.Email = email

_, err = safe.StateGet[*authres.PublicKey](ctx, s.state, newPubKey.Metadata())
if state.IsNotFoundError(err) {
setPubKeyAttributes(newPubKey)
Expand Down Expand Up @@ -236,6 +249,16 @@ func (s *authServer) ConfirmPublicKey(ctx context.Context, request *authpb.Confi
return nil, errors.New("public key <> id mismatch")
}

auditData, ok := ctxstore.Value[*audit.Data](ctx)
if !ok {
return nil, errors.New("audit data not found")
}

auditData.UserID = userID
auditData.Fingerprint = pubKey.Metadata().ID()
auditData.PublicKeyExpiration = pubKey.TypedSpec().Value.Expiration.Seconds
auditData.Role = role.Role(pubKey.TypedSpec().Value.GetRole())

_, err = safe.StateUpdateWithConflicts(ctx, s.state, pubKey.Metadata(), func(pk *authres.PublicKey) error {
pk.TypedSpec().Value.Confirmed = true

Expand Down
21 changes: 15 additions & 6 deletions internal/backend/runtime/omni/audit/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,21 @@ import (
"github.com/siderolabs/omni/internal/pkg/auth/role"
)

const (
// Auth0 is auth0 confirmation type.
Auth0 = "auth0"
// SAML is SAML confirmation type.
SAML = "saml"
)

// Data contains the audit data.
type Data struct {
UserAgent string `json:"user_agent,omitempty"`
IPAddress string `json:"ip_address,omitempty"`
UserID string `json:"user_id,omitempty"`
Identity string `json:"identity,omitempty"`
Role role.Role `json:"role,omitempty"`
Email string `json:"email,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
IPAddress string `json:"ip_address,omitempty"`
UserID string `json:"user_id,omitempty"`
Role role.Role `json:"role,omitempty"`
Email string `json:"email,omitempty"`
ConfirmationType string `json:"confirmation_type,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
PublicKeyExpiration int64 `json:"public_key_expiration,omitempty"`
}
143 changes: 143 additions & 0 deletions internal/backend/runtime/omni/audit/gate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright (c) 2024 Sidero Labs, Inc.
//
// Use of this software is governed by the Business Source License
// included in the LICENSE file.

package audit

import (
"context"
"sync"

"github.com/cosi-project/runtime/pkg/resource"
"github.com/siderolabs/gen/pair"
)

// Check is a function that checks if the event is allowed.
type Check = func(ctx context.Context, eventType EventType, args ...any) bool

// Gate is a gate that checks if the event is allowed.
//
//nolint:govet
type Gate struct {
mu sync.RWMutex
fns [10]map[resource.Type]Check
}

// Check checks if the event is allowed.
func (g *Gate) Check(ctx context.Context, eventType EventType, typ resource.Type, args ...any) bool {
fn := g.check(eventType, typ)
if fn == nil {
return false
}

return fn(ctx, eventType, args...)
}

func (g *Gate) check(eventType EventType, typ resource.Type) Check {
g.mu.RLock()
defer g.mu.RUnlock()

if g.fns[0] == nil {
return nil
}

for i, e := range allEvents {
if eventType == e.typ {
return g.fns[i][typ]
}
}

return nil
}

// AddChecks adds checks for the event types. It's allowed to pass several at once using bitwise OR.
func (g *Gate) AddChecks(eventTypes EventType, pairs []pair.Pair[resource.Type, Check]) {
g.mu.Lock()
defer g.mu.Unlock()

if g.fns[0] == nil {
for i := range g.fns {
g.fns[i] = map[resource.Type]Check{}
}
}

for _, p := range pairs {
g.addCheck(eventTypes, p)
}
}

func (g *Gate) addCheck(eventTypes EventType, p pair.Pair[resource.Type, Check]) {
for i, e := range allEvents {
if e.typ&eventTypes != 0 {
if _, ok := g.fns[i][p.F1]; ok {
panic("duplicate check")
}

g.fns[i][p.F1] = p.F2
}
}
}

// AllowAll is a check that allows all events for certain event type.
func AllowAll(context.Context, EventType, ...any) bool {
return true
}

const (
// EventGet is the get event type.
EventGet EventType = 1 << iota
// EventList is the list event type.
EventList
// EventCreate is the create event type.
EventCreate
// EventUpdate is the update event type.
EventUpdate
// EventDestroy is the destroy event type.
EventDestroy
// EventWatch is the watch event type.
EventWatch
// EventWatchKind is the watch kind event type.
EventWatchKind
// EventWatchKindAggregated is the watch kind aggregated event type.
EventWatchKindAggregated
// EventUpdateWithConflicts is the update with conflicts event type.
EventUpdateWithConflicts
// EventWatchFor is the watch for event type.
EventWatchFor
)

// EventType represents the type of event.
type EventType int

// MarshalJSON marshals the event type to JSON.
func (e *EventType) MarshalJSON() ([]byte, error) {
return []byte(`"` + e.String() + `"`), nil
}

// String returns the string representation of the event type.
func (e *EventType) String() string {
for _, ev := range allEvents {
if *e == ev.typ {
return ev.str
}
}

return "<unknown>"
}

var allEvents = []struct {
str string
typ EventType
}{
{"get", EventGet},
{"list", EventList},
{"create", EventCreate},
{"update", EventUpdate},
{"destroy", EventDestroy},
{"watch", EventWatch},
{"watch_kind", EventWatchKind},
{"watch_kind_aggregated", EventWatchKindAggregated},
{"update_with_conflicts", EventUpdateWithConflicts},
{"watch_for", EventWatchFor},
}
76 changes: 76 additions & 0 deletions internal/backend/runtime/omni/audit/log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (c) 2024 Sidero Labs, Inc.
//
// Use of this software is governed by the Business Source License
// included in the LICENSE file.

package audit

import (
"context"
"fmt"
"os"
"time"

"github.com/cosi-project/runtime/pkg/resource"
"github.com/siderolabs/gen/pair"
"go.uber.org/zap"

"github.com/siderolabs/omni/internal/pkg/ctxstore"
)

// NewLogger creates a new audit logger.
func NewLogger(auditLogDir string, logger *zap.Logger) (*Logger, error) {
err := os.MkdirAll(auditLogDir, 0o755)
if err != nil {
return nil, fmt.Errorf("failed to create audit logger: %w", err)
}

return &Logger{
logFile: NewLogFile(auditLogDir),
logger: logger,
}, nil
}

// Logger logs audit events.
type Logger struct {
gate Gate
logFile *LogFile
logger *zap.Logger
}

// LogEvent logs an audit event.
func (l *Logger) LogEvent(ctx context.Context, eventType EventType, resType resource.Type, args ...any) {
if !l.gate.Check(ctx, eventType, resType, args...) {
return
}

value, ok := ctxstore.Value[*Data](ctx)
if !ok {
return
}

err := l.logFile.Dump(&event{
Type: eventType,
ResourceType: resType,
Time: time.Now().UnixMilli(),
Data: value,
})
if err == nil {
return
}

l.logger.Error("failed to dump audit log", zap.Error(err))
}

// ShoudLog adds checks that allow event type to be logged.
func (l *Logger) ShoudLog(eventType EventType, p ...pair.Pair[resource.Type, Check]) {
l.gate.AddChecks(eventType, p)
}

//nolint:govet
type event struct {
Type EventType `json:"event_type,omitempty"`
ResourceType resource.Type `json:"resource_type,omitempty"`
Time int64 `json:"event_ts,omitempty"`
Data *Data `json:"event_data,omitempty"`
}
Loading
Loading