Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/docker-reverse-proxy/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/e2b-dev/infra/packages/shared v0.0.0
github.com/google/uuid v1.6.0
github.com/jellydator/ttlcache/v3 v3.4.0
github.com/launchdarkly/go-server-sdk/v7 v7.13.0
github.com/stretchr/testify v1.11.1
)

Expand All @@ -35,16 +36,26 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.10.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/launchdarkly/ccache v1.1.0 // indirect
github.com/launchdarkly/eventsource v1.10.0 // indirect
github.com/launchdarkly/go-jsonstream/v3 v3.1.0 // indirect
github.com/launchdarkly/go-sdk-common/v3 v3.3.0 // indirect
github.com/launchdarkly/go-sdk-events/v3 v3.5.0 // indirect
github.com/launchdarkly/go-semver v1.0.3 // indirect
github.com/launchdarkly/go-server-sdk-evaluation/v3 v3.0.1 // indirect
github.com/lib/pq v1.11.2 // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.2.0 // indirect
Expand All @@ -57,6 +68,7 @@ require (
github.com/moby/term v0.5.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/pressly/goose/v3 v3.27.2 // indirect
Expand All @@ -78,6 +90,7 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sync v0.21.0 // indirect
Expand Down
33 changes: 33 additions & 0 deletions packages/docker-reverse-proxy/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 9 additions & 5 deletions packages/docker-reverse-proxy/internal/auth/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"log"
"strings"

"github.com/google/uuid"

"github.com/e2b-dev/infra/packages/db/client"
authdb "github.com/e2b-dev/infra/packages/db/pkg/auth"
"github.com/e2b-dev/infra/packages/db/queries"
Expand All @@ -30,20 +32,22 @@ func Validate(ctx context.Context, sqlcDB *client.Client, token, envID string) (
return exists, nil
}

func ValidateAccessToken(ctx context.Context, db *authdb.Client, accessToken string) bool {
// ValidateAccessToken verifies the token format and DB presence and returns
// the owning user ID so callers can evaluate per-user feature flags.
func ValidateAccessToken(ctx context.Context, db *authdb.Client, accessToken string) (uuid.UUID, bool) {
hashedToken, err := keys.VerifyKey(keys.AccessTokenPrefix, accessToken)
if err != nil {
return false
return uuid.UUID{}, false
}

_, err = db.Read.GetUserIDFromAccessToken(ctx, hashedToken)
userID, err := db.Read.GetUserIDFromAccessToken(ctx, hashedToken)
if err != nil {
log.Printf("Error while checking access token: %s\n", err.Error())

return false
return uuid.UUID{}, false
}

return true
return userID, true
}

func ExtractAccessToken(authHeader, authType string) (string, error) {
Expand Down
24 changes: 16 additions & 8 deletions packages/docker-reverse-proxy/internal/handlers/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@
"github.com/e2b-dev/infra/packages/db/pkg/pool"
"github.com/e2b-dev/infra/packages/docker-reverse-proxy/internal/cache"
"github.com/e2b-dev/infra/packages/shared/pkg/consts"
"github.com/e2b-dev/infra/packages/shared/pkg/featureflags"
"github.com/e2b-dev/infra/packages/shared/pkg/utils"
)

type APIStore struct {
db *client.Client
authDb *authdb.Client
AuthCache *cache.AuthCache
proxy *httputil.ReverseProxy
db *client.Client
authDb *authdb.Client
AuthCache *cache.AuthCache
proxy *httputil.ReverseProxy
featureFlags *featureflags.Client
}

func NewStore(ctx context.Context) *APIStore {
Expand All @@ -38,6 +40,11 @@
log.Fatal(err)
}

featureFlags, err := featureflags.NewClient()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass the LaunchDarkly key to the proxy job

In the GCP Nomad config I checked, docker_reverse_proxy_env_vars only passes the Postgres/Google/GCP/domain values (iac/provider-gcp/main.tf:188-195), while NewClient() falls back to the offline datasource whenever LAUNCH_DARKLY_API_KEY is empty (packages/shared/pkg/featureflags/client.go:58-61). Since this commit creates the LD client here but does not wire that env var into the docker-reverse-proxy job, deployed proxy instances will always evaluate disable-e2b-access-token-auth as its default false and will continue accepting deprecated access-token logins even when the flag is enabled.

Useful? React with 👍 / 👎.

if err != nil {
log.Fatal(err)
}

Check failure on line 46 in packages/docker-reverse-proxy/internal/handlers/store.go

View check run for this annotation

Claude / Claude Code Review

docker-reverse-proxy Nomad job missing LAUNCH_DARKLY_API_KEY; flag silently no-ops in prod

The Nomad job for docker-reverse-proxy is missing `LAUNCH_DARKLY_API_KEY` — `docker_reverse_proxy_env_vars` in `iac/provider-gcp/main.tf` (L188-195) is the only service block that omits it, while api/client-proxy/orchestrator/template-manager/filestore-cleanup all set it. Without the key, `featureflags.NewClient()` silently falls back to the offline store and `DisableE2BAccessTokenAuthFlag` always resolves to its `false` fallback in prod, so the LD targeting this PR adds cannot actually gate any

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LaunchDarkly unset in deployment

High Severity

featureflags.NewClient() is initialized here, but the docker-reverse-proxy Nomad job env (docker_reverse_proxy_env_vars) does not include LAUNCH_DARKLY_API_KEY, unlike API and template-manager. Without that variable the client uses the offline datasource, so disable-e2b-access-token-auth stays at its false fallback and per-user LD rejection never applies to V1 docker login in production.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a5ba40d. Configure here.

Comment on lines +43 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The Nomad job for docker-reverse-proxy is missing LAUNCH_DARKLY_API_KEY — docker_reverse_proxy_env_vars in iac/provider-gcp/main.tf (L188-195) is the only service block that omits it, while api/client-proxy/orchestrator/template-manager/filestore-cleanup all set it. Without the key, featureflags.NewClient() silently falls back to the offline store and DisableE2BAccessTokenAuthFlag always resolves to its false fallback in prod, so the LD targeting this PR adds cannot actually gate anything. Fix: add LAUNCH_DARKLY_API_KEY = trimspace(data.google_secret_manager_secret_version.launch_darkly_api_key.secret_data) to docker_reverse_proxy_env_vars, mirroring the sibling services.

Extended reasoning...

What breaks

This PR wires a LaunchDarkly-backed gate into docker-reverse-proxy for the V1 build docker login path (packages/docker-reverse-proxy/internal/handlers/token.go:54), using DisableE2BAccessTokenAuthFlag evaluated with a per-user context. The PR description explicitly states the flag is meant to be "rolled out per-user via LD targeting during the deprecation cutover". However, no LaunchDarkly connection can be established in production, so the gate silently never activates.

Code path

  1. packages/docker-reverse-proxy/internal/handlers/store.go:43 calls featureflags.NewClient() at startup.
  2. packages/shared/pkg/featureflags/client.go:23 sets launchDarklyApiKey = os.Getenv("LAUNCH_DARKLY_API_KEY") at package init.
  3. NewClient() at client.go:58-61:
    if launchDarklyApiKey == "" {
        return NewClientWithDatasource(launchDarklyOfflineStore)
    }
    Empty key → offline test data source, no error, no warning. The offline store is seeded via NewBoolFlag(...).VariationForAll(fallback) (flags.go:96-100), so BoolFlag returns the compile-time fallback (false) for every user.
  4. iac/provider-gcp/main.tf:188-195 defines:
    docker_reverse_proxy_env_vars = merge({
      POSTGRES_CONNECTION_STRING    = ...
      GOOGLE_SERVICE_ACCOUNT_BASE64 = ...
      GCP_REGION                    = var.gcp_region
      GCP_PROJECT_ID                = var.gcp_project_id
      GCP_DOCKER_REPOSITORY_NAME    = ...
      DOMAIN_NAME                   = var.domain_name
    }, var.docker_reverse_proxy_env_vars)
    LAUNCH_DARKLY_API_KEY is absent. Every other flag-consuming block sets it: api (L109), client-proxy (L136), orchestrator (L162), template-manager (L185), filestore-cleanup (L198).
  5. iac/provider-gcp/nomad/jobs/docker-reverse-proxy.hcl:45-49 renders the container env block purely from job_env_vars, so a missing entry in the terraform local means a missing env var in the container.

Step-by-step proof

  1. Ops toggles disable-e2b-access-token-auth ON in LaunchDarkly (e.g. targeting a canary user cohort) expecting docker-reverse-proxy to start rejecting E2B_ACCESS_TOKEN-based docker login on the V1 build path.
  2. A canary user runs docker login against the reverse proxy. The Authorization header carries their access token.
  3. GetToken calls auth.ValidateAccessToken, which returns their real userID.
  4. Line 54 evaluates a.featureFlags.BoolFlag(ctx, DisableE2BAccessTokenAuthFlag, UserContext(userID.String())).
  5. featureFlags.ld is the offline-datasource client (because LAUNCH_DARKLY_API_KEY was empty at process start). It ignores the real LD environment entirely — the datasource is fed only from launchDarklyOfflineStore, which for this flag holds VariationForAll(false).
  6. BoolFlag returns false. The 403 branch is skipped. A docker token is issued.
  7. Result: LD targeting has zero effect on this surface, no matter how ops configures it. The deprecation cutover cannot be executed for the V1 docker-login path.

Why existing code doesn't prevent it

  • NewClient() treats an empty key as "run offline" — not an error. No log, no warning, no metric. The service boots cleanly and appears healthy.
  • The unit test in token_test.go uses ldtestdata.DataSource() directly, so it exercises the flag logic but cannot detect a missing production env-var wiring.
  • No terraform validation or CI test asserts that a service reaching featureflags.NewClient() also has LAUNCH_DARKLY_API_KEY set.

Fix

One-line addition to iac/provider-gcp/main.tf mirroring the sibling services:

docker_reverse_proxy_env_vars = merge({
  POSTGRES_CONNECTION_STRING    = ...
  # ...
  DOMAIN_NAME                   = var.domain_name
  LAUNCH_DARKLY_API_KEY         = trimspace(data.google_secret_manager_secret_version.launch_darkly_api_key.secret_data)
}, var.docker_reverse_proxy_env_vars)

Also worth auditing any other deployment providers (AWS self-host) for the same omission, since this is a new dependency for docker-reverse-proxy.


targetUrl := &url.URL{
Scheme: "https",
Host: fmt.Sprintf("%s-docker.pkg.dev", consts.GCPRegion),
Expand All @@ -57,10 +64,11 @@
}

return &APIStore{
db: database,
authDb: authDatabase,
AuthCache: authCache,
proxy: proxy,
db: database,
authDb: authDatabase,
AuthCache: authCache,
proxy: proxy,
featureFlags: featureFlags,
}
}

Expand Down
13 changes: 12 additions & 1 deletion packages/docker-reverse-proxy/internal/handlers/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/e2b-dev/infra/packages/docker-reverse-proxy/internal/auth"
"github.com/e2b-dev/infra/packages/shared/pkg/consts"
"github.com/e2b-dev/infra/packages/shared/pkg/featureflags"
)

