diff --git a/config/config.go b/config/config.go index 7d23d3e18..f2f7dd4d9 100644 --- a/config/config.go +++ b/config/config.go @@ -54,6 +54,8 @@ type Config struct { ZeroClusterID string // ZeroOrganizationID is the zero organization id, only set when in zero mode. 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. diff --git a/internal/zero/bootstrap/source.go b/internal/zero/bootstrap/source.go index 3ab5af262..27a79a22f 100644 --- a/internal/zero/bootstrap/source.go +++ b/internal/zero/bootstrap/source.go @@ -99,4 +99,5 @@ func applyBootstrapConfig(dst *config.Config, src *cluster_api.BootstrapConfig) } dst.ZeroClusterID = src.ClusterId dst.ZeroOrganizationID = src.OrganizationId + dst.ZeroPseudonymizationKey = src.PseudonymizationKey } diff --git a/internal/zero/bootstrap/writers/k8s/secret_test.go b/internal/zero/bootstrap/writers/k8s/secret_test.go index 9b82875ae..1067a5056 100644 --- a/internal/zero/bootstrap/writers/k8s/secret_test.go +++ b/internal/zero/bootstrap/writers/k8s/secret_test.go @@ -6,6 +6,7 @@ import ( "crypto/tls" "crypto/x509" "encoding/base64" + "encoding/json" "fmt" "io" "net/http" @@ -63,6 +64,7 @@ func TestSecretWriter(t *testing.T) { txt := "test" src := cluster_api.BootstrapConfig{ DatabrokerStorageConnection: &txt, + PseudonymizationKey: []byte{1, 2, 3}, } writer = writer.WithOptions(writers.ConfigWriterOptions{ @@ -95,7 +97,13 @@ func TestSecretWriter(t *testing.T) { "namespace": "pomerium", }, "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) }) @@ -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) +} diff --git a/internal/zero/controller/controller.go b/internal/zero/controller/controller.go index 591111a76..deb0bae97 100644 --- a/internal/zero/controller/controller.go +++ b/internal/zero/controller/controller.go @@ -211,7 +211,7 @@ func (c *controller) runHealthChecksLeased(ctx context.Context, client databroke } 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 { // start the usage reporter return ur.Run(ctx, client) diff --git a/internal/zero/controller/usagereporter/usagereporter.go b/internal/zero/controller/usagereporter/usagereporter.go index 5ad6a039f..f2419d6ac 100644 --- a/internal/zero/controller/usagereporter/usagereporter.go +++ b/internal/zero/controller/usagereporter/usagereporter.go @@ -39,9 +39,9 @@ type usageReporterRecord struct { // A UsageReporter reports usage to the zero api. type UsageReporter struct { - api API - organizationID string - reportInterval time.Duration + api API + pseudonymizationKey []byte + reportInterval time.Duration mu sync.Mutex byUserID map[string]usageReporterRecord @@ -49,11 +49,11 @@ type UsageReporter struct { } // 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{ - api: api, - organizationID: organizationID, - reportInterval: reportInterval, + api: api, + pseudonymizationKey: pseudonymizationKey, + reportInterval: reportInterval, byUserID: make(map[string]usageReporterRecord), updates: set.New[string](0), @@ -62,7 +62,7 @@ func New(api API, organizationID string, reportInterval time.Duration) *UsageRep // Run runs the usage reporter. 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 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 { req := cluster.ReportUsageRequest{ - Users: convertUsageReporterRecords(ur.organizationID, records), + Users: convertUsageReporterRecords(ur.pseudonymizationKey, records), } return backoff.Retry(func() error { 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 for _, record := range records { u := cluster.ReportUsageUser{ LastSignedInAt: record.lastSignedInAt, - PseudonymousId: cryptutil.Pseudonymize(organizationID, record.userID), + PseudonymousId: cryptutil.Pseudonymize(pseudonymizationKey, record.userID), } if record.userEmail != "" { - u.PseudonymousEmail = cryptutil.Pseudonymize(organizationID, record.userEmail) + u.PseudonymousEmail = cryptutil.Pseudonymize(pseudonymizationKey, record.userEmail) } users = append(users, u) } diff --git a/internal/zero/controller/usagereporter/usagereporter_test.go b/internal/zero/controller/usagereporter/usagereporter_test.go index 6f031fd95..24a07ba14 100644 --- a/internal/zero/controller/usagereporter/usagereporter_test.go +++ b/internal/zero/controller/usagereporter/usagereporter_test.go @@ -55,7 +55,7 @@ func TestUsageReporter(t *testing.T) { } return nil }, - }, "bQjwPpxcwJRbvsSMFgbZFkXmxFJ", time.Millisecond*100) + }, []byte("bQjwPpxcwJRbvsSMFgbZFkXmxFJ"), time.Millisecond*100) eg, ctx := errgroup.WithContext(ctx) 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) - assert.Empty(t, convertUsageReporterRecords("XXX", nil)) + assert.Empty(t, convertUsageReporterRecords([]byte("XXX"), nil)) assert.Equal(t, []cluster.ReportUsageUser{{ LastSignedInAt: tm1, PseudonymousId: "T9V1yL/UueF/LVuF6XjoSNde0INElXG10zKepmyPke8=", PseudonymousEmail: "8w5rtnZyv0EGkpHmTlkmupgb1jCzn/IxGCfvpdGGnvI=", - }}, convertUsageReporterRecords("XXX", []usageReporterRecord{{ + }}, convertUsageReporterRecords([]byte("XXX"), []usageReporterRecord{{ userID: "ID", userEmail: "EMAIL@example.com", lastSignedInAt: tm1, @@ -138,7 +138,7 @@ func Test_convertUsageReporterRecords(t *testing.T) { assert.Equal(t, []cluster.ReportUsageUser{{ LastSignedInAt: tm1, PseudonymousId: "T9V1yL/UueF/LVuF6XjoSNde0INElXG10zKepmyPke8=", - }}, convertUsageReporterRecords("XXX", []usageReporterRecord{{ + }}, convertUsageReporterRecords([]byte("XXX"), []usageReporterRecord{{ userID: "ID", lastSignedInAt: tm1, }}), "should leave empty email") diff --git a/pkg/cryptutil/pseudonymize.go b/pkg/cryptutil/pseudonymize.go index c791a5696..7ef17586b 100644 --- a/pkg/cryptutil/pseudonymize.go +++ b/pkg/cryptutil/pseudonymize.go @@ -8,8 +8,8 @@ import ( ) // Pseudonymize pseudonymizes data by computing the HMAC-SHA256 of the data. -func Pseudonymize(organizationID string, data string) string { - h := hmac.New(sha256.New, []byte(organizationID)) +func Pseudonymize(key []byte, data string) string { + h := hmac.New(sha256.New, key) _, _ = io.WriteString(h, data) bs := h.Sum(nil) return base64.StdEncoding.EncodeToString(bs) diff --git a/pkg/zero/cluster/models.gen.go b/pkg/zero/cluster/models.gen.go index 2aec18fa3..cc080d454 100644 --- a/pkg/zero/cluster/models.gen.go +++ b/pkg/zero/cluster/models.gen.go @@ -27,6 +27,7 @@ type BootstrapConfig struct { // DatabrokerStorageConnection databroker storage connection string DatabrokerStorageConnection *string `json:"databrokerStorageConnection,omitempty"` OrganizationId string `json:"organizationId"` + PseudonymizationKey []byte `json:"pseudonymizationKey"` // SharedSecret shared secret SharedSecret []byte `json:"sharedSecret"` diff --git a/pkg/zero/cluster/openapi.yaml b/pkg/zero/cluster/openapi.yaml index 451da1fdd..3810dea6f 100644 --- a/pkg/zero/cluster/openapi.yaml +++ b/pkg/zero/cluster/openapi.yaml @@ -197,6 +197,9 @@ components: description: databroker storage connection string organizationId: type: string + pseudonymizationKey: + type: string + format: byte sharedSecret: type: string format: byte @@ -204,6 +207,7 @@ components: required: - clusterId - organizationId + - pseudonymizationKey - sharedSecret Bundle: