Skip to content

Commit

Permalink
feat: implement audit log
Browse files Browse the repository at this point in the history
This PR implements audit logs. To enable it you have to set the `--audit-log-dir` flag
to a directory where the audit logs will be stored. The audit logs are stored in a JSON format.

Example:
```json
{"event_type":"update","resource_type":"PublicKeys.omni.sidero.dev","event_ts":1722537710182,"event_data":{"user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36","ip_address":"<snip>","user_id":"a19a7a38-1793-4262-a9ef-97bc00c7a155","role":"Admin","email":"[email protected]","confirmation_type":"auth0","fingerprint":"15acb974f769bdccd38a4b28f282b78736b80bc7","public_key_expiration":1722565909}}
```

Keep in mind that `event_ts` are in milliseconds instead of seconds.
Field `event_data` contains all relevant information about the event.

To enabled it in the development environment you will have to add the
`--audit-log-dir /tmp/omni-data/audit-logs` line to `docker-compose.override.yml`
or run `generate-certs` again.

For siderolabs#37

Signed-off-by: Dmitriy Matrenichev <[email protected]>
  • Loading branch information
DmitriyMV committed Aug 1, 2024
1 parent 60355b6 commit c11bd68
Show file tree
Hide file tree
Showing 22 changed files with 737 additions and 44 deletions.
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"`
}
205 changes: 205 additions & 0 deletions internal/backend/runtime/omni/audit/gate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// 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"
"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 {
g.mu.RLock()

if g.fns[0] == nil {
g.mu.RUnlock()

return false
}

var fn Check

switch {
case eventType&EventGet != 0:
fn = g.fns[0][typ]
case eventType&EventList != 0:
fn = g.fns[1][typ]
case eventType&EventCreate != 0:
fn = g.fns[2][typ]
case eventType&EventUpdate != 0:
fn = g.fns[3][typ]
case eventType&EventDestroy != 0:
fn = g.fns[4][typ]
case eventType&EventWatch != 0:
fn = g.fns[5][typ]
case eventType&EventWatchKind != 0:
fn = g.fns[6][typ]
case eventType&EventWatchKindAggregated != 0:
fn = g.fns[7][typ]
case eventType&EventUpdateWithConflicts != 0:
fn = g.fns[8][typ]
case eventType&EventWatchFor != 0:
fn = g.fns[9][typ]
}

g.mu.RUnlock()

if fn == nil {
return false
}

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

// AddChecks adds checks for the event type.
func (g *Gate) AddChecks(eventType 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(eventType, p)
}
}

func (g *Gate) addCheck(eventType EventType, p pair.Pair[resource.Type, Check]) {
slc := []EventType{
EventGet,
EventList,
EventCreate,
EventUpdate,
EventDestroy,
EventWatch,
EventWatchKind,
EventWatchKindAggregated,
EventUpdateWithConflicts,
EventWatchFor,
}

for i, e := range slc {
if eventType&e != 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
}

// UnmarshalJSON unmarshals the event type from JSON.
func (e *EventType) UnmarshalJSON(bytes []byte) error {
switch string(bytes) {
case `"get"`:
*e = EventGet
case `"list"`:
*e = EventList
case `"create"`:
*e = EventCreate
case `"update"`:
*e = EventUpdate
case `"destroy"`:
*e = EventDestroy
case `"watch"`:
*e = EventWatch
case `"watch_kind"`:
*e = EventWatchKind
case `"watch_kind_aggregated"`:
*e = EventWatchKindAggregated
case `"update_with_conflicts"`:
*e = EventUpdateWithConflicts
case `"watch_for"`:
*e = EventWatchFor
default:
return fmt.Errorf("unknown event type: %s", bytes)
}

return nil
}

// String returns the string representation of the event type.
func (e *EventType) String() string {
switch *e {
case EventGet:
return "get"
case EventList:
return "list"
case EventCreate:
return "create"
case EventUpdate:
return "update"
case EventDestroy:
return "destroy"
case EventWatch:
return "watch"
case EventWatchKind:
return "watch_kind"
case EventWatchKindAggregated:
return "watch_kind_aggregated"
case EventUpdateWithConflicts:
return "update_with_conflicts"
case EventWatchFor:
return "watch_for"
default:
return "<unknown>"
}
}
Loading

0 comments on commit c11bd68

Please sign in to comment.