type DockerToken struct {
Expand All @@ -39,7 +40,8 @@ func (a *APIStore) GetToken(w http.ResponseWriter, r *http.Request) error {
return fmt.Errorf("error while extracting access token: %w", err)
}

if !auth.ValidateAccessToken(ctx, a.authDb, accessToken) {
userID, ok := auth.ValidateAccessToken(ctx, a.authDb, accessToken)
if !ok {
log.Printf("Invalid access token: '%s'\n", accessToken)

w.WriteHeader(http.StatusForbidden)
Expand All @@ -48,6 +50,15 @@ func (a *APIStore) GetToken(w http.ResponseWriter, r *http.Request) error {
return errors.New("invalid access token")
}

// Access token acceptance is gated after validation so the flag can be
// rolled out per-user via LD targeting during the deprecation cutover.
if a.featureFlags.BoolFlag(ctx, featureflags.DisableE2BAccessTokenAuthFlag, featureflags.UserContext(userID.String())) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recheck the flag for cached Docker bearer tokens

When using this flag for the deprecation cutover, this check only blocks new /v2/token exchanges; scoped bearer tokens already returned by AuthCache.Create are later accepted by Proxy/LoginWithToken via cache lookup only (internal/handlers/proxy.go:21, internal/handlers/login.go:15) and cache entries live for two hours. A targeted user who obtained a scoped Docker token before the flag flipped can therefore continue pushing/pulling through the proxy until that cache entry expires, so the per-user cutoff is not enforced when the flag is enabled.

Useful? React with 👍 / 👎.

w.WriteHeader(http.StatusForbidden)
w.Write([]byte("E2B_ACCESS_TOKEN is deprecated and no longer accepted. Use an API key (E2B_API_KEY) instead. See https://e2b.dev/docs/migration/access-token-deprecation"))

return errors.New("access token authentication is disabled")
}

scope := r.URL.Query().Get("scope")
hasScope := scope != ""

Expand Down
106 changes: 106 additions & 0 deletions packages/docker-reverse-proxy/internal/handlers/token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package handlers

import (
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/google/uuid"
"github.com/launchdarkly/go-server-sdk/v7/testhelpers/ldtestdata"
"github.com/stretchr/testify/require"

authqueries "github.com/e2b-dev/infra/packages/db/pkg/auth/queries"
"github.com/e2b-dev/infra/packages/db/pkg/testutils"
"github.com/e2b-dev/infra/packages/docker-reverse-proxy/internal/cache"
"github.com/e2b-dev/infra/packages/shared/pkg/featureflags"
"github.com/e2b-dev/infra/packages/shared/pkg/keys"
)

func newTokenTestStore(t *testing.T, accessTokenAuthDisabled bool) (*APIStore, keys.Key) {
t.Helper()

td := ldtestdata.DataSource()
td.Update(td.Flag(featureflags.DisableE2BAccessTokenAuthFlag.Key()).VariationForAll(accessTokenAuthDisabled))
ff, err := featureflags.NewClientWithDatasource(td)
require.NoError(t, err)
t.Cleanup(func() { _ = ff.Close(t.Context()) })

db := testutils.SetupDatabase(t)

accessToken, err := keys.GenerateKey(keys.AccessTokenPrefix)
require.NoError(t, err)

userID := uuid.New()
require.NoError(t, db.AuthDB.Write.UpsertPublicUser(t.Context(), userID))

_, err = db.AuthDB.Write.CreateAccessToken(t.Context(), authqueries.CreateAccessTokenParams{
ID: uuid.New(),
UserID: userID,
AccessTokenHash: accessToken.HashedValue,
AccessTokenPrefix: accessToken.Masked.Prefix,
AccessTokenLength: int32(accessToken.Masked.ValueLength),
AccessTokenMaskPrefix: accessToken.Masked.MaskedValuePrefix,
AccessTokenMaskSuffix: accessToken.Masked.MaskedValueSuffix,
Name: "Test token",
})
require.NoError(t, err)

return &APIStore{
db: db.SqlcClient,
authDb: db.AuthDB,
AuthCache: cache.New(),
featureFlags: ff,
}, accessToken
}

func newTokenRequest(t *testing.T, rawAccessToken string) *http.Request {
t.Helper()

req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/v2/token", nil)
loginInfo := base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "_e2b_access_token:%s", rawAccessToken))
req.Header.Set("Authorization", "Basic "+loginInfo)

return req
}

