From c1fb3b06c711db38f7011fac9bcb9e71b731cfc8 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Wed, 9 Sep 2026 10:18:17 +0200 Subject: [PATCH 1/3] feat(cli): point telemetry at the EU PostHog project The CLI telemetry endpoint and key now target the EU project via t.chainloop.dev. The endpoint is compiled in and has no runtime override, so crb.chainloop.dev stays frozen on the previous project to keep serving binaries released before this change, and the sandbox egress allowlist keeps both hosts. Both values were already overridable with -X but nothing consumed the release secrets, so every released binary shipped the hardcoded value. GoReleaser now reads them when present and falls back to the compiled-in defaults otherwise, which keeps builds outside the release pipeline working. Two fixes to the events themselves: - The organization group replaced the groups map instead of adding to it, so cp_installation was dropped from every event that carried an org. Both groups now travel together on a single event. - An alias between the machine and the account was emitted on every authenticated command, which accounted for a third of all events. It is now limited to interactive user sessions; API and federated tokens are shared identities running on ephemeral machines, where aliasing merges unrelated runners into one person. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez Chainloop-Trace-Sessions: 1c4d66be-c354-41b4-b484-81ddadcfc0a1 --- .goreleaser.yml | 8 + app/cli/cmd/root.go | 16 +- app/cli/internal/telemetry/posthog/posthog.go | 44 ++-- .../posthog/tracker_internal_test.go | 192 ++++++++++++++++++ app/cli/internal/telemetry/telemetry.go | 16 +- app/cli/internal/telemetry/telemetry_test.go | 46 ++++- devel/sandbox-kit/spec.yaml | 8 +- 7 files changed, 307 insertions(+), 23 deletions(-) create mode 100644 app/cli/internal/telemetry/posthog/tracker_internal_test.go diff --git a/.goreleaser.yml b/.goreleaser.yml index 677413407..807e30d5b 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -30,6 +30,14 @@ builds: ldflags: - "{{ .Env.COMMON_LDFLAGS }}" - -X github.com/chainloop-dev/chainloop/app/cli/cmd.Version={{ .Version }} + # Telemetry destination as a release-time input. Both fall back to the values compiled + # into app/cli/cmd/root.go, which is what builds outside this pipeline (Homebrew, + # go install) use. `index .Env` rather than `.Env.X` because GoReleaser templates run + # with missingkey=error, and `with` rather than `envOrDefault` because GitHub Actions + # renders a missing secret as an empty string, which would inject an empty key and + # silently disable telemetry for the whole release. + - '{{ with index .Env "POSTHOG_API_KEY" }}-X github.com/chainloop-dev/chainloop/app/cli/cmd.posthogAPIKey={{ . }}{{ end }}' + - '{{ with index .Env "POSTHOG_ENDPOINT" }}-X github.com/chainloop-dev/chainloop/app/cli/cmd.posthogEndpoint={{ . }}{{ end }}' targets: - darwin_amd64 - darwin_arm64 diff --git a/app/cli/cmd/root.go b/app/cli/cmd/root.go index 55e3ce8c2..29242a9b2 100644 --- a/app/cli/cmd/root.go +++ b/app/cli/cmd/root.go @@ -451,12 +451,18 @@ func loadAuthToken(cmd *cobra.Command) (string, bool, error) { } var ( - // Posthog API key and endpoint are not sensitive information it represents Chainloop's Posthog instance. - // It can be overridden by the user if they want to use their own instance of Posthog or deactivated by setting - // DO_NOT_TRACK=1 more information that can be found at: https://github.com/chainloop-dev/chainloop/blob/main/docs/docs/reference/operator/cli-telemetry.mdx + // Posthog API key and endpoint are not sensitive information, they represent Chainloop's + // Posthog instance. Both are overridable at build time with -X so anyone compiling the CLI + // can point it at their own instance, and telemetry can be turned off entirely with + // DO_NOT_TRACK=1. See https://docs.chainloop.dev/command-line-reference/cli-telemetry. + // + // The endpoint is compiled in and has no runtime override, so an installed binary reports + // wherever it was built to report. https://crb.chainloop.dev is therefore frozen on the + // previous project to keep serving binaries released before this change; it must not be + // repointed. // nolint:gosec - posthogAPIKey = "phc_TWWW19kEiD6sEejlHKWcICQ5Vc06vZUTYia8WdPB0A0" // gitleaks:allow - posthogEndpoint = "https://crb.chainloop.dev" + posthogAPIKey = "phc_rCvU7fL4Ndr4GpzH54RNdyNUWNgTpuHX7ApDxFW6G9FS" // gitleaks:allow + posthogEndpoint = "https://t.chainloop.dev" ) // recordCommand sends the command to the telemetry service diff --git a/app/cli/internal/telemetry/posthog/posthog.go b/app/cli/internal/telemetry/posthog/posthog.go index 39c3683c3..c7c06e3eb 100644 --- a/app/cli/internal/telemetry/posthog/posthog.go +++ b/app/cli/internal/telemetry/posthog/posthog.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "log" + "time" "github.com/chainloop-dev/chainloop/app/cli/internal/telemetry" "github.com/posthog/posthog-go" @@ -40,6 +41,9 @@ func NewClient(apiKey string, endpointURL string) (*Tracker, error) { client, err := posthog.NewWithConfig(apiKey, posthog.Config{ Endpoint: endpointURL, Logger: posthog.StdLogger(noopLogger, false), + // Close flushes the batch, and with no timeout it waits indefinitely. The CLI only + // abandons the telemetry goroutine after its own deadline, so bound the flush here. + ShutdownTimeout: 2 * time.Second, }) if err != nil { return nil, fmt.Errorf("failed to create PostHog client: %w", err) @@ -50,9 +54,17 @@ func NewClient(apiKey string, endpointURL string) (*Tracker, error) { }, nil } +// enqueuer is the subset of posthog.Client that the Tracker uses. Depending on the +// narrow interface instead of the full SDK one keeps the tracker testable: the SDK +// interface also carries the feature-flag surface, which this package never touches. +type enqueuer interface { + posthog.EnqueueClient + Close() error +} + // Tracker is an implementation of the telemetry.Client interface for PostHog. type Tracker struct { - client posthog.Client + client enqueuer } // TrackEvent sends an event to the PostHog server. @@ -73,24 +85,26 @@ func (p *Tracker) TrackEvent(_ context.Context, eventName string, id string, tag msg.Properties.Set(k, v) } - // Assign the installation ID if available as a group. - // It creates a new group named cp_installation where the values are the cp_url_hash. + // Both groups belong on the same event: cp_installation identifies the control plane the + // command talked to, organization the tenant within it. They are set on a single Groups + // map because replacing the map would drop whichever group was assigned first. + groups := posthog.NewGroups() if tags["cp_url_hash"] != "" { - msg.Groups = posthog. - NewGroups(). - Set("cp_installation", tags["cp_url_hash"]) + groups.Set("cp_installation", tags["cp_url_hash"]) } - // It creates a new group named org_id where the values are the org_id. if tags["org_id"] != "" { - msg.Groups = posthog. - NewGroups(). - Set("organization", tags["org_id"]) + groups.Set("organization", tags["org_id"]) } - // Assign an alias to the userID in the following cases: - // - The machine ID is available and different from the userID. - // - The userID is different from the default one. - // An alias can help to track the same user across different devices even when it was not logged in. - if (tags["machine_id"] != "" && tags["machine_id"] != id) && id != telemetry.UnrecognisedUserID { + if len(groups) > 0 { + msg.Groups = groups + } + + // Alias the machine ID onto the account so the events a user produced before logging in + // still resolve to them. Only interactive user sessions qualify: API and federated tokens + // are shared identities running on ephemeral CI machines, where aliasing merges unrelated + // runners into a single person and emits an alias on every command. + if tags.IsInteractiveUserSession() && + (tags["machine_id"] != "" && tags["machine_id"] != id) && id != telemetry.UnrecognisedUserID { if err := p.client.Enqueue(posthog.Alias{ DistinctId: id, Alias: tags["machine_id"], diff --git a/app/cli/internal/telemetry/posthog/tracker_internal_test.go b/app/cli/internal/telemetry/posthog/tracker_internal_test.go new file mode 100644 index 000000000..e8b476e7a --- /dev/null +++ b/app/cli/internal/telemetry/posthog/tracker_internal_test.go @@ -0,0 +1,192 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package posthog + +import ( + "context" + "testing" + + "github.com/chainloop-dev/chainloop/app/cli/internal/telemetry" + "github.com/posthog/posthog-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// recordingClient captures the messages the Tracker enqueues, in order, so the tests +// can assert on the exact payload that would reach PostHog. +type recordingClient struct { + messages []posthog.Message + closed bool +} + +func (r *recordingClient) Enqueue(msg posthog.Message) error { + r.messages = append(r.messages, msg) + return nil +} + +func (r *recordingClient) Close() error { + r.closed = true + return nil +} + +const ( + testCPHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + testOrgID = "3b8f3b1e-0b7a-4a4a-9a1e-8a6b4c2d1e0f" + testUserID = "9f1c2b3a-4d5e-6f70-8192-a3b4c5d6e7f8" + testMachineID = "machine-1234" + + tagCPURLHash = "cp_url_hash" + tagMachineID = "machine_id" + tagTokenType = "token_type" + tagCI = "ci" + + groupCPInstallation = "cp_installation" + + tagFalse = "false" + tagTrue = "true" +) + +func TestTrackEventGroupsAndAlias(t *testing.T) { + testCases := []struct { + name string + // id is the distinct ID the CommandTracker resolved for the event. + id string + tags telemetry.Tags + wantGroups posthog.Groups + wantAliasFor string + wantNoGroups bool + wantAliasCount int + }{ + { + // Unauthenticated run: no token parsed, so the machine ID is the distinct ID + // and there is nothing to alias it to. + name: "unauthenticated, installation group only", + id: testMachineID, + tags: telemetry.Tags{ + tagCPURLHash: testCPHash, + tagMachineID: testMachineID, + tagCI: tagFalse, + }, + wantGroups: posthog.Groups{groupCPInstallation: testCPHash}, + }, + { + // An API token carries an org ID. Both groups have to travel on the same event: + // dropping cp_installation here is what made the group keys look inconsistent + // across CLI versions. + name: "api token in CI, both groups and no alias", + id: testOrgID, + tags: telemetry.Tags{ + tagCPURLHash: testCPHash, + "org_id": testOrgID, + tagMachineID: testMachineID, + tagTokenType: "AUTH_TYPE_API_TOKEN", + tagCI: tagTrue, + }, + wantGroups: posthog.Groups{ + groupCPInstallation: testCPHash, + "organization": testOrgID, + }, + }, + { + // The only case where stitching a pre-login machine identity onto an account + // is meaningful. + name: "interactive user token, aliased", + id: testUserID, + tags: telemetry.Tags{ + tagCPURLHash: testCPHash, + tagMachineID: testMachineID, + tagTokenType: "AUTH_TYPE_USER", + tagCI: tagFalse, + }, + wantGroups: posthog.Groups{groupCPInstallation: testCPHash}, + wantAliasFor: testMachineID, + wantAliasCount: 1, + }, + { + // A user token used from CI is still a shared identity across ephemeral machines. + name: "user token in CI, not aliased", + id: testUserID, + tags: telemetry.Tags{ + tagCPURLHash: testCPHash, + tagMachineID: testMachineID, + tagTokenType: "AUTH_TYPE_USER", + tagCI: tagTrue, + }, + wantGroups: posthog.Groups{groupCPInstallation: testCPHash}, + }, + { + // Federated tokens resolve to the issuer URL, which is identical for every + // customer, so aliasing merges unrelated runners into a single person. + name: "federated token, not aliased", + id: "https://token.actions.githubusercontent.com", + tags: telemetry.Tags{ + tagCPURLHash: testCPHash, + tagMachineID: testMachineID, + tagTokenType: "AUTH_TYPE_FEDERATED", + tagCI: tagTrue, + }, + wantGroups: posthog.Groups{groupCPInstallation: testCPHash}, + }, + { + name: "no group tags, no groups sent", + id: testMachineID, + tags: telemetry.Tags{ + tagMachineID: testMachineID, + tagCI: tagFalse, + }, + wantNoGroups: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + client := &recordingClient{} + tracker := &Tracker{client: client} + + err := tracker.TrackEvent(context.Background(), "command_executed", tc.id, tc.tags) + require.NoError(t, err) + assert.True(t, client.closed, "the client must be closed so the batch is flushed") + + var captures []posthog.Capture + var aliases []posthog.Alias + for _, msg := range client.messages { + switch m := msg.(type) { + case posthog.Capture: + captures = append(captures, m) + case posthog.Alias: + aliases = append(aliases, m) + default: + require.Failf(t, "unexpected message type", "%T", msg) + } + } + + require.Len(t, captures, 1) + if tc.wantNoGroups { + assert.Nil(t, captures[0].Groups) + } else { + assert.Equal(t, tc.wantGroups, captures[0].Groups) + } + + require.Len(t, aliases, tc.wantAliasCount) + if tc.wantAliasCount > 0 { + assert.Equal(t, tc.id, aliases[0].DistinctId) + assert.Equal(t, tc.wantAliasFor, aliases[0].Alias) + // The alias has to be enqueued before the event it stitches. + assert.IsType(t, posthog.Alias{}, client.messages[0]) + } + }) + } +} diff --git a/app/cli/internal/telemetry/telemetry.go b/app/cli/internal/telemetry/telemetry.go index b1f184864..3054207c8 100644 --- a/app/cli/internal/telemetry/telemetry.go +++ b/app/cli/internal/telemetry/telemetry.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -28,6 +28,11 @@ import ( const commandTrackerEventName = "command_executed" const UnrecognisedUserID = "unrecognised" +// authTypeUser mirrors v1.Attestation_Auth_AUTH_TYPE_USER.String(). It is duplicated as a +// literal so this package keeps no dependency on the attestation API; a test pins the two +// values together. +const authTypeUser = "AUTH_TYPE_USER" + // Tags represents a collection of event tags. type Tags map[string]string @@ -68,6 +73,15 @@ func (t *CommandTracker) Track(ctx context.Context, cmd string, tags Tags) error return t.client.TrackEvent(ctx, commandTrackerEventName, id, computedTags) } +// IsInteractiveUserSession reports whether the command was run by a logged-in human outside +// of CI. API and federated tokens are shared identities: an API token belongs to an +// organization rather than a person, and a federated token resolves to the issuer URL, which +// is the same value for every Chainloop installation using that provider. Anything derived +// from the machine the command ran on is only meaningful for the interactive case. +func (tg Tags) IsInteractiveUserSession() bool { + return tg["token_type"] == authTypeUser && tg["ci"] == "false" +} + // Merges two tag maps. func mergeTags(defaultTags, userTags Tags) Tags { result := make(Tags) diff --git a/app/cli/internal/telemetry/telemetry_test.go b/app/cli/internal/telemetry/telemetry_test.go index 1fb9f244e..d01300eca 100644 --- a/app/cli/internal/telemetry/telemetry_test.go +++ b/app/cli/internal/telemetry/telemetry_test.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -23,10 +23,54 @@ import ( "github.com/chainloop-dev/chainloop/app/cli/internal/telemetry" "github.com/chainloop-dev/chainloop/app/cli/internal/telemetry/mocks" + v1 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) +const ( + tagCI = "ci" + tagTokenType = "token_type" + tagFalse = "false" + tagTrue = "true" +) + +func TestTagsIsInteractiveUserSession(t *testing.T) { + testCases := []struct { + name string + tags telemetry.Tags + want bool + }{ + { + name: "user token outside CI", + tags: telemetry.Tags{tagTokenType: v1.Attestation_Auth_AUTH_TYPE_USER.String(), tagCI: tagFalse}, + want: true, + }, + { + name: "user token in CI", + tags: telemetry.Tags{tagTokenType: v1.Attestation_Auth_AUTH_TYPE_USER.String(), tagCI: tagTrue}, + }, + { + name: "api token outside CI", + tags: telemetry.Tags{tagTokenType: v1.Attestation_Auth_AUTH_TYPE_API_TOKEN.String(), tagCI: tagFalse}, + }, + { + name: "federated token in CI", + tags: telemetry.Tags{tagTokenType: v1.Attestation_Auth_AUTH_TYPE_FEDERATED.String(), tagCI: tagTrue}, + }, + { + name: "unauthenticated", + tags: telemetry.Tags{tagCI: tagFalse}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, tc.tags.IsInteractiveUserSession()) + }) + } +} + func TestTagsWithEnvironmentInfo(t *testing.T) { tags := telemetry.Tags{}.WithRuntimeInfo() diff --git a/devel/sandbox-kit/spec.yaml b/devel/sandbox-kit/spec.yaml index 31b3ea842..cbe3e9a28 100644 --- a/devel/sandbox-kit/spec.yaml +++ b/devel/sandbox-kit/spec.yaml @@ -364,7 +364,13 @@ permissions: # v1: `network.allowedDomains:` - "api.cp.chainloop.dev:443" # control plane - ALWAYS (attest + keyless signing CSR) - "api.cas.chainloop.dev:443" # CAS - only if the org CAS backend is external - "api.app.chainloop.dev:443" # platform backend - the EE CLI also dials this - - "crb.chainloop.dev:443" # CLI usage telemetry (PostHog) + - "t.chainloop.dev:443" # CLI usage telemetry (PostHog) - current builds + - "crb.chainloop.dev:443" # CLI usage telemetry (PostHog) - binaries released + # before the endpoint moved. The host is compiled + # in, so an older tag checked out in a sandbox + # still reaches for it, and a blocked telemetry + # egress stalls every command for the request + # deadline instead of failing cleanly. - "timestamp.digicert.com:80" # RFC 3161 timestamp authority (plain HTTP, signed response) # used during attestation signing; host comes from the # control plane's signing options From a913bbfe94864182d619ad0cf4bb9ba6f29de6a6 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Wed, 9 Sep 2026 11:01:43 +0200 Subject: [PATCH 2/3] refactor(cli): share the telemetry flush deadline The PostHog client's shutdown deadline and the deadline the command hook waits on were two separate literals that happened to hold the same value. Both now read a single constant, so they cannot drift apart. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez Chainloop-Trace-Sessions: 1c4d66be-c354-41b4-b484-81ddadcfc0a1 --- app/cli/cmd/root.go | 7 +++---- app/cli/internal/telemetry/posthog/posthog.go | 6 ++---- app/cli/internal/telemetry/telemetry.go | 7 +++++++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/cli/cmd/root.go b/app/cli/cmd/root.go index 29242a9b2..1c81cda6f 100644 --- a/app/cli/cmd/root.go +++ b/app/cli/cmd/root.go @@ -25,7 +25,6 @@ import ( "path/filepath" "strings" "sync" - "time" "github.com/adrg/xdg" "github.com/chainloop-dev/chainloop/app/cli/internal/telemetry" @@ -208,9 +207,9 @@ func NewRootCmd(l zerolog.Logger) *cobra.Command { go func() { defer telemetryWg.Done() - // Create a context that times out after 1 seconds, this is because posthog has a 10 seconds hardcoded timeout - // and we want to finish earlier than that - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + // Stop waiting on the delivery goroutine after the flush deadline, so a slow + // or unreachable telemetry endpoint cannot hold up the command. + ctx, cancel := context.WithTimeout(context.Background(), telemetry.FlushTimeout) defer cancel() done := make(chan struct{}) diff --git a/app/cli/internal/telemetry/posthog/posthog.go b/app/cli/internal/telemetry/posthog/posthog.go index c7c06e3eb..27f25a572 100644 --- a/app/cli/internal/telemetry/posthog/posthog.go +++ b/app/cli/internal/telemetry/posthog/posthog.go @@ -21,7 +21,6 @@ import ( "fmt" "io" "log" - "time" "github.com/chainloop-dev/chainloop/app/cli/internal/telemetry" "github.com/posthog/posthog-go" @@ -41,9 +40,8 @@ func NewClient(apiKey string, endpointURL string) (*Tracker, error) { client, err := posthog.NewWithConfig(apiKey, posthog.Config{ Endpoint: endpointURL, Logger: posthog.StdLogger(noopLogger, false), - // Close flushes the batch, and with no timeout it waits indefinitely. The CLI only - // abandons the telemetry goroutine after its own deadline, so bound the flush here. - ShutdownTimeout: 2 * time.Second, + // Close is what flushes the batch, and it waits indefinitely unless bounded. + ShutdownTimeout: telemetry.FlushTimeout, }) if err != nil { return nil, fmt.Errorf("failed to create PostHog client: %w", err) diff --git a/app/cli/internal/telemetry/telemetry.go b/app/cli/internal/telemetry/telemetry.go index 3054207c8..d0759c4fe 100644 --- a/app/cli/internal/telemetry/telemetry.go +++ b/app/cli/internal/telemetry/telemetry.go @@ -18,6 +18,7 @@ package telemetry import ( "context" "runtime" + "time" schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" "github.com/chainloop-dev/chainloop/pkg/attestation/crafter" @@ -28,6 +29,12 @@ import ( const commandTrackerEventName = "command_executed" const UnrecognisedUserID = "unrecognised" +// FlushTimeout bounds how long the CLI spends delivering a telemetry event. The PostHog +// client takes it as the deadline for flushing its batch on close, and the command hook +// takes it as the deadline after which it stops waiting on the delivery goroutine. Both +// read it from here so the two deadlines cannot drift apart. +const FlushTimeout = 2 * time.Second + // authTypeUser mirrors v1.Attestation_Auth_AUTH_TYPE_USER.String(). It is duplicated as a // literal so this package keeps no dependency on the attestation API; a test pins the two // values together. From 351e9bd6a298a26d6bc0bf1bed2604621772e7b1 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Wed, 9 Sep 2026 12:03:16 +0200 Subject: [PATCH 3/3] docs(cli): correct the telemetry egress note in the sandbox spec The note claimed a blocked telemetry endpoint stalls every command and stops it failing cleanly. Neither holds: commands run and exit normally regardless of telemetry, commands that skip control plane initialization are unaffected, and a refused connection or an unresolvable host costs nothing. Only a silently dropped connection is slow, and only for tracked commands, which then wait on the send deadline. Assisted-by: Claude Code Signed-off-by: Javier Rodriguez Chainloop-Trace-Sessions: 1c4d66be-c354-41b4-b484-81ddadcfc0a1 --- devel/sandbox-kit/spec.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/devel/sandbox-kit/spec.yaml b/devel/sandbox-kit/spec.yaml index cbe3e9a28..25ef8caf3 100644 --- a/devel/sandbox-kit/spec.yaml +++ b/devel/sandbox-kit/spec.yaml @@ -368,9 +368,10 @@ permissions: # v1: `network.allowedDomains:` - "crb.chainloop.dev:443" # CLI usage telemetry (PostHog) - binaries released # before the endpoint moved. The host is compiled # in, so an older tag checked out in a sandbox - # still reaches for it, and a blocked telemetry - # egress stalls every command for the request - # deadline instead of failing cleanly. + # still reaches for it. Commands succeed either + # way, but a connection that is dropped rather + # than refused costs each tracked command a + # couple of seconds waiting on the send deadline. - "timestamp.digicert.com:80" # RFC 3161 timestamp authority (plain HTTP, signed response) # used during attestation signing; host comes from the # control plane's signing options