Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
bdd1fac
Openapi for network rules
sitole Apr 28, 2026
b25dc55
Orchestrator gRPC for network rules
sitole Apr 28, 2026
8569059
Bump orchestrator verions with network rules support
sitole Apr 28, 2026
6627f22
Validate and propagate sbx network rules to orchestrator
sitole Apr 28, 2026
10844a3
Use static domain validator
sitole Apr 28, 2026
bdf8920
Tests for network rules object
sitole Apr 28, 2026
084caa7
chore: auto-commit generated changes
github-actions[bot] Apr 28, 2026
66ce87e
Posthog events
sitole Apr 28, 2026
192fc67
Update comment about domain patterns in openapi
sitole Apr 28, 2026
202ebb6
api: reject header names/values containing CR or LF characters
sitole Apr 28, 2026
8bb62c9
api: clone headers map in dbNetworkConfigToAPI
sitole Apr 28, 2026
4c96456
api: fix posthog event label in network update handler
sitole Apr 28, 2026
f88af9e
Merge branch 'main' into feat/api-for-setting-up-domain-transform-eng…
sitole Apr 29, 2026
5db6f5c
api/handlers: tighten network rule header limits
sitole Apr 29, 2026
64dc39a
api/orchestrator: keep Rules nil in egress config when no rules given
sitole Apr 29, 2026
3917154
Use native tool to validate header name
sitole Apr 29, 2026
5830584
fix inverted if
sitole Apr 29, 2026
dc81689
Fixed error message for invalid header name format
sitole Apr 30, 2026
78b77c6
Transform rules can only apply to template builds with supported envd…
sitole Apr 30, 2026
a0fe0c0
Use `httpguts.ValidHeaderFieldValue` to validate transform header values
sitole Apr 30, 2026
3962ca5
api: return 400 when envd version is missing in version checks
sitole May 4, 2026
3f27bb2
api,orchestrator: fix test and nil-guard for network rules
sitole May 4, 2026
3257546
api: improve errNoEnvdVersion message for client clarity
sitole May 4, 2026
7122d8b
api: skip GetSandbox when no rules are provided in network update
sitole May 4, 2026
bdcf334
Change openapi description for sbx network update endpoint
sitole May 4, 2026
51fcafd
api/nodemanager: restore domain transform rules on sandbox sync
sitole May 4, 2026
12e4a0a
chore: auto-commit generated changes
github-actions[bot] May 4, 2026
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
1 change: 1 addition & 0 deletions packages/api/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ tool (
)

require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/bsm/redislock v0.9.4
github.com/caarlos0/env/v11 v11.3.1
github.com/e2b-dev/infra/packages/auth v0.0.0
Expand Down
2 changes: 2 additions & 0 deletions packages/api/go.sum

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

347 changes: 185 additions & 162 deletions packages/api/internal/api/api.gen.go

Large diffs are not rendered by default.