func TestGetTokenAcceptsAccessTokenWhenAuthEnabled(t *testing.T) {
t.Parallel()

store, accessToken := newTokenTestStore(t, false)

recorder := httptest.NewRecorder()
err := store.GetToken(recorder, newTokenRequest(t, accessToken.PrefixedRawValue))

require.NoError(t, err)
require.Equal(t, http.StatusOK, recorder.Code)
require.Contains(t, recorder.Body.String(), "token")
}

func TestGetTokenRejectsAccessTokenWhenAuthDisabled(t *testing.T) {
t.Parallel()

store, accessToken := newTokenTestStore(t, true)

recorder := httptest.NewRecorder()
err := store.GetToken(recorder, newTokenRequest(t, accessToken.PrefixedRawValue))

require.Error(t, err)
require.Equal(t, http.StatusForbidden, recorder.Code)
require.Contains(t, recorder.Body.String(), "E2B_API_KEY")
require.Contains(t, recorder.Body.String(), "https://e2b.dev/docs/migration/access-token-deprecation")
}

func TestGetTokenRejectsInvalidAccessTokenRegardlessOfFlag(t *testing.T) {
t.Parallel()

store, _ := newTokenTestStore(t, true)

recorder := httptest.NewRecorder()
err := store.GetToken(recorder, newTokenRequest(t, keys.AccessTokenPrefix+"invalid"))

require.Error(t, err)
require.Equal(t, http.StatusForbidden, recorder.Code)
require.Contains(t, recorder.Body.String(), "invalid access token")
}
11 changes: 6 additions & 5 deletions packages/shared/pkg/featureflags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,12 @@ var (
// default so issuance keeps working until the deprecation cutover.
DisableE2BAccessTokenProvisioningFlag = NewBoolFlag("disable-e2b-access-token-provisioning", false)

// DisableE2BAccessTokenAuthFlag stops the API from accepting E2B access
// tokens (sk_e2b_) for authentication once enabled. E2B_ACCESS_TOKEN is
// superseded by E2B_API_KEY; existing tokens stop working on the
// deprecation cutover (Aug 1, 2026). Off by default. Evaluated per-user so
// rejection can be rolled out gradually via LD targeting.
// DisableE2BAccessTokenAuthFlag stops the API and docker-reverse-proxy
// (V1 build docker login) from accepting E2B access tokens (sk_e2b_) for
// authentication once enabled. E2B_ACCESS_TOKEN is deprecated in favor of
// E2B_API_KEY; existing tokens stop working on the deprecation cutover
// (Aug 1, 2026). Off by default. Evaluated per-user so rejection can be
// rolled out gradually via LD targeting.
DisableE2BAccessTokenAuthFlag = NewBoolFlag("disable-e2b-access-token-auth", false)
)

Expand Down