From 7fe4cf5d9e0085392a0513ab5bcee6241889f783 Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Thu, 30 Jul 2026 23:25:01 +0530 Subject: [PATCH 1/5] feat(agent-proxy): add adoption telemetry for agent proxy commands Capture start, connect and run as PostHog events with their deployment shape (sandbox state, unmatched-host policy, sandbox escape-hatch counts, agent executable name). Group CLI events under the organization so usage is attributable, including for commands that authenticate with a client id/secret and therefore have no token in the environment to read. --- packages/cmd/agent_proxy.go | 31 +++++- packages/cmd/agent_proxy_run.go | 32 +++++++ packages/cmd/agent_proxy_start.go | 11 +++ packages/telemetry/telemetry.go | 154 +++++++++++++++++++----------- 4 files changed, 169 insertions(+), 59 deletions(-) diff --git a/packages/cmd/agent_proxy.go b/packages/cmd/agent_proxy.go index 7363874e..ebdc640a 100644 --- a/packages/cmd/agent_proxy.go +++ b/packages/cmd/agent_proxy.go @@ -17,6 +17,7 @@ import ( "github.com/Infisical/infisical-merge/packages/util" "github.com/fatih/color" "github.com/go-resty/resty/v2" + "github.com/posthog/posthog-go" "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) @@ -161,6 +162,17 @@ func runAgentProxyConnect(cmd *cobra.Command, args []string) { token := resolveAgentToken(cmd) + extraNoProxy, _ := cmd.Flags().GetString("no-proxy") + allowReadableBrokered := util.GetBoolFlagOrEnv(cmd, "allow-readable-brokered-secrets", util.INFISICAL_AGENT_PROXY_ALLOW_READABLE_BROKERED_SECRETS_NAME) + + Telemetry.AttachTokenIdentity(token.Token) + Telemetry.CaptureEvent("cli-command:agent-proxy connect", posthog.NewProperties(). + Set("version", util.CLI_VERSION). + Set("agent", telemetryAgentName(args)). + Set("credentialSource", credentialSource(cmd)). + Set("allowReadableBrokeredSecrets", allowReadableBrokered). + Set("noProxySet", extraNoProxy != "")) + httpClient := resty.New().SetAuthToken(token.Token) caResp, err := api.CallGetAgentProxyCa(httpClient) @@ -176,7 +188,6 @@ func runAgentProxyConnect(cmd *cobra.Command, args []string) { realSecrets := fetchAgentRealSecrets(token, projectID, environment, secretPath) - allowReadableBrokered := util.GetBoolFlagOrEnv(cmd, "allow-readable-brokered-secrets", util.INFISICAL_AGENT_PROXY_ALLOW_READABLE_BROKERED_SECRETS_NAME) if !allowReadableBrokered { // static readability is derived from realSecrets we already fetch; dynamic lease-ability comes from // the server (callerCanLease) since we don't fetch dynamic secrets here. @@ -184,7 +195,6 @@ func runAgentProxyConnect(cmd *cobra.Command, args []string) { assertNoBrokeredDynamicSecretsLeasable(leasableDynamicCreds) } - extraNoProxy, _ := cmd.Flags().GetString("no-proxy") env := buildAgentEnv(proxyURL(proxyAddr, projectID, environment, secretPath, token.Token), caPath, token.Token, extraNoProxy, placeholderEnvs, realSecrets) if err := runAgentProcess(args, env); err != nil { @@ -192,6 +202,23 @@ func runAgentProxyConnect(cmd *cobra.Command, args []string) { } } +// telemetryAgentName is the agent executable's name, with the path and every +// argument after it dropped: argv past the first word routinely carries +// credentials, so only the first word is ever reported. +func telemetryAgentName(args []string) string { + if len(args) == 0 { + return "" + } + return filepath.Base(args[0]) +} + +func credentialSource(cmd *cobra.Command) string { + if cmd.Flags().Changed("client-id") { + return "flag" + } + return "env" +} + func resolveAgentToken(cmd *cobra.Command) *models.TokenDetails { clientID, _ := util.GetCmdFlagOrEnvWithDefaultValue(cmd, "client-id", []string{util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME}, "") clientSecret, _ := util.GetCmdFlagOrEnvWithDefaultValue(cmd, "client-secret", []string{util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME}, "") diff --git a/packages/cmd/agent_proxy_run.go b/packages/cmd/agent_proxy_run.go index 7b51f923..1cc3abb3 100644 --- a/packages/cmd/agent_proxy_run.go +++ b/packages/cmd/agent_proxy_run.go @@ -21,6 +21,7 @@ import ( "github.com/fatih/color" "github.com/go-resty/resty/v2" "github.com/mattn/go-isatty" + "github.com/posthog/posthog-go" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -128,6 +129,24 @@ func runAgentProxyRun(cmd *cobra.Command, args []string) { extraWrite, _ := cmd.Flags().GetStringArray("allow-write") allowHosts, _ := cmd.Flags().GetStringArray("allow-host") + passEnv, _ := cmd.Flags().GetStringArray("pass-env") + setEnv, _ := cmd.Flags().GetStringArray("set-env") + Telemetry.AttachTokenIdentity(src.token()) + Telemetry.CaptureEvent("cli-command:agent-proxy run", posthog.NewProperties(). + Set("version", util.CLI_VERSION). + Set("agent", telemetryAgentName(args)). + Set("platform", runtime.GOOS). + Set("sandboxEnabled", sandboxEnabled). + Set("sandboxSource", sandboxSource(cmd)). + Set("unmatchedHost", unmatchedHost). + Set("pollInterval", pollInterval). + Set("logFileSet", logFile != ""). + Set("allowReadCount", len(extraRead)). + Set("allowWriteCount", len(extraWrite)). + Set("allowHostCount", len(allowHosts)). + Set("passEnvCount", len(passEnv)). + Set("setEnvCount", len(setEnv))) + // macOS keeps the root under ~/.infisical (already sandbox-denied) so it can be trusted once in the // keychain, which is what Go tools like gh need. Elsewhere the injected CA env var is enough. if runtime.GOOS == "darwin" && home != "" { @@ -281,6 +300,19 @@ func shutdownProxy(proxy *agentproxy.Proxy) { _ = proxy.Shutdown(ctx) } +// sandboxSource names where the sandbox toggle came from, so a deliberate opt-out is +// distinguishable from the default in telemetry. Mirrors resolveSandboxEnabled's order. +func sandboxSource(cmd *cobra.Command) string { + switch { + case cmd.Flags().Changed("sandbox"), cmd.Flags().Changed("no-sandbox"): + return "flag" + case os.Getenv("INFISICAL_AGENT_PROXY_SANDBOX") != "": + return "env" + default: + return "default" + } +} + // resolveSandboxEnabled reads the toggle from flag or env only, never .infisical.json (a committed // file must not be able to silently disable the boundary). func resolveSandboxEnabled(cmd *cobra.Command) bool { diff --git a/packages/cmd/agent_proxy_start.go b/packages/cmd/agent_proxy_start.go index 80d5da2b..4b486933 100644 --- a/packages/cmd/agent_proxy_start.go +++ b/packages/cmd/agent_proxy_start.go @@ -8,6 +8,7 @@ import ( "github.com/Infisical/infisical-merge/packages/agentproxy" "github.com/Infisical/infisical-merge/packages/util" "github.com/fatih/color" + "github.com/posthog/posthog-go" "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) @@ -45,6 +46,16 @@ func runAgentProxyStart(cmd *cobra.Command, args []string) { util.HandleError(err, "Failed to authenticate the agent proxy machine identity") } + Telemetry.AttachTokenIdentity(loginResp.AccessToken) + Telemetry.CaptureEvent("cli-command:agent-proxy start", posthog.NewProperties(). + Set("version", util.CLI_VERSION). + Set("port", port). + Set("unmatchedHost", unmatchedHost). + Set("pollInterval", pollInterval). + Set("logFormat", logFormat). + Set("logFileSet", logFile != ""). + Set("credentialSource", credentialSource(cmd))) + log.Info().Msg(color.GreenString("Agent proxy authenticated; starting MITM proxy")) var proxyToken atomic.Value diff --git a/packages/telemetry/telemetry.go b/packages/telemetry/telemetry.go index 51f0d8b6..137202d9 100644 --- a/packages/telemetry/telemetry.go +++ b/packages/telemetry/telemetry.go @@ -18,6 +18,13 @@ var POSTHOG_API_KEY_FOR_CLI string type Telemetry struct { isEnabled bool posthogClient posthog.Client + + // Identity attached by AttachTokenIdentity. Long-running commands + // (agent-proxy start/connect/run) authenticate with a client id/secret or a + // flag-supplied token, so there is nothing in the environment for + // machineIdentityClaimsFromEnv to read. + attachedIdentityId string + attachedOrgId string } type NoOpLogger struct{} @@ -60,16 +67,42 @@ func (t *Telemetry) CaptureEvent(eventName string, properties posthog.Properties // is idempotent and persists its state in the local config file. t.IdentifyUserIfNeeded() - t.posthogClient.Enqueue(posthog.Capture{ + capture := posthog.Capture{ DistinctId: userIdentity, Event: eventName, Properties: properties, - }) + } + + // Group the event under the organization so usage can be attributed to a + // company, matching the `organization` group type the backend sets. + if orgId := t.resolveOrganizationId(); orgId != "" { + capture.Groups = posthog.NewGroups().Set("organization", orgId) + } + + t.posthogClient.Enqueue(capture) defer t.posthogClient.Close() } } +// AttachTokenIdentity records the identity and organization carried by an +// Infisical access token the command has already resolved, so events fired +// afterwards are attributed even when no token is exported in the environment. +// Accepts both machine identity tokens and user session JWTs. +func (t *Telemetry) AttachTokenIdentity(token string) { + identityId, orgId := tokenIdentityClaims(token) + t.attachedIdentityId = identityId + t.attachedOrgId = orgId +} + +func (t *Telemetry) resolveOrganizationId() string { + if t.attachedOrgId != "" { + return t.attachedOrgId + } + _, orgId := machineIdentityClaimsFromEnv() + return orgId +} + // IdentifyUserIfNeeded sends a PostHog Identify call to enrich the person // record with the user's email, and aliases the anonymous machine ID to the // email so that pre-login CLI events are merged into the same person. @@ -146,55 +179,26 @@ func (t *Telemetry) IdentifyUserIfNeeded() { } } -// getMachineIdentityIdFromEnv inspects the environment variables that the -// CLI uses to receive machine-identity access tokens (the same set checked -// by util.GetInfisicalToken, minus the `--token` flag which is per-command -// and not visible to the telemetry layer) and, if a machine-identity JWT -// is present, returns the `identityId` claim from its payload. +// tokenIdentityClaims returns the `identityId` and organization claims carried +// by an Infisical JWT. Machine identity tokens name the organization `orgId` +// and user session tokens name it `organizationId`; both are read. // -// The function is intentionally best-effort and silent on failure: -// - returns "" if no token is set -// - returns "" for service tokens (`st.` prefix), which carry no JWT -// payload and represent the deprecated service-token auth mode -// - returns "" if the JWT is malformed or missing the `identityId` claim +// The function is intentionally best-effort and silent on failure, returning +// empty strings for a service token (`st.` prefix, not a JWT), a malformed +// JWT, or a payload missing the claims. // -// The token's signature is not verified — the value is only used to derive -// a PostHog distinctId, never for authorization. The same token has already -// been (or is about to be) sent to the Infisical API where its signature is -// verified server-side. -func getMachineIdentityIdFromEnv() string { - // Mirror the env-var precedence in util.GetInfisicalToken so that the - // telemetry distinctId aligns with the credential the API call will - // actually use: - // 1. INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN - // 2. INFISICAL_TOKEN - // 3. TOKEN (legacy gateway env var) - envVars := []string{ - util.INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME, - util.INFISICAL_TOKEN_NAME, - util.INFISICAL_GATEWAY_TOKEN_NAME_LEGACY, - } - - var token string - for _, name := range envVars { - if v := os.Getenv(name); v != "" { - token = v - break - } - } - - if token == "" { - return "" - } - - // Service tokens are deprecated and not JWTs — no identityId to extract. - if strings.HasPrefix(token, "st.") { - return "" +// The token's signature is not verified — the values are only used to derive a +// PostHog distinctId and group, never for authorization. The same token has +// already been (or is about to be) sent to the Infisical API where its +// signature is verified server-side. +func tokenIdentityClaims(token string) (identityId string, orgId string) { + if token == "" || strings.HasPrefix(token, "st.") { + return "", "" } parts := strings.Split(token, ".") if len(parts) != 3 { - return "" + return "", "" } payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1]) @@ -202,18 +206,50 @@ func getMachineIdentityIdFromEnv() string { // Some JWT issuers pad the payload with `=`; tolerate that variant. payloadBytes, err = base64.URLEncoding.DecodeString(parts[1]) if err != nil { - return "" + return "", "" } } var claims struct { - IdentityID string `json:"identityId"` + IdentityID string `json:"identityId"` + OrgID string `json:"orgId"` + OrganizationID string `json:"organizationId"` } if err := json.Unmarshal(payloadBytes, &claims); err != nil { - return "" + return "", "" + } + + if claims.OrgID != "" { + return claims.IdentityID, claims.OrgID + } + return claims.IdentityID, claims.OrganizationID +} + +// machineIdentityClaimsFromEnv inspects the environment variables that the +// CLI uses to receive machine-identity access tokens (the same set checked +// by util.GetInfisicalToken, minus the `--token` flag which is per-command +// and not visible to the telemetry layer) and returns the identity and +// organization claims from the first one set. +func machineIdentityClaimsFromEnv() (identityId string, orgId string) { + // Mirror the env-var precedence in util.GetInfisicalToken so that the + // telemetry distinctId aligns with the credential the API call will + // actually use: + // 1. INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN + // 2. INFISICAL_TOKEN + // 3. TOKEN (legacy gateway env var) + envVars := []string{ + util.INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME, + util.INFISICAL_TOKEN_NAME, + util.INFISICAL_GATEWAY_TOKEN_NAME_LEGACY, + } + + for _, name := range envVars { + if v := os.Getenv(name); v != "" { + return tokenIdentityClaims(v) + } } - return claims.IdentityID + return "", "" } func (t *Telemetry) GetDistinctId() (string, error) { @@ -239,17 +275,21 @@ func (t *Telemetry) GetDistinctId() (string, error) { // those events to a stale `identity-` would corrupt person-level // analytics, while attributing them to the logged-in email is always // correct. - // 2. Machine-identity access token from env. This is the dominant case - // in CI / containers / Kubernetes pods, where there is no logged-in - // user and the only credential is `INFISICAL_TOKEN` (or the UA-scoped - // env var). Aligns with the `identity-` distinctId the backend - // uses for MachineIdentityLogin and other identity-scoped events, - // so CLI events flow into the same person record. + // 2. Machine identity, either from an access token in the env (the dominant + // case in CI / containers / Kubernetes pods, where the only credential + // is `INFISICAL_TOKEN` or the UA-scoped env var) or attached by the + // command via AttachTokenIdentity when it logged in with a client + // id/secret. Aligns with the `identity-` distinctId the backend uses + // for MachineIdentityLogin and other identity-scoped events, so CLI + // events flow into the same person record. // 3. Anonymous fallback keyed by the local machine ID. + envIdentityId, _ := machineIdentityClaimsFromEnv() if infisicalConfig.LoggedInUserEmail != "" { distinctId = infisicalConfig.LoggedInUserEmail - } else if identityId := getMachineIdentityIdFromEnv(); identityId != "" { - distinctId = "identity-" + identityId + } else if envIdentityId != "" { + distinctId = "identity-" + envIdentityId + } else if t.attachedIdentityId != "" { + distinctId = "identity-" + t.attachedIdentityId } else if machineId != "" { distinctId = "anonymous_cli_" + machineId } From 97a9f389f4501bfc104bc0ac0be720e36197d3b3 Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Thu, 30 Jul 2026 23:52:38 +0530 Subject: [PATCH 2/5] fix(agent-proxy): prefer the attached identity and report the real credential source --- packages/cmd/agent_proxy.go | 21 +++++++++++++-------- packages/cmd/agent_proxy_start.go | 2 +- packages/telemetry/telemetry.go | 24 ++++++++++++++---------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/packages/cmd/agent_proxy.go b/packages/cmd/agent_proxy.go index ebdc640a..33bc523e 100644 --- a/packages/cmd/agent_proxy.go +++ b/packages/cmd/agent_proxy.go @@ -160,7 +160,7 @@ func runAgentProxyConnect(cmd *cobra.Command, args []string) { util.HandleError(fmt.Errorf("project id is required; pass --projectId, set INFISICAL_PROJECT_ID, or run inside a project with .infisical.json")) } - token := resolveAgentToken(cmd) + token, tokenSource := resolveAgentToken(cmd) extraNoProxy, _ := cmd.Flags().GetString("no-proxy") allowReadableBrokered := util.GetBoolFlagOrEnv(cmd, "allow-readable-brokered-secrets", util.INFISICAL_AGENT_PROXY_ALLOW_READABLE_BROKERED_SECRETS_NAME) @@ -169,7 +169,7 @@ func runAgentProxyConnect(cmd *cobra.Command, args []string) { Telemetry.CaptureEvent("cli-command:agent-proxy connect", posthog.NewProperties(). Set("version", util.CLI_VERSION). Set("agent", telemetryAgentName(args)). - Set("credentialSource", credentialSource(cmd)). + Set("credentialSource", tokenSource). Set("allowReadableBrokeredSecrets", allowReadableBrokered). Set("noProxySet", extraNoProxy != "")) @@ -212,14 +212,19 @@ func telemetryAgentName(args []string) string { return filepath.Base(args[0]) } -func credentialSource(cmd *cobra.Command) string { +// universalAuthCredentialSource names where the client id/secret came from. Only +// meaningful once a universal auth login is known to be the path taken. +func universalAuthCredentialSource(cmd *cobra.Command) string { if cmd.Flags().Changed("client-id") { - return "flag" + return "universal-auth-flag" } - return "env" + return "universal-auth-env" } -func resolveAgentToken(cmd *cobra.Command) *models.TokenDetails { +// resolveAgentToken returns the token to authenticate with and a label naming +// which of its branches produced it, so telemetry reports the path actually +// taken rather than re-deriving it and drifting from this function. +func resolveAgentToken(cmd *cobra.Command) (*models.TokenDetails, string) { clientID, _ := util.GetCmdFlagOrEnvWithDefaultValue(cmd, "client-id", []string{util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME}, "") clientSecret, _ := util.GetCmdFlagOrEnvWithDefaultValue(cmd, "client-secret", []string{util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME}, "") @@ -231,7 +236,7 @@ func resolveAgentToken(cmd *cobra.Command) *models.TokenDetails { return &models.TokenDetails{ Type: util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER, Token: loginResp.AccessToken, - } + }, universalAuthCredentialSource(cmd) } token, err := util.GetInfisicalToken(cmd) @@ -241,7 +246,7 @@ func resolveAgentToken(cmd *cobra.Command) *models.TokenDetails { if token == nil { util.HandleError(fmt.Errorf("authentication required; provide --client-id/--client-secret, env vars, or a token")) } - return token + return token, "token" } // Builds http://:/:@host:port (username=projectId, password="/:", jwt last). diff --git a/packages/cmd/agent_proxy_start.go b/packages/cmd/agent_proxy_start.go index 4b486933..f71e5ee8 100644 --- a/packages/cmd/agent_proxy_start.go +++ b/packages/cmd/agent_proxy_start.go @@ -54,7 +54,7 @@ func runAgentProxyStart(cmd *cobra.Command, args []string) { Set("pollInterval", pollInterval). Set("logFormat", logFormat). Set("logFileSet", logFile != ""). - Set("credentialSource", credentialSource(cmd))) + Set("credentialSource", universalAuthCredentialSource(cmd))) log.Info().Msg(color.GreenString("Agent proxy authenticated; starting MITM proxy")) diff --git a/packages/telemetry/telemetry.go b/packages/telemetry/telemetry.go index 137202d9..2b04ceb0 100644 --- a/packages/telemetry/telemetry.go +++ b/packages/telemetry/telemetry.go @@ -275,21 +275,25 @@ func (t *Telemetry) GetDistinctId() (string, error) { // those events to a stale `identity-` would corrupt person-level // analytics, while attributing them to the logged-in email is always // correct. - // 2. Machine identity, either from an access token in the env (the dominant - // case in CI / containers / Kubernetes pods, where the only credential - // is `INFISICAL_TOKEN` or the UA-scoped env var) or attached by the - // command via AttachTokenIdentity when it logged in with a client - // id/secret. Aligns with the `identity-` distinctId the backend uses - // for MachineIdentityLogin and other identity-scoped events, so CLI - // events flow into the same person record. - // 3. Anonymous fallback keyed by the local machine ID. + // 2. Machine identity attached by the command via AttachTokenIdentity. This + // is the credential the command actually authenticated with, so it wins + // over anything left in the environment; otherwise an unrelated + // `INFISICAL_TOKEN` in the shell would name a different identity than + // the organization the event is grouped under. + // 3. Machine-identity access token from env. The dominant case in CI / + // containers / Kubernetes pods, where the only credential is + // `INFISICAL_TOKEN` or the UA-scoped env var. Aligns with the + // `identity-` distinctId the backend uses for MachineIdentityLogin + // and other identity-scoped events, so CLI events flow into the same + // person record. + // 4. Anonymous fallback keyed by the local machine ID. envIdentityId, _ := machineIdentityClaimsFromEnv() if infisicalConfig.LoggedInUserEmail != "" { distinctId = infisicalConfig.LoggedInUserEmail - } else if envIdentityId != "" { - distinctId = "identity-" + envIdentityId } else if t.attachedIdentityId != "" { distinctId = "identity-" + t.attachedIdentityId + } else if envIdentityId != "" { + distinctId = "identity-" + envIdentityId } else if machineId != "" { distinctId = "anonymous_cli_" + machineId } From 329e2089b57ee38732f9e582f48f50e25b233a48 Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Fri, 31 Jul 2026 00:21:00 +0530 Subject: [PATCH 3/5] fix(agent-proxy): prefer attached identity over stored login, trim event properties The attached machine identity now outranks a persisted human login, so a machine-identity run on a developer host no longer files the event against the human's person record while grouping it under the machine's org. A user session JWT carries no identityId, so login-backed runs still resolve to the email. Also drops port, logFormat and logFileSet, moves the run capture below the sandbox preflight, and reports netDowngraded so a fence that fell back to shared networking is visible. --- packages/cmd/agent_proxy.go | 5 ++--- packages/cmd/agent_proxy_run.go | 37 +++++++++++++++++-------------- packages/cmd/agent_proxy_start.go | 3 --- packages/telemetry/telemetry.go | 25 +++++++++++++++------ 4 files changed, 40 insertions(+), 30 deletions(-) diff --git a/packages/cmd/agent_proxy.go b/packages/cmd/agent_proxy.go index 33bc523e..d4b3a87a 100644 --- a/packages/cmd/agent_proxy.go +++ b/packages/cmd/agent_proxy.go @@ -162,7 +162,6 @@ func runAgentProxyConnect(cmd *cobra.Command, args []string) { token, tokenSource := resolveAgentToken(cmd) - extraNoProxy, _ := cmd.Flags().GetString("no-proxy") allowReadableBrokered := util.GetBoolFlagOrEnv(cmd, "allow-readable-brokered-secrets", util.INFISICAL_AGENT_PROXY_ALLOW_READABLE_BROKERED_SECRETS_NAME) Telemetry.AttachTokenIdentity(token.Token) @@ -170,8 +169,7 @@ func runAgentProxyConnect(cmd *cobra.Command, args []string) { Set("version", util.CLI_VERSION). Set("agent", telemetryAgentName(args)). Set("credentialSource", tokenSource). - Set("allowReadableBrokeredSecrets", allowReadableBrokered). - Set("noProxySet", extraNoProxy != "")) + Set("allowReadableBrokeredSecrets", allowReadableBrokered)) httpClient := resty.New().SetAuthToken(token.Token) @@ -195,6 +193,7 @@ func runAgentProxyConnect(cmd *cobra.Command, args []string) { assertNoBrokeredDynamicSecretsLeasable(leasableDynamicCreds) } + extraNoProxy, _ := cmd.Flags().GetString("no-proxy") env := buildAgentEnv(proxyURL(proxyAddr, projectID, environment, secretPath, token.Token), caPath, token.Token, extraNoProxy, placeholderEnvs, realSecrets) if err := runAgentProcess(args, env); err != nil { diff --git a/packages/cmd/agent_proxy_run.go b/packages/cmd/agent_proxy_run.go index 1cc3abb3..26e10f33 100644 --- a/packages/cmd/agent_proxy_run.go +++ b/packages/cmd/agent_proxy_run.go @@ -129,23 +129,6 @@ func runAgentProxyRun(cmd *cobra.Command, args []string) { extraWrite, _ := cmd.Flags().GetStringArray("allow-write") allowHosts, _ := cmd.Flags().GetStringArray("allow-host") - passEnv, _ := cmd.Flags().GetStringArray("pass-env") - setEnv, _ := cmd.Flags().GetStringArray("set-env") - Telemetry.AttachTokenIdentity(src.token()) - Telemetry.CaptureEvent("cli-command:agent-proxy run", posthog.NewProperties(). - Set("version", util.CLI_VERSION). - Set("agent", telemetryAgentName(args)). - Set("platform", runtime.GOOS). - Set("sandboxEnabled", sandboxEnabled). - Set("sandboxSource", sandboxSource(cmd)). - Set("unmatchedHost", unmatchedHost). - Set("pollInterval", pollInterval). - Set("logFileSet", logFile != ""). - Set("allowReadCount", len(extraRead)). - Set("allowWriteCount", len(extraWrite)). - Set("allowHostCount", len(allowHosts)). - Set("passEnvCount", len(passEnv)). - Set("setEnvCount", len(setEnv))) // macOS keeps the root under ~/.infisical (already sandbox-denied) so it can be trusted once in the // keychain, which is what Go tools like gh need. Elsewhere the injected CA env var is enough. @@ -188,6 +171,26 @@ func runAgentProxyRun(cmd *cobra.Command, args []string) { util.PrintWarning(fmt.Sprintf("Unable to isolate the network on this host (%s), the agent will share your network connection. Requests are still routed through the proxy, but a program that ignores the proxy settings can reach the network directly. Credential protections are unchanged.", pre.Reason)) } + // Captured after preflight so a fence that silently downgraded is visible, and before the + // proxy starts so a run that later dies is still counted. + passEnv, _ := cmd.Flags().GetStringArray("pass-env") + setEnv, _ := cmd.Flags().GetStringArray("set-env") + Telemetry.AttachTokenIdentity(src.token()) + Telemetry.CaptureEvent("cli-command:agent-proxy run", posthog.NewProperties(). + Set("version", util.CLI_VERSION). + Set("agent", telemetryAgentName(args)). + Set("platform", runtime.GOOS). + Set("sandboxEnabled", sandboxEnabled). + Set("sandboxSource", sandboxSource(cmd)). + Set("netDowngraded", pre.FallbackToSharedNet). + Set("unmatchedHost", unmatchedHost). + Set("pollInterval", pollInterval). + Set("allowReadCount", len(extraRead)). + Set("allowWriteCount", len(extraWrite)). + Set("allowHostCount", len(allowHosts)). + Set("passEnvCount", len(passEnv)). + Set("setEnvCount", len(setEnv))) + proxy, err := agentproxy.New(agentproxy.Options{ UnmatchedHost: unmatchedHost, PollInterval: time.Duration(pollInterval) * time.Second, diff --git a/packages/cmd/agent_proxy_start.go b/packages/cmd/agent_proxy_start.go index f71e5ee8..f6645775 100644 --- a/packages/cmd/agent_proxy_start.go +++ b/packages/cmd/agent_proxy_start.go @@ -49,11 +49,8 @@ func runAgentProxyStart(cmd *cobra.Command, args []string) { Telemetry.AttachTokenIdentity(loginResp.AccessToken) Telemetry.CaptureEvent("cli-command:agent-proxy start", posthog.NewProperties(). Set("version", util.CLI_VERSION). - Set("port", port). Set("unmatchedHost", unmatchedHost). Set("pollInterval", pollInterval). - Set("logFormat", logFormat). - Set("logFileSet", logFile != ""). Set("credentialSource", universalAuthCredentialSource(cmd))) log.Info().Msg(color.GreenString("Agent proxy authenticated; starting MITM proxy")) diff --git a/packages/telemetry/telemetry.go b/packages/telemetry/telemetry.go index 2b04ceb0..07994667 100644 --- a/packages/telemetry/telemetry.go +++ b/packages/telemetry/telemetry.go @@ -275,11 +275,22 @@ func (t *Telemetry) GetDistinctId() (string, error) { // those events to a stale `identity-` would corrupt person-level // analytics, while attributing them to the logged-in email is always // correct. - // 2. Machine identity attached by the command via AttachTokenIdentity. This + // 1. Machine identity attached by the command via AttachTokenIdentity. This // is the credential the command actually authenticated with, so it wins - // over anything left in the environment; otherwise an unrelated - // `INFISICAL_TOKEN` in the shell would name a different identity than - // the organization the event is grouped under. + // over both a persisted human login and a stray token in the shell: + // otherwise the event names one actor while being grouped under a + // different actor's organization. Only set when the attached token is a + // machine identity token; a user session JWT carries no `identityId`, so + // a login-backed run still falls through to the email below. + // 2. Logged-in user email from the persisted config. A logged-in user + // takes precedence over any machine-identity token that happens to + // be exported in the shell, because some commands never authenticate + // against the backend at all (e.g. `infisical user switch`, the + // local-config branch of `infisical login`) and others authenticate + // with the user's session JWT rather than the env-token. Attributing + // those events to a stale `identity-` would corrupt person-level + // analytics, while attributing them to the logged-in email is always + // correct. // 3. Machine-identity access token from env. The dominant case in CI / // containers / Kubernetes pods, where the only credential is // `INFISICAL_TOKEN` or the UA-scoped env var. Aligns with the @@ -288,10 +299,10 @@ func (t *Telemetry) GetDistinctId() (string, error) { // person record. // 4. Anonymous fallback keyed by the local machine ID. envIdentityId, _ := machineIdentityClaimsFromEnv() - if infisicalConfig.LoggedInUserEmail != "" { - distinctId = infisicalConfig.LoggedInUserEmail - } else if t.attachedIdentityId != "" { + if t.attachedIdentityId != "" { distinctId = "identity-" + t.attachedIdentityId + } else if infisicalConfig.LoggedInUserEmail != "" { + distinctId = infisicalConfig.LoggedInUserEmail } else if envIdentityId != "" { distinctId = "identity-" + envIdentityId } else if machineId != "" { From c3db5702dc20614995db9038e742426dca7b783e Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Fri, 31 Jul 2026 01:48:31 +0530 Subject: [PATCH 4/5] refactor(agent-proxy): keep tokens out of the telemetry API and trim comments SetActor takes the identity and org ids instead of an access token, so nothing token-shaped crosses into telemetry. Also removes a duplicated comment block and cuts the added comments back. --- packages/cmd/agent_proxy.go | 13 ++---- packages/cmd/agent_proxy_run.go | 10 ++--- packages/cmd/agent_proxy_start.go | 3 +- packages/telemetry/telemetry.go | 75 ++++++++++--------------------- 4 files changed, 33 insertions(+), 68 deletions(-) diff --git a/packages/cmd/agent_proxy.go b/packages/cmd/agent_proxy.go index d4b3a87a..ad908bce 100644 --- a/packages/cmd/agent_proxy.go +++ b/packages/cmd/agent_proxy.go @@ -14,6 +14,7 @@ import ( "github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/models" "github.com/Infisical/infisical-merge/packages/sandbox" + "github.com/Infisical/infisical-merge/packages/telemetry" "github.com/Infisical/infisical-merge/packages/util" "github.com/fatih/color" "github.com/go-resty/resty/v2" @@ -164,7 +165,7 @@ func runAgentProxyConnect(cmd *cobra.Command, args []string) { allowReadableBrokered := util.GetBoolFlagOrEnv(cmd, "allow-readable-brokered-secrets", util.INFISICAL_AGENT_PROXY_ALLOW_READABLE_BROKERED_SECRETS_NAME) - Telemetry.AttachTokenIdentity(token.Token) + Telemetry.SetActor(telemetry.IdentityClaimsFromToken(token.Token)) Telemetry.CaptureEvent("cli-command:agent-proxy connect", posthog.NewProperties(). Set("version", util.CLI_VERSION). Set("agent", telemetryAgentName(args)). @@ -201,9 +202,7 @@ func runAgentProxyConnect(cmd *cobra.Command, args []string) { } } -// telemetryAgentName is the agent executable's name, with the path and every -// argument after it dropped: argv past the first word routinely carries -// credentials, so only the first word is ever reported. +// Only the executable name: argv past the first word routinely carries credentials. func telemetryAgentName(args []string) string { if len(args) == 0 { return "" @@ -211,8 +210,6 @@ func telemetryAgentName(args []string) string { return filepath.Base(args[0]) } -// universalAuthCredentialSource names where the client id/secret came from. Only -// meaningful once a universal auth login is known to be the path taken. func universalAuthCredentialSource(cmd *cobra.Command) string { if cmd.Flags().Changed("client-id") { return "universal-auth-flag" @@ -220,9 +217,7 @@ func universalAuthCredentialSource(cmd *cobra.Command) string { return "universal-auth-env" } -// resolveAgentToken returns the token to authenticate with and a label naming -// which of its branches produced it, so telemetry reports the path actually -// taken rather than re-deriving it and drifting from this function. +// Returns the token and a label for the branch that produced it. func resolveAgentToken(cmd *cobra.Command) (*models.TokenDetails, string) { clientID, _ := util.GetCmdFlagOrEnvWithDefaultValue(cmd, "client-id", []string{util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME}, "") clientSecret, _ := util.GetCmdFlagOrEnvWithDefaultValue(cmd, "client-secret", []string{util.INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET_NAME}, "") diff --git a/packages/cmd/agent_proxy_run.go b/packages/cmd/agent_proxy_run.go index 26e10f33..8c31c76c 100644 --- a/packages/cmd/agent_proxy_run.go +++ b/packages/cmd/agent_proxy_run.go @@ -17,6 +17,7 @@ import ( "github.com/Infisical/infisical-merge/packages/agentproxy" "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/sandbox" + "github.com/Infisical/infisical-merge/packages/telemetry" "github.com/Infisical/infisical-merge/packages/util" "github.com/fatih/color" "github.com/go-resty/resty/v2" @@ -129,7 +130,6 @@ func runAgentProxyRun(cmd *cobra.Command, args []string) { extraWrite, _ := cmd.Flags().GetStringArray("allow-write") allowHosts, _ := cmd.Flags().GetStringArray("allow-host") - // macOS keeps the root under ~/.infisical (already sandbox-denied) so it can be trusted once in the // keychain, which is what Go tools like gh need. Elsewhere the injected CA env var is enough. if runtime.GOOS == "darwin" && home != "" { @@ -171,11 +171,10 @@ func runAgentProxyRun(cmd *cobra.Command, args []string) { util.PrintWarning(fmt.Sprintf("Unable to isolate the network on this host (%s), the agent will share your network connection. Requests are still routed through the proxy, but a program that ignores the proxy settings can reach the network directly. Credential protections are unchanged.", pre.Reason)) } - // Captured after preflight so a fence that silently downgraded is visible, and before the - // proxy starts so a run that later dies is still counted. + // After preflight so a downgraded fence is visible; before the proxy starts so a run that dies still counts. passEnv, _ := cmd.Flags().GetStringArray("pass-env") setEnv, _ := cmd.Flags().GetStringArray("set-env") - Telemetry.AttachTokenIdentity(src.token()) + Telemetry.SetActor(telemetry.IdentityClaimsFromToken(src.token())) Telemetry.CaptureEvent("cli-command:agent-proxy run", posthog.NewProperties(). Set("version", util.CLI_VERSION). Set("agent", telemetryAgentName(args)). @@ -303,8 +302,7 @@ func shutdownProxy(proxy *agentproxy.Proxy) { _ = proxy.Shutdown(ctx) } -// sandboxSource names where the sandbox toggle came from, so a deliberate opt-out is -// distinguishable from the default in telemetry. Mirrors resolveSandboxEnabled's order. +// Mirrors resolveSandboxEnabled's order, so a deliberate opt-out is distinguishable from the default. func sandboxSource(cmd *cobra.Command) string { switch { case cmd.Flags().Changed("sandbox"), cmd.Flags().Changed("no-sandbox"): diff --git a/packages/cmd/agent_proxy_start.go b/packages/cmd/agent_proxy_start.go index f6645775..9185cb2f 100644 --- a/packages/cmd/agent_proxy_start.go +++ b/packages/cmd/agent_proxy_start.go @@ -6,6 +6,7 @@ import ( "time" "github.com/Infisical/infisical-merge/packages/agentproxy" + "github.com/Infisical/infisical-merge/packages/telemetry" "github.com/Infisical/infisical-merge/packages/util" "github.com/fatih/color" "github.com/posthog/posthog-go" @@ -46,7 +47,7 @@ func runAgentProxyStart(cmd *cobra.Command, args []string) { util.HandleError(err, "Failed to authenticate the agent proxy machine identity") } - Telemetry.AttachTokenIdentity(loginResp.AccessToken) + Telemetry.SetActor(telemetry.IdentityClaimsFromToken(loginResp.AccessToken)) Telemetry.CaptureEvent("cli-command:agent-proxy start", posthog.NewProperties(). Set("version", util.CLI_VERSION). Set("unmatchedHost", unmatchedHost). diff --git a/packages/telemetry/telemetry.go b/packages/telemetry/telemetry.go index 07994667..1ffffb68 100644 --- a/packages/telemetry/telemetry.go +++ b/packages/telemetry/telemetry.go @@ -19,10 +19,7 @@ type Telemetry struct { isEnabled bool posthogClient posthog.Client - // Identity attached by AttachTokenIdentity. Long-running commands - // (agent-proxy start/connect/run) authenticate with a client id/secret or a - // flag-supplied token, so there is nothing in the environment for - // machineIdentityClaimsFromEnv to read. + // Set by SetActor for commands whose credential is not in the environment. attachedIdentityId string attachedOrgId string } @@ -73,8 +70,6 @@ func (t *Telemetry) CaptureEvent(eventName string, properties posthog.Properties Properties: properties, } - // Group the event under the organization so usage can be attributed to a - // company, matching the `organization` group type the backend sets. if orgId := t.resolveOrganizationId(); orgId != "" { capture.Groups = posthog.NewGroups().Set("organization", orgId) } @@ -85,12 +80,9 @@ func (t *Telemetry) CaptureEvent(eventName string, properties posthog.Properties } } -// AttachTokenIdentity records the identity and organization carried by an -// Infisical access token the command has already resolved, so events fired -// afterwards are attributed even when no token is exported in the environment. -// Accepts both machine identity tokens and user session JWTs. -func (t *Telemetry) AttachTokenIdentity(token string) { - identityId, orgId := tokenIdentityClaims(token) +// SetActor records who the command is acting as, for commands that resolve their +// own credential and so have nothing in the environment to read. +func (t *Telemetry) SetActor(identityId, orgId string) { t.attachedIdentityId = identityId t.attachedOrgId = orgId } @@ -179,19 +171,14 @@ func (t *Telemetry) IdentifyUserIfNeeded() { } } -// tokenIdentityClaims returns the `identityId` and organization claims carried -// by an Infisical JWT. Machine identity tokens name the organization `orgId` -// and user session tokens name it `organizationId`; both are read. +// IdentityClaimsFromToken reads the identity and organization claims out of an +// Infisical JWT. Nothing from the token itself is kept. Machine identity tokens +// name the organization `orgId`, user session tokens `organizationId`. // -// The function is intentionally best-effort and silent on failure, returning -// empty strings for a service token (`st.` prefix, not a JWT), a malformed -// JWT, or a payload missing the claims. -// -// The token's signature is not verified — the values are only used to derive a -// PostHog distinctId and group, never for authorization. The same token has -// already been (or is about to be) sent to the Infisical API where its -// signature is verified server-side. -func tokenIdentityClaims(token string) (identityId string, orgId string) { +// Best-effort and silent on failure: returns empty strings for a service token +// (`st.` prefix, not a JWT) or a malformed payload. The signature is not +// verified because the values are only used for telemetry, never authorization. +func IdentityClaimsFromToken(token string) (identityId string, orgId string) { if token == "" || strings.HasPrefix(token, "st.") { return "", "" } @@ -225,11 +212,9 @@ func tokenIdentityClaims(token string) (identityId string, orgId string) { return claims.IdentityID, claims.OrganizationID } -// machineIdentityClaimsFromEnv inspects the environment variables that the -// CLI uses to receive machine-identity access tokens (the same set checked -// by util.GetInfisicalToken, minus the `--token` flag which is per-command -// and not visible to the telemetry layer) and returns the identity and -// organization claims from the first one set. +// machineIdentityClaimsFromEnv returns the claims from the first machine-identity +// access token set in the environment (the same set util.GetInfisicalToken checks, +// minus the `--token` flag, which is per-command and not visible here). func machineIdentityClaimsFromEnv() (identityId string, orgId string) { // Mirror the env-var precedence in util.GetInfisicalToken so that the // telemetry distinctId aligns with the credential the API call will @@ -245,7 +230,7 @@ func machineIdentityClaimsFromEnv() (identityId string, orgId string) { for _, name := range envVars { if v := os.Getenv(name); v != "" { - return tokenIdentityClaims(v) + return IdentityClaimsFromToken(v) } } @@ -266,22 +251,8 @@ func (t *Telemetry) GetDistinctId() (string, error) { } // Resolution priority: - // 1. Logged-in user email from the persisted config. A logged-in user - // takes precedence over any machine-identity token that happens to - // be exported in the shell, because some commands never authenticate - // against the backend at all (e.g. `infisical user switch`, the - // local-config branch of `infisical login`) and others authenticate - // with the user's session JWT rather than the env-token. Attributing - // those events to a stale `identity-` would corrupt person-level - // analytics, while attributing them to the logged-in email is always - // correct. - // 1. Machine identity attached by the command via AttachTokenIdentity. This - // is the credential the command actually authenticated with, so it wins - // over both a persisted human login and a stray token in the shell: - // otherwise the event names one actor while being grouped under a - // different actor's organization. Only set when the attached token is a - // machine identity token; a user session JWT carries no `identityId`, so - // a login-backed run still falls through to the email below. + // 1. Actor set by SetActor: the credential the command itself resolved, so it + // outranks anything on the machine. Empty for a user session token. // 2. Logged-in user email from the persisted config. A logged-in user // takes precedence over any machine-identity token that happens to // be exported in the shell, because some commands never authenticate @@ -291,12 +262,12 @@ func (t *Telemetry) GetDistinctId() (string, error) { // those events to a stale `identity-` would corrupt person-level // analytics, while attributing them to the logged-in email is always // correct. - // 3. Machine-identity access token from env. The dominant case in CI / - // containers / Kubernetes pods, where the only credential is - // `INFISICAL_TOKEN` or the UA-scoped env var. Aligns with the - // `identity-` distinctId the backend uses for MachineIdentityLogin - // and other identity-scoped events, so CLI events flow into the same - // person record. + // 3. Machine-identity access token from env. This is the dominant case + // in CI / containers / Kubernetes pods, where there is no logged-in + // user and the only credential is `INFISICAL_TOKEN` (or the UA-scoped + // env var). Aligns with the `identity-` distinctId the backend + // uses for MachineIdentityLogin and other identity-scoped events, + // so CLI events flow into the same person record. // 4. Anonymous fallback keyed by the local machine ID. envIdentityId, _ := machineIdentityClaimsFromEnv() if t.attachedIdentityId != "" { From 01f66dbbef02ad1a212ab4a5a52a1c5550552e53 Mon Sep 17 00:00:00 2001 From: saif <11242541+saifsmailbox98@users.noreply.github.com> Date: Fri, 31 Jul 2026 02:17:41 +0530 Subject: [PATCH 5/5] fix(agent-proxy): keep env token parsing lazy and scope org resolution to the set actor --- packages/telemetry/telemetry.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/telemetry/telemetry.go b/packages/telemetry/telemetry.go index 1ffffb68..b568304f 100644 --- a/packages/telemetry/telemetry.go +++ b/packages/telemetry/telemetry.go @@ -87,8 +87,10 @@ func (t *Telemetry) SetActor(identityId, orgId string) { t.attachedOrgId = orgId } +// Once an actor is set, its organization is authoritative even when empty: falling +// back to the environment would group the event under a different actor's org. func (t *Telemetry) resolveOrganizationId() string { - if t.attachedOrgId != "" { + if t.attachedIdentityId != "" || t.attachedOrgId != "" { return t.attachedOrgId } _, orgId := machineIdentityClaimsFromEnv() @@ -269,12 +271,11 @@ func (t *Telemetry) GetDistinctId() (string, error) { // uses for MachineIdentityLogin and other identity-scoped events, // so CLI events flow into the same person record. // 4. Anonymous fallback keyed by the local machine ID. - envIdentityId, _ := machineIdentityClaimsFromEnv() if t.attachedIdentityId != "" { distinctId = "identity-" + t.attachedIdentityId } else if infisicalConfig.LoggedInUserEmail != "" { distinctId = infisicalConfig.LoggedInUserEmail - } else if envIdentityId != "" { + } else if envIdentityId, _ := machineIdentityClaimsFromEnv(); envIdentityId != "" { distinctId = "identity-" + envIdentityId } else if machineId != "" { distinctId = "anonymous_cli_" + machineId