174 changes: 171 additions & 3 deletions packages/api/internal/handlers/sandbox_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import (
"strings"
"time"

"github.com/asaskevich/govalidator"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/launchdarkly/go-sdk-common/v3/ldcontext"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
"golang.org/x/net/http/httpguts"
"golang.org/x/net/idna"

"github.com/e2b-dev/infra/packages/api/internal/api"
Expand Down Expand Up @@ -47,6 +49,13 @@ const (

// Network validation error messages
ErrMsgDomainsRequireBlockAll = "When specifying allowed domains in allow out, you must include 'ALL_TRAFFIC' in deny out to block all other traffic."

maxNetworkRuleDomains = 10
maxNetworkRuleTransformsPerDomain = 1
maxNetworkRuleDomainLen = 128
maxNetworkRuleHeaderNameLen = 64
Comment thread
sitole marked this conversation as resolved.
maxNetworkRuleHeaderValueLen = 2048
maxNetworkRuleHeadersPerRule = 20
)

func (a *APIStore) PostSandboxes(c *gin.Context) {
Expand Down Expand Up @@ -174,7 +183,7 @@ func (a *APIStore) PostSandboxes(c *gin.Context) {

var network *types.SandboxNetworkConfig
if n := body.Network; n != nil {
if err := validateNetworkConfig(n); err != nil {
if err := validateNetworkConfig(ctx, a.featureFlags, teamInfo.Team.ID, n); err != nil {
telemetry.ReportError(ctx, "invalid network config", err.Err, telemetry.WithSandboxID(sandboxID))
a.sendAPIStoreError(c, err.Code, err.ClientMsg)

Expand All @@ -189,6 +198,7 @@ func (a *APIStore) PostSandboxes(c *gin.Context) {
Egress: &types.SandboxNetworkEgressConfig{
AllowedAddresses: sharedUtils.DerefOrDefault(n.AllowOut, nil),
DeniedAddresses: sharedUtils.DerefOrDefault(n.DenyOut, nil),
Rules: apiRulesToDBRules(n.Rules),
},
}

Expand Down Expand Up @@ -265,6 +275,19 @@ func (a *APIStore) PostSandboxes(c *gin.Context) {
return
}

if n := body.Network; n != nil && n.Rules != nil && len(*n.Rules) > 0 {
domains := make([]string, 0, len(*n.Rules))
for domain := range *n.Rules {
domains = append(domains, domain)
}

a.posthog.CreateAnalyticsTeamEvent(ctx, teamInfo.Team.ID.String(), "sandbox with network transform rules created",
a.posthog.GetPackageToPosthogProperties(&c.Request.Header).
Set("sandbox_id", sandboxID).
Set("domains", domains),
Comment thread
sitole marked this conversation as resolved.
)
}

c.JSON(http.StatusCreated, &sbx)
}

Expand Down Expand Up @@ -514,7 +537,33 @@ func splitHostPortOptional(hostport string) (host string, port string, err error
return host, port, nil
}

func validateNetworkConfig(network *api.SandboxNetworkConfig) *api.APIError {
func apiRulesToDBRules(apiRules *map[string][]api.SandboxNetworkRule) map[string][]types.SandboxNetworkRule {
if apiRules == nil {
return nil
}

dbRules := make(map[string][]types.SandboxNetworkRule, len(*apiRules))
for domain, rules := range *apiRules {
dbDomainRules := make([]types.SandboxNetworkRule, 0, len(rules))
for _, r := range rules {
dbRule := types.SandboxNetworkRule{}

if r.Transform != nil {
dbRule.Transform = &types.SandboxNetworkTransform{
Headers: sharedUtils.DerefOrDefault(r.Transform.Headers, nil),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Agentic Security Review
Severity: HIGH
Description: Network transform header values are persisted directly from API input into paused sandbox config (Headers: ...r.Transform.Headers...). The API contract states secret resolution happens client-side before request submission, so these values can include resolved secrets that are then stored and later re-exposed through sandbox read flows.
Impact: Secret header values can persist in plaintext across DB snapshots/backups and become visible to broader internal read paths than the original secret manager boundary.

}
}

dbDomainRules = append(dbDomainRules, dbRule)
}

dbRules[domain] = dbDomainRules
}

return dbRules
}

func validateNetworkConfig(ctx context.Context, featureFlags featureFlagsClient, teamID uuid.UUID, network *api.SandboxNetworkConfig) *api.APIError {
if network == nil {
return nil
}
Expand Down Expand Up @@ -550,7 +599,11 @@ func validateNetworkConfig(network *api.SandboxNetworkConfig) *api.APIError {
denyOut := sharedUtils.DerefOrDefault(network.DenyOut, nil)
allowOut := sharedUtils.DerefOrDefault(network.AllowOut, nil)

return validateEgressRules(allowOut, denyOut)
if err := validateEgressRules(allowOut, denyOut); err != nil {
return err
}

return validateNetworkRules(ctx, featureFlags, teamID, network.Rules)
Comment thread
sitole marked this conversation as resolved.
Outdated
}

// validateEgressRules validates egress allow/deny rules:
Expand Down Expand Up @@ -593,3 +646,118 @@ func validateEgressRules(allowOut, denyOut []string) *api.APIError {

return nil
}

func validateNetworkRules(ctx context.Context, featureFlags featureFlagsClient, teamID uuid.UUID, rules *map[string][]api.SandboxNetworkRule) *api.APIError {
if rules == nil {
return nil
}

if !featureFlags.BoolFlag(ctx, featureflags.NetworkTransformRulesFlag, featureflags.TeamContext(teamID.String())) {
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("team %s is not allowed to use network transform rules", teamID),
ClientMsg: "Network transform rules are not available for your team.",
}
}

if len(*rules) > maxNetworkRuleDomains {
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("too many rule domains: %d (max %d)", len(*rules), maxNetworkRuleDomains),
ClientMsg: fmt.Sprintf("Network rules can have at most %d domains.", maxNetworkRuleDomains),
}
}

for domain, domainRules := range *rules {
if len(domain) == 0 {
return &api.APIError{
Code: http.StatusBadRequest,
Err: errors.New("rule domain must not be empty"),
ClientMsg: "Rule domain must not be empty.",
}
}

if len(domain) > maxNetworkRuleDomainLen {
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("rule domain %q exceeds max length %d", domain, maxNetworkRuleDomainLen),
ClientMsg: fmt.Sprintf("Rule domain %q exceeds maximum length of %d characters.", domain, maxNetworkRuleDomainLen),
}
}

if !govalidator.IsDNSName(domain) {
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("rule domain %q is not a valid domain", domain),
ClientMsg: fmt.Sprintf("Rule domain %q is not a valid domain name.", domain),
}
}

if len(domainRules) > maxNetworkRuleTransformsPerDomain {
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("domain %q has %d transforms (max %d)", domain, len(domainRules), maxNetworkRuleTransformsPerDomain),
ClientMsg: fmt.Sprintf("Domain %q can have at most %d transform rule.", domain, maxNetworkRuleTransformsPerDomain),
}
}

for _, rule := range domainRules {
if rule.Transform == nil {
continue
}

headers := sharedUtils.DerefOrDefault(rule.Transform.Headers, nil)
if len(headers) > maxNetworkRuleHeadersPerRule {
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("domain %q has %d headers (max %d)", domain, len(headers), maxNetworkRuleHeadersPerRule),
ClientMsg: fmt.Sprintf("Domain %q can have at most %d headers per rule.", domain, maxNetworkRuleHeadersPerRule),
}
}

for name, value := range headers {
if len(name) == 0 {
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("header name in rule for domain %q must not be empty", domain),
ClientMsg: fmt.Sprintf("Header name in rule for domain %q must not be empty.", domain),
}
}

if !httpguts.ValidHeaderFieldName(name) {
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("header name %q in rule for domain %q contains invalid characters", name, domain),
ClientMsg: fmt.Sprintf("Header name %q in rule for domain %q must not contain CR or LF characters.", name, domain),
}
}
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
sitole marked this conversation as resolved.

if len(name) > maxNetworkRuleHeaderNameLen {
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("header name %q in rule for domain %q exceeds max length %d", name, domain, maxNetworkRuleHeaderNameLen),
ClientMsg: fmt.Sprintf("Header name %q in rule for domain %q exceeds maximum length of %d characters.", name, domain, maxNetworkRuleHeaderNameLen),
}
}

if strings.ContainsAny(value, "\r\n") {
Comment thread
sitole marked this conversation as resolved.
Outdated
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("value for header %q in rule for domain %q contains invalid characters", name, domain),
ClientMsg: fmt.Sprintf("Value for header %q in rule for domain %q must not contain CR or LF characters.", name, domain),
}
}

if len(value) > maxNetworkRuleHeaderValueLen {
Comment thread
sitole marked this conversation as resolved.
return &api.APIError{
Code: http.StatusBadRequest,
Err: fmt.Errorf("value for header %q in rule for domain %q exceeds max length %d", name, domain, maxNetworkRuleHeaderValueLen),
ClientMsg: fmt.Sprintf("Value for header %q in rule for domain %q exceeds maximum length of %d characters.", name, domain, maxNetworkRuleHeaderValueLen),
}
}
}
}
}

return nil
}
Loading
Loading