core/zero: add pseudonymization key (#5290)

This commit is contained in:
Caleb Doxsey 2024-09-19 14:43:01 -06:00 committed by Kenneth Jenkins
parent 982275b1bb
commit ddbf2249a3
9 changed files with 44 additions and 20 deletions

View file

@ -54,6 +54,8 @@ type Config struct {
ZeroClusterID string ZeroClusterID string
// ZeroOrganizationID is the zero organization id, only set when in zero mode. // ZeroOrganizationID is the zero organization id, only set when in zero mode.
ZeroOrganizationID string ZeroOrganizationID string
// ZeroPseudonymizationKey is the zero key used to pseudonymize data, only set in zero mode.
ZeroPseudonymizationKey []byte
} }
// Clone creates a clone of the config. // Clone creates a clone of the config.

View file

@ -99,4 +99,5 @@ func applyBootstrapConfig(dst *config.Config, src *cluster_api.BootstrapConfig)
} }
dst.ZeroClusterID = src.ClusterId dst.ZeroClusterID = src.ClusterId
dst.ZeroOrganizationID = src.OrganizationId dst.ZeroOrganizationID = src.OrganizationId
dst.ZeroPseudonymizationKey = src.PseudonymizationKey
} }

View file

@ -6,6 +6,7 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -63,6 +64,7 @@ func TestSecretWriter(t *testing.T) {
txt := "test" txt := "test"
src := cluster_api.BootstrapConfig{ src := cluster_api.BootstrapConfig{
DatabrokerStorageConnection: &txt, DatabrokerStorageConnection: &txt,
PseudonymizationKey: []byte{1, 2, 3},
} }
writer = writer.WithOptions(writers.ConfigWriterOptions{ writer = writer.WithOptions(writers.ConfigWriterOptions{
@ -95,7 +97,13 @@ func TestSecretWriter(t *testing.T) {
"namespace": "pomerium", "namespace": "pomerium",
}, },
"data": map[string]any{ "data": map[string]any{
"bootstrap.dat": `{"clusterId":"","databrokerStorageConnection":"test","organizationId":"","sharedSecret":null}`, "bootstrap.dat": mustJSON(map[string]any{
"clusterId": "",
"databrokerStorageConnection": "test",
"organizationId": "",
"pseudonymizationKey": "AQID",
"sharedSecret": nil,
}),
}, },
}, unstructured) }, unstructured)
}) })
@ -152,3 +160,11 @@ func TestSecretWriter(t *testing.T) {
} }
}) })
} }
func mustJSON(v any) string {
bs, err := json.Marshal(v)
if err != nil {
panic(err)
}
return string(bs)
}

View file

@ -211,7 +211,7 @@ func (c *controller) runHealthChecksLeased(ctx context.Context, client databroke
} }
func (c *controller) runUsageReporter(ctx context.Context, client databroker.DataBrokerServiceClient) error { func (c *controller) runUsageReporter(ctx context.Context, client databroker.DataBrokerServiceClient) error {
ur := usagereporter.New(c.api, c.bootstrapConfig.GetConfig().ZeroOrganizationID, time.Minute) ur := usagereporter.New(c.api, c.bootstrapConfig.GetConfig().ZeroPseudonymizationKey, time.Minute)
return retry.WithBackoff(ctx, "zero-usage-reporter", func(ctx context.Context) error { return retry.WithBackoff(ctx, "zero-usage-reporter", func(ctx context.Context) error {
// start the usage reporter // start the usage reporter
return ur.Run(ctx, client) return ur.Run(ctx, client)

View file

@ -39,9 +39,9 @@ type usageReporterRecord struct {
// A UsageReporter reports usage to the zero api. // A UsageReporter reports usage to the zero api.
type UsageReporter struct { type UsageReporter struct {
api API api API
organizationID string pseudonymizationKey []byte
reportInterval time.Duration reportInterval time.Duration
mu sync.Mutex mu sync.Mutex
byUserID map[string]usageReporterRecord byUserID map[string]usageReporterRecord
@ -49,11 +49,11 @@ type UsageReporter struct {
} }
// New creates a new UsageReporter. // New creates a new UsageReporter.
func New(api API, organizationID string, reportInterval time.Duration) *UsageReporter { func New(api API, pseudonymizationKey []byte, reportInterval time.Duration) *UsageReporter {
return &UsageReporter{ return &UsageReporter{
api: api, api: api,
organizationID: organizationID, pseudonymizationKey: pseudonymizationKey,
reportInterval: reportInterval, reportInterval: reportInterval,
byUserID: make(map[string]usageReporterRecord), byUserID: make(map[string]usageReporterRecord),
updates: set.New[string](0), updates: set.New[string](0),
@ -62,7 +62,7 @@ func New(api API, organizationID string, reportInterval time.Duration) *UsageRep
// Run runs the usage reporter. // Run runs the usage reporter.
func (ur *UsageReporter) Run(ctx context.Context, client databroker.DataBrokerServiceClient) error { func (ur *UsageReporter) Run(ctx context.Context, client databroker.DataBrokerServiceClient) error {
ctx = log.Ctx(ctx).With().Str("organization-id", ur.organizationID).Logger().WithContext(ctx) ctx = log.Ctx(ctx).With().Logger().WithContext(ctx)
// first initialize the user collection // first initialize the user collection
serverVersion, latestSessionRecordVersion, latestUserRecordVersion, err := ur.runInit(ctx, client) serverVersion, latestSessionRecordVersion, latestUserRecordVersion, err := ur.runInit(ctx, client)
@ -76,7 +76,7 @@ func (ur *UsageReporter) Run(ctx context.Context, client databroker.DataBrokerSe
func (ur *UsageReporter) report(ctx context.Context, records []usageReporterRecord) error { func (ur *UsageReporter) report(ctx context.Context, records []usageReporterRecord) error {
req := cluster.ReportUsageRequest{ req := cluster.ReportUsageRequest{
Users: convertUsageReporterRecords(ur.organizationID, records), Users: convertUsageReporterRecords(ur.pseudonymizationKey, records),
} }
return backoff.Retry(func() error { return backoff.Retry(func() error {
log.Debug(ctx).Int("updated-users", len(req.Users)).Msg("reporting usage") log.Debug(ctx).Int("updated-users", len(req.Users)).Msg("reporting usage")
@ -193,15 +193,15 @@ func (ur *UsageReporter) onUpdateUser(u *user.User) {
} }
} }
func convertUsageReporterRecords(organizationID string, records []usageReporterRecord) []cluster.ReportUsageUser { func convertUsageReporterRecords(pseudonymizationKey []byte, records []usageReporterRecord) []cluster.ReportUsageUser {
var users []cluster.ReportUsageUser var users []cluster.ReportUsageUser
for _, record := range records { for _, record := range records {
u := cluster.ReportUsageUser{ u := cluster.ReportUsageUser{
LastSignedInAt: record.lastSignedInAt, LastSignedInAt: record.lastSignedInAt,
PseudonymousId: cryptutil.Pseudonymize(organizationID, record.userID), PseudonymousId: cryptutil.Pseudonymize(pseudonymizationKey, record.userID),
} }
if record.userEmail != "" { if record.userEmail != "" {
u.PseudonymousEmail = cryptutil.Pseudonymize(organizationID, record.userEmail) u.PseudonymousEmail = cryptutil.Pseudonymize(pseudonymizationKey, record.userEmail)
} }
users = append(users, u) users = append(users, u)
} }

View file

@ -55,7 +55,7 @@ func TestUsageReporter(t *testing.T) {
} }
return nil return nil
}, },
}, "bQjwPpxcwJRbvsSMFgbZFkXmxFJ", time.Millisecond*100) }, []byte("bQjwPpxcwJRbvsSMFgbZFkXmxFJ"), time.Millisecond*100)
eg, ctx := errgroup.WithContext(ctx) eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error { eg.Go(func() error {
@ -125,12 +125,12 @@ func Test_convertUsageReporterRecords(t *testing.T) {
tm1 := time.Date(2024, time.September, 11, 11, 56, 0, 0, time.UTC) tm1 := time.Date(2024, time.September, 11, 11, 56, 0, 0, time.UTC)
assert.Empty(t, convertUsageReporterRecords("XXX", nil)) assert.Empty(t, convertUsageReporterRecords([]byte("XXX"), nil))
assert.Equal(t, []cluster.ReportUsageUser{{ assert.Equal(t, []cluster.ReportUsageUser{{
LastSignedInAt: tm1, LastSignedInAt: tm1,
PseudonymousId: "T9V1yL/UueF/LVuF6XjoSNde0INElXG10zKepmyPke8=", PseudonymousId: "T9V1yL/UueF/LVuF6XjoSNde0INElXG10zKepmyPke8=",
PseudonymousEmail: "8w5rtnZyv0EGkpHmTlkmupgb1jCzn/IxGCfvpdGGnvI=", PseudonymousEmail: "8w5rtnZyv0EGkpHmTlkmupgb1jCzn/IxGCfvpdGGnvI=",
}}, convertUsageReporterRecords("XXX", []usageReporterRecord{{ }}, convertUsageReporterRecords([]byte("XXX"), []usageReporterRecord{{
userID: "ID", userID: "ID",
userEmail: "EMAIL@example.com", userEmail: "EMAIL@example.com",
lastSignedInAt: tm1, lastSignedInAt: tm1,
@ -138,7 +138,7 @@ func Test_convertUsageReporterRecords(t *testing.T) {
assert.Equal(t, []cluster.ReportUsageUser{{ assert.Equal(t, []cluster.ReportUsageUser{{
LastSignedInAt: tm1, LastSignedInAt: tm1,
PseudonymousId: "T9V1yL/UueF/LVuF6XjoSNde0INElXG10zKepmyPke8=", PseudonymousId: "T9V1yL/UueF/LVuF6XjoSNde0INElXG10zKepmyPke8=",
}}, convertUsageReporterRecords("XXX", []usageReporterRecord{{ }}, convertUsageReporterRecords([]byte("XXX"), []usageReporterRecord{{
userID: "ID", userID: "ID",
lastSignedInAt: tm1, lastSignedInAt: tm1,
}}), "should leave empty email") }}), "should leave empty email")

View file

@ -8,8 +8,8 @@ import (
) )
// Pseudonymize pseudonymizes data by computing the HMAC-SHA256 of the data. // Pseudonymize pseudonymizes data by computing the HMAC-SHA256 of the data.
func Pseudonymize(organizationID string, data string) string { func Pseudonymize(key []byte, data string) string {
h := hmac.New(sha256.New, []byte(organizationID)) h := hmac.New(sha256.New, key)
_, _ = io.WriteString(h, data) _, _ = io.WriteString(h, data)
bs := h.Sum(nil) bs := h.Sum(nil)
return base64.StdEncoding.EncodeToString(bs) return base64.StdEncoding.EncodeToString(bs)

View file

@ -27,6 +27,7 @@ type BootstrapConfig struct {
// DatabrokerStorageConnection databroker storage connection string // DatabrokerStorageConnection databroker storage connection string
DatabrokerStorageConnection *string `json:"databrokerStorageConnection,omitempty"` DatabrokerStorageConnection *string `json:"databrokerStorageConnection,omitempty"`
OrganizationId string `json:"organizationId"` OrganizationId string `json:"organizationId"`
PseudonymizationKey []byte `json:"pseudonymizationKey"`
// SharedSecret shared secret // SharedSecret shared secret
SharedSecret []byte `json:"sharedSecret"` SharedSecret []byte `json:"sharedSecret"`

View file

@ -197,6 +197,9 @@ components:
description: databroker storage connection string description: databroker storage connection string
organizationId: organizationId:
type: string type: string
pseudonymizationKey:
type: string
format: byte
sharedSecret: sharedSecret:
type: string type: string
format: byte format: byte
@ -204,6 +207,7 @@ components:
required: required:
- clusterId - clusterId
- organizationId - organizationId
- pseudonymizationKey
- sharedSecret - sharedSecret
Bundle: Bundle: