diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go
index a4ef2ee57a..af8c7d6d44 100644
--- a/api/v1alpha1/envoyproxy_types.go
+++ b/api/v1alpha1/envoyproxy_types.go
@@ -35,6 +35,7 @@ type EnvoyProxy struct {
}
// EnvoyProxySpec defines the desired state of EnvoyProxy.
+// +kubebuilder:validation:XValidation:rule="!has(self.luaStrictValidation) || !has(self.luaValidation) || self.luaValidation == 'Strict'",message="luaStrictValidation can only be set when luaValidation is Strict"
type EnvoyProxySpec struct {
// Provider defines the desired resource provider and provider-specific configuration.
// If unspecified, the "Kubernetes" resource provider is used with default configuration
@@ -189,6 +190,20 @@ type EnvoyProxySpec struct {
// +optional
LuaValidation *LuaValidation `json:"luaValidation,omitempty"`
+ // LuaStrictValidation defines the filesystem paths and environment variables that Lua
+ // scripts from EnvoyExtensionPolicy resources are permitted to access during Strict
+ // validation in the gateway controller.
+ //
+ // The allowlist is fail-closed: when unset or empty, Lua scripts are denied access to ALL
+ // filesystem paths and environment variables during validation. Only entries that match the
+ // allowlist are permitted.
+ //
+ // This field only takes effect when LuaValidation is Strict (the default). It has no effect
+ // for the InsecureSyntax or Disabled validation modes, which do not execute the security sandbox.
+ //
+ // +optional
+ LuaStrictValidation *LuaStrictValidation `json:"luaStrictValidation,omitempty"`
+
// DynamicModules defines the set of dynamic modules that are allowed to be
// used by EnvoyExtensionPolicy resources and dynamic module load balancer
// policies. Each entry registers a module by a logical name and specifies
@@ -250,6 +265,39 @@ const (
LuaValidationDisabled LuaValidation = "Disabled"
)
+// LuaStrictValidation defines the configuration that Strict Lua validation runs with.
+//
+// This configuration only applies to the Strict validation mode; it has no effect on the
+// InsecureSyntax and Disabled modes.
+type LuaStrictValidation struct {
+ // AllowedPaths is the list of filesystem path prefixes that Lua scripts are permitted to
+ // access during validation (via io.open, io.input, io.output, io.lines, os.remove, os.rename).
+ // A path is allowed when it equals an entry or is contained within an entry's subtree
+ // (e.g. "/tmp" allows "/tmp/file.txt"). Paths are normalized (separators collapsed, made
+ // absolute) before matching, and any "." or ".." traversal segment is always rejected.
+ // When empty, all filesystem access is denied. Blank or whitespace-only entries are rejected,
+ // as they would otherwise match every path and disable the sandbox.
+ //
+ // +kubebuilder:validation:MaxItems=64
+ // +kubebuilder:validation:items:MinLength=1
+ // +kubebuilder:validation:items:MaxLength=4096
+ // +kubebuilder:validation:XValidation:rule="self.all(p, p.trim() != '')",message="allowedPaths entries must not be blank or whitespace-only"
+ // +optional
+ AllowedPaths []string `json:"allowedPaths,omitempty"`
+
+ // AllowedEnvVars is the list of environment variable names that Lua scripts are permitted to
+ // access during validation (via os.getenv, os.setenv). Matching is exact and case-sensitive.
+ // When empty, access to all environment variables is denied. Blank or whitespace-only entries
+ // are rejected.
+ //
+ // +kubebuilder:validation:MaxItems=64
+ // +kubebuilder:validation:items:MinLength=1
+ // +kubebuilder:validation:items:MaxLength=256
+ // +kubebuilder:validation:XValidation:rule="self.all(e, e.trim() != '')",message="allowedEnvVars entries must not be blank or whitespace-only"
+ // +optional
+ AllowedEnvVars []string `json:"allowedEnvVars,omitempty"`
+}
+
// RoutingType defines the type of routing of this Envoy proxy.
type RoutingType string
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 4207e23a77..2ed0beaaf4 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -3289,6 +3289,11 @@ func (in *EnvoyProxySpec) DeepCopyInto(out *EnvoyProxySpec) {
*out = new(LuaValidation)
**out = **in
}
+ if in.LuaStrictValidation != nil {
+ in, out := &in.LuaStrictValidation, &out.LuaStrictValidation
+ *out = new(LuaStrictValidation)
+ (*in).DeepCopyInto(*out)
+ }
if in.DynamicModules != nil {
in, out := &in.DynamicModules, &out.DynamicModules
*out = make([]DynamicModuleEntry, len(*in))
@@ -5991,6 +5996,31 @@ func (in *Lua) DeepCopy() *Lua {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *LuaStrictValidation) DeepCopyInto(out *LuaStrictValidation) {
+ *out = *in
+ if in.AllowedPaths != nil {
+ in, out := &in.AllowedPaths, &out.AllowedPaths
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.AllowedEnvVars != nil {
+ in, out := &in.AllowedEnvVars, &out.AllowedEnvVars
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LuaStrictValidation.
+func (in *LuaStrictValidation) DeepCopy() *LuaStrictValidation {
+ if in == nil {
+ return nil
+ }
+ out := new(LuaStrictValidation)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MethodMatch) DeepCopyInto(out *MethodMatch) {
*out = *in
diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml
index d8a048a90b..c5f832428e 100644
--- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml
+++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_envoyproxies.yaml
@@ -769,6 +769,53 @@ spec:
and the log level is the value. If unspecified, defaults to "default: warn".
type: object
type: object
+ luaStrictValidation:
+ description: |-
+ LuaStrictValidation defines the filesystem paths and environment variables that Lua
+ scripts from EnvoyExtensionPolicy resources are permitted to access during Strict
+ validation in the gateway controller.
+
+ The allowlist is fail-closed: when unset or empty, Lua scripts are denied access to ALL
+ filesystem paths and environment variables during validation. Only entries that match the
+ allowlist are permitted.
+
+ This field only takes effect when LuaValidation is Strict (the default). It has no effect
+ for the InsecureSyntax or Disabled validation modes, which do not execute the security sandbox.
+ properties:
+ allowedEnvVars:
+ description: |-
+ AllowedEnvVars is the list of environment variable names that Lua scripts are permitted to
+ access during validation (via os.getenv, os.setenv). Matching is exact and case-sensitive.
+ When empty, access to all environment variables is denied. Blank or whitespace-only entries
+ are rejected.
+ items:
+ maxLength: 256
+ minLength: 1
+ type: string
+ maxItems: 64
+ type: array
+ x-kubernetes-validations:
+ - message: allowedEnvVars entries must not be blank or whitespace-only
+ rule: self.all(e, e.trim() != '')
+ allowedPaths:
+ description: |-
+ AllowedPaths is the list of filesystem path prefixes that Lua scripts are permitted to
+ access during validation (via io.open, io.input, io.output, io.lines, os.remove, os.rename).
+ A path is allowed when it equals an entry or is contained within an entry's subtree
+ (e.g. "/tmp" allows "/tmp/file.txt"). Paths are normalized (separators collapsed, made
+ absolute) before matching, and any "." or ".." traversal segment is always rejected.
+ When empty, all filesystem access is denied. Blank or whitespace-only entries are rejected,
+ as they would otherwise match every path and disable the sandbox.
+ items:
+ maxLength: 4096
+ minLength: 1
+ type: string
+ maxItems: 64
+ type: array
+ x-kubernetes-validations:
+ - message: allowedPaths entries must not be blank or whitespace-only
+ rule: self.all(p, p.trim() != '')
+ type: object
luaValidation:
description: |-
LuaValidation determines strictness of the Lua script validation for Lua EnvoyExtensionPolicies
@@ -18112,6 +18159,10 @@ spec:
rule: '!(has(self.samplingRate) && has(self.samplingFraction))'
type: object
type: object
+ x-kubernetes-validations:
+ - message: luaStrictValidation can only be set when luaValidation is Strict
+ rule: '!has(self.luaStrictValidation) || !has(self.luaValidation) ||
+ self.luaValidation == ''Strict'''
status:
description: EnvoyProxyStatus defines the actual state of EnvoyProxy.
properties:
diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml
index 5736db43cd..4e7a001bf9 100644
--- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml
+++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml
@@ -768,6 +768,53 @@ spec:
and the log level is the value. If unspecified, defaults to "default: warn".
type: object
type: object
+ luaStrictValidation:
+ description: |-
+ LuaStrictValidation defines the filesystem paths and environment variables that Lua
+ scripts from EnvoyExtensionPolicy resources are permitted to access during Strict
+ validation in the gateway controller.
+
+ The allowlist is fail-closed: when unset or empty, Lua scripts are denied access to ALL
+ filesystem paths and environment variables during validation. Only entries that match the
+ allowlist are permitted.
+
+ This field only takes effect when LuaValidation is Strict (the default). It has no effect
+ for the InsecureSyntax or Disabled validation modes, which do not execute the security sandbox.
+ properties:
+ allowedEnvVars:
+ description: |-
+ AllowedEnvVars is the list of environment variable names that Lua scripts are permitted to
+ access during validation (via os.getenv, os.setenv). Matching is exact and case-sensitive.
+ When empty, access to all environment variables is denied. Blank or whitespace-only entries
+ are rejected.
+ items:
+ maxLength: 256
+ minLength: 1
+ type: string
+ maxItems: 64
+ type: array
+ x-kubernetes-validations:
+ - message: allowedEnvVars entries must not be blank or whitespace-only
+ rule: self.all(e, e.trim() != '')
+ allowedPaths:
+ description: |-
+ AllowedPaths is the list of filesystem path prefixes that Lua scripts are permitted to
+ access during validation (via io.open, io.input, io.output, io.lines, os.remove, os.rename).
+ A path is allowed when it equals an entry or is contained within an entry's subtree
+ (e.g. "/tmp" allows "/tmp/file.txt"). Paths are normalized (separators collapsed, made
+ absolute) before matching, and any "." or ".." traversal segment is always rejected.
+ When empty, all filesystem access is denied. Blank or whitespace-only entries are rejected,
+ as they would otherwise match every path and disable the sandbox.
+ items:
+ maxLength: 4096
+ minLength: 1
+ type: string
+ maxItems: 64
+ type: array
+ x-kubernetes-validations:
+ - message: allowedPaths entries must not be blank or whitespace-only
+ rule: self.all(p, p.trim() != '')
+ type: object
luaValidation:
description: |-
LuaValidation determines strictness of the Lua script validation for Lua EnvoyExtensionPolicies
@@ -18111,6 +18158,10 @@ spec:
rule: '!(has(self.samplingRate) && has(self.samplingFraction))'
type: object
type: object
+ x-kubernetes-validations:
+ - message: luaStrictValidation can only be set when luaValidation is Strict
+ rule: '!has(self.luaStrictValidation) || !has(self.luaValidation) ||
+ self.luaValidation == ''Strict'''
status:
description: EnvoyProxyStatus defines the actual state of EnvoyProxy.
properties:
diff --git a/internal/gatewayapi/luavalidator/lua_validator.go b/internal/gatewayapi/luavalidator/lua_validator.go
index fbdc0316d2..86cf7ca22b 100644
--- a/internal/gatewayapi/luavalidator/lua_validator.go
+++ b/internal/gatewayapi/luavalidator/lua_validator.go
@@ -90,6 +90,59 @@ func (l *LuaValidator) getLuaValidation() egv1a1.LuaValidation {
return egv1a1.LuaValidationStrict
}
+// allowlistData generates the Lua source consumed by security.lua: the path allowlist as a list and
+// the env var allowlist as a map (name -> true). The allowlist is fail-closed; an unconfigured
+// EnvoyProxy yields empty tables, denying all access.
+func (l *LuaValidator) allowlistData() string {
+ var paths, envVars []string
+ if l.envoyProxy != nil && l.envoyProxy.Spec.LuaStrictValidation != nil {
+ paths = l.envoyProxy.Spec.LuaStrictValidation.AllowedPaths
+ envVars = l.envoyProxy.Spec.LuaStrictValidation.AllowedEnvVars
+ }
+
+ var b strings.Builder
+
+ b.WriteString("__lua_allowed_paths = {")
+ for i, p := range paths {
+ if i > 0 {
+ b.WriteString(", ")
+ }
+ b.WriteString(luaStringLiteral(p))
+ }
+ b.WriteString("}\n")
+
+ b.WriteString("__lua_allowed_env_vars = {")
+ for i, e := range envVars {
+ if i > 0 {
+ b.WriteString(", ")
+ }
+ // Spaces around the key are required: "[[[..." would lex as a long-string opener.
+ b.WriteString("[ ")
+ b.WriteString(luaStringLiteral(e))
+ b.WriteString(" ] = true")
+ }
+ b.WriteString("}\n")
+
+ return b.String()
+}
+
+// luaStringLiteral encodes s as a Lua long-bracket literal ([==[...]==]) with an equals level that
+// avoids any closing delimiter in s. Long brackets don't interpret escapes, so spec values are
+// passed verbatim as data and cannot inject Lua.
+func luaStringLiteral(s string) string {
+ level := 0
+ for strings.Contains(s, "]"+strings.Repeat("=", level)+"]") {
+ level++
+ }
+ eq := strings.Repeat("=", level)
+ // Lua strips a leading newline after the opening bracket; re-add it to preserve such values.
+ prefix := ""
+ if strings.HasPrefix(s, "\n") {
+ prefix = "\n"
+ }
+ return "[" + eq + "[" + prefix + s + "]" + eq + "]"
+}
+
// newLuaState creates a new Lua state with global settings and resource limits applied
// Returns the Lua state and a cancel function that must be called when done
func (l *LuaValidator) newLuaState() (*lua.LState, context.CancelFunc) {
@@ -123,6 +176,8 @@ func (l *LuaValidator) runLua(code string) error {
// Execute mocks first (trusted code, needs setmetatable, defines StreamHandle, etc.)
_ = L.DoString(mockData)
+ // Inject the allowlists before security.lua, which reads and then clears them.
+ _ = L.DoString(l.allowlistData())
// Execute Lua security wrappers (trusted code) to protect the gateway controller
// See security advisory: https://github.com/envoyproxy/gateway/security/advisories/GHSA-xrwg-mqj6-6m22
_ = L.DoString(securityData)
diff --git a/internal/gatewayapi/luavalidator/lua_validator_test.go b/internal/gatewayapi/luavalidator/lua_validator_test.go
index 497b27d847..ce292aa50b 100644
--- a/internal/gatewayapi/luavalidator/lua_validator_test.go
+++ b/internal/gatewayapi/luavalidator/lua_validator_test.go
@@ -209,431 +209,315 @@ func Test_BasicValidation(t *testing.T) {
}
}
-func Test_block_or_sanitize_io(t *testing.T) {
- type testCase struct {
+// allowlistProxy returns an EnvoyProxy configured with the given Lua validation allowlists.
+func allowlistProxy(paths, envVars []string) *egv1a1.EnvoyProxy {
+ return &egv1a1.EnvoyProxy{
+ Spec: egv1a1.EnvoyProxySpec{
+ LuaStrictValidation: &egv1a1.LuaStrictValidation{
+ AllowedPaths: paths,
+ AllowedEnvVars: envVars,
+ },
+ },
+ }
+}
+
+// Test_io_path_allowlist verifies the filesystem allowlist for the sanitized io functions.
+// The allowlist is fail-closed: only paths under /tmp are permitted; everything else is denied.
+// Path traversal segments are always rejected, regardless of the allowlist.
+func Test_io_path_allowlist(t *testing.T) {
+ proxy := allowlistProxy([]string{"/tmp"}, nil)
+
+ tests := []struct {
name string
code string
expectedErrSubstring string
- }
- tests := []testCase{
- // io.open tests
+ }{
{
- name: "io.open critical path /certs",
- code: `function envoy_on_response(response_handle)
- local file = io.open("/certs/tls.crt", "w")
- if file then file:close() end
- end`,
- expectedErrSubstring: "critical path",
+ name: "io.open allowed /tmp",
+ code: `function envoy_on_response(h) local f = io.open("/tmp/x", "w") if f then f:close() end end`,
+ expectedErrSubstring: "",
},
{
- name: "io.open non-critical path /tmp",
- code: `function envoy_on_response(response_handle)
- local file = io.open("/tmp/tls.crt", "w")
- if file then
- file:write("test")
- file:close()
- end
- end`,
+ name: "io.open allowed via subtree /tmp/sub/x",
+ code: `function envoy_on_response(h) local f = io.open("/tmp/sub/x", "w") if f then f:close() end end`,
expectedErrSubstring: "",
},
{
- name: "io.open /etc/passwd",
- code: `function envoy_on_response(response_handle)
- local file = io.open("/etc/passwd", "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "critical path",
+ name: "io.open denied /etc/passwd",
+ code: `function envoy_on_response(h) io.open("/etc/passwd", "r") end`,
+ expectedErrSubstring: "io.open restricted for param",
},
{
- name: "io.open /proc/self/environ",
- code: `function envoy_on_response(response_handle)
- local file = io.open("/proc/self/environ", "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "critical path",
+ name: "io.open denied path outside allowlist",
+ code: `function envoy_on_response(h) io.open("/tmpfoo/x", "r") end`,
+ expectedErrSubstring: "io.open restricted for param",
},
+ // Path normalization: relative, backslash, and multi-slash forms must normalize
+ // consistently before the allowlist match.
{
- name: "io.open /sys/kernel",
- code: `function envoy_on_response(response_handle)
- local file = io.open("/sys/kernel", "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "critical path",
+ name: "io.open relative path normalizes under allowed root",
+ code: `function envoy_on_response(h) local f = io.open("tmp/x", "r") if f then f:close() end end`,
+ expectedErrSubstring: "",
},
{
- name: "io.open /var/run/secrets/token",
- code: `function envoy_on_response(response_handle)
- local file = io.open("/var/run/secrets/token", "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "critical path",
+ name: "io.open allowed double-slash //tmp/x",
+ code: `function envoy_on_response(h) local f = io.open("//tmp/x", "w") if f then f:close() end end`,
+ expectedErrSubstring: "",
},
{
- name: "io.open relative path etc/passwd",
- code: `function envoy_on_response(response_handle)
- local file = io.open("etc/passwd", "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "critical path",
+ name: "io.open allowed trailing slash /tmp/",
+ code: `function envoy_on_response(h) local f = io.open("/tmp/", "r") if f then f:close() end end`,
+ expectedErrSubstring: "",
},
{
- name: "io.open path traversal /tmp/../etc/passwd",
- code: `function envoy_on_response(response_handle)
- local file = io.open("/tmp/../etc/passwd", "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "path traversals",
+ name: "io.open denied double-slash //etc/passwd",
+ code: `function envoy_on_response(h) io.open("//etc/passwd", "r") end`,
+ expectedErrSubstring: "io.open restricted for param",
},
{
- name: "io.open path traversal ../etc/passwd",
- code: `function envoy_on_response(response_handle)
- local file = io.open("../etc/passwd", "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "path traversals",
+ name: "io.open denied embedded double-slash /etc//passwd",
+ code: `function envoy_on_response(h) io.open("/etc//passwd", "r") end`,
+ expectedErrSubstring: "io.open restricted for param",
},
{
- name: "io.open relative path certs/tls.crt",
- code: `function envoy_on_response(response_handle)
- local file = io.open("certs/tls.crt", "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "critical path",
+ name: "io.open denied multiple-slash ///etc/passwd",
+ code: `function envoy_on_response(h) io.open("///etc/passwd", "r") end`,
+ expectedErrSubstring: "io.open restricted for param",
},
{
- name: "io.open relative path var/run/secrets/token",
- code: `function envoy_on_response(response_handle)
- local file = io.open("var/run/secrets/token", "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "critical path",
+ name: "io.open denied backslash etc\\passwd",
+ code: `function envoy_on_response(h) io.open("etc\\passwd", "r") end`,
+ expectedErrSubstring: "io.open restricted for param",
},
{
- name: "io.open with backslash etc\\passwd",
- code: `function envoy_on_response(response_handle)
- local file = io.open("etc\\passwd", "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "critical path",
+ name: "io.open denied string concatenation",
+ code: `function envoy_on_response(h) local p = "/" .. "etc" .. "/" .. "passwd" io.open(p, "r") end`,
+ expectedErrSubstring: "io.open restricted for param",
},
+ // Traversal segments are always rejected with a distinct error, regardless of the allowlist.
{
- name: "io.open path traversal with backslash",
- code: `function envoy_on_response(response_handle)
- local file = io.open("..\\etc\\passwd", "r")
- if file then file:close() end
- end`,
+ name: "io.open traversal rejected even under allowed root",
+ code: `function envoy_on_response(h) io.open("/tmp/../etc/passwd", "r") end`,
expectedErrSubstring: "path traversals",
},
{
- name: "io.open with string concatenation",
- code: `function envoy_on_response(response_handle)
- local path = "/" .. "etc" .. "/" .. "passwd"
- local file = io.open(path, "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "critical path",
- },
- {
- name: "io.open with trailing slash /certs/",
- code: `function envoy_on_response(response_handle)
- local file = io.open("/certs/", "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "critical path",
- },
- {
- name: "io.open double-slash //etc/passwd",
- code: `function envoy_on_response(response_handle)
- local file = io.open("//etc/passwd", "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "critical path",
- },
- {
- name: "io.open double-slash //var/run/secrets/token",
- code: `function envoy_on_response(response_handle)
- local file = io.open("//var/run/secrets/kubernetes.io/serviceaccount/token", "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "critical path",
- },
- {
- name: "io.open run secrets alias /run/secrets/token",
- code: `function envoy_on_response(response_handle)
- local file = io.open("/run/secrets/kubernetes.io/serviceaccount/token", "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "critical path",
- },
- {
- name: "io.open run secrets alias double-slash //run/secrets/token",
- code: `function envoy_on_response(response_handle)
- local file = io.open("//run/secrets/kubernetes.io/serviceaccount/token", "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "critical path",
- },
- {
- name: "io.open multiple-slash ///etc/passwd",
- code: `function envoy_on_response(response_handle)
- local file = io.open("///etc/passwd", "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "critical path",
- },
- {
- name: "io.open embedded double-slash /etc//passwd",
- code: `function envoy_on_response(response_handle)
- local file = io.open("/etc//passwd", "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "critical path",
- },
- {
- name: "io.open double-slash //proc/self/environ",
- code: `function envoy_on_response(response_handle)
- local file = io.open("//proc/self/environ", "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "critical path",
+ name: "io.open traversal rejected ../etc/passwd",
+ code: `function envoy_on_response(h) io.open("../etc/passwd", "r") end`,
+ expectedErrSubstring: "path traversals",
},
{
- name: "io.open double-slash //certs/tls.crt",
- code: `function envoy_on_response(response_handle)
- local file = io.open("//certs/tls.crt", "r")
- if file then file:close() end
- end`,
- expectedErrSubstring: "critical path",
+ name: "io.open traversal rejected with backslash",
+ code: `function envoy_on_response(h) io.open("..\\etc\\passwd", "r") end`,
+ expectedErrSubstring: "path traversals",
},
{
- name: "io.open dot segment /etc/./passwd",
- code: `function envoy_on_response(response_handle)
- local file = io.open("/etc/./passwd", "r")
- if file then file:close() end
- end`,
+ name: "io.open traversal rejected dot segment /tmp/./x",
+ code: `function envoy_on_response(h) io.open("/tmp/./x", "r") end`,
expectedErrSubstring: "path traversals",
},
{
- name: "io.open leading dot ./etc/passwd",
- code: `function envoy_on_response(response_handle)
- local file = io.open("./etc/passwd", "r")
- if file then file:close() end
- end`,
+ name: "io.open traversal rejected leading dot ./tmp/x",
+ code: `function envoy_on_response(h) io.open("./tmp/x", "r") end`,
expectedErrSubstring: "path traversals",
},
{
- name: "io.open trailing dot /etc/.",
- code: `function envoy_on_response(response_handle)
- local file = io.open("/etc/.", "r")
- if file then file:close() end
- end`,
+ name: "io.open traversal rejected trailing dot /tmp/.",
+ code: `function envoy_on_response(h) io.open("/tmp/.", "r") end`,
expectedErrSubstring: "path traversals",
},
{
- name: "io.open dot with backslash etc\\.\\passwd",
- code: `function envoy_on_response(response_handle)
- local file = io.open("etc\\.\\passwd", "r")
- if file then file:close() end
- end`,
+ name: "io.open traversal rejected dot with backslash tmp\\.\\x",
+ code: `function envoy_on_response(h) io.open("tmp\\.\\x", "r") end`,
expectedErrSubstring: "path traversals",
},
- // io.input tests
{
- name: "io.input critical path /certs",
- code: `function envoy_on_response(response_handle)
- io.input("/certs/tls.crt")
- end`,
- expectedErrSubstring: "critical path",
+ name: "io.input denied /etc/passwd",
+ code: `function envoy_on_response(h) io.input("/etc/passwd") end`,
+ expectedErrSubstring: "io.input restricted for param",
},
{
- name: "io.input non-critical path /tmp",
- code: `function envoy_on_response(response_handle)
- local file = io.open("/tmp/tls.crt", "w")
- if file then
- file:write("test content")
- file:close()
- end
- io.input("/tmp/tls.crt")
- end`,
- expectedErrSubstring: "",
+ name: "io.output denied /certs",
+ code: `function envoy_on_response(h) io.output("/certs/tls.crt") end`,
+ expectedErrSubstring: "io.output restricted for param",
},
{
- name: "io.input /etc/passwd",
- code: `function envoy_on_response(response_handle)
- io.input("/etc/passwd")
- end`,
- expectedErrSubstring: "critical path",
+ name: "io.lines denied /etc/passwd",
+ code: `function envoy_on_response(h) for l in io.lines("/etc/passwd") do end end`,
+ expectedErrSubstring: "io.lines restricted for param",
},
- // io.output tests
+ }
+ runAllowlistCases(t, proxy, tests)
+}
+
+// Test_path_allowlist_literal_match ensures allowed prefixes containing Lua pattern magic characters
+// (e.g. ".") are matched literally and do not widen the security boundary.
+func Test_path_allowlist_literal_match(t *testing.T) {
+ proxy := allowlistProxy([]string{"/var/lib/app.v1"}, nil)
+
+ tests := []struct {
+ name string
+ code string
+ expectedErrSubstring string
+ }{
{
- name: "io.output critical path /certs",
- code: `function envoy_on_response(response_handle)
- io.output("/certs/tls.crt")
- end`,
- expectedErrSubstring: "critical path",
+ name: "exact allowed entry",
+ code: `function envoy_on_response(h) local f = io.open("/var/lib/app.v1", "r") if f then f:close() end end`,
+ expectedErrSubstring: "",
},
{
- name: "io.output non-critical path /tmp",
- code: `function envoy_on_response(response_handle)
- io.output("/tmp/tls.crt")
- end`,
+ name: "subtree of allowed entry",
+ code: `function envoy_on_response(h) local f = io.open("/var/lib/app.v1/data", "r") if f then f:close() end end`,
expectedErrSubstring: "",
},
- // io.lines tests
{
- name: "io.lines critical path /certs",
- code: `function envoy_on_response(response_handle)
- for line in io.lines("/certs/tls.crt") do
- response_handle:logInfo(line)
- end
- end`,
- expectedErrSubstring: "critical path",
+ name: "dot must not match arbitrary character",
+ code: `function envoy_on_response(h) io.open("/var/lib/appXv1/data", "r") end`,
+ expectedErrSubstring: "io.open restricted for param",
},
+ }
+ runAllowlistCases(t, proxy, tests)
+}
+
+// Test_path_allowlist_blank_entry_denied ensures a blank allowlist entry does not match every path
+// and silently disable the sandbox (defense in depth; the CRD also rejects blank entries).
+func Test_path_allowlist_blank_entry_denied(t *testing.T) {
+ proxy := allowlistProxy([]string{"", " ", "/tmp"}, nil)
+
+ tests := []struct {
+ name string
+ code string
+ expectedErrSubstring string
+ }{
{
- name: "io.lines non-critical path /tmp",
- code: `function envoy_on_response(response_handle)
- for line in io.lines("/tmp/tls.crt") do
- response_handle:logInfo(line)
- end
- end`,
- expectedErrSubstring: "",
+ name: "blank entry does not allow arbitrary path",
+ code: `function envoy_on_response(h) io.open("/etc/passwd", "r") end`,
+ expectedErrSubstring: "io.open restricted for param",
},
{
- name: "io.lines /etc/passwd",
- code: `function envoy_on_response(response_handle)
- for line in io.lines("/etc/passwd") do
- response_handle:logInfo(line)
- end
- end`,
- expectedErrSubstring: "critical path",
+ name: "real entry still allowed",
+ code: `function envoy_on_response(h) local f = io.open("/tmp/x", "r") if f then f:close() end end`,
+ expectedErrSubstring: "",
},
}
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- l := NewLuaValidator(tt.code, nil)
- if err := l.Validate(); err != nil && tt.expectedErrSubstring == "" {
- t.Errorf("Unexpected error: %v", err)
- } else if err != nil && !strings.Contains(err.Error(), tt.expectedErrSubstring) {
- t.Errorf("Expected substring in error: %v, got error: %v", tt.expectedErrSubstring, err)
- } else if err == nil && tt.expectedErrSubstring != "" {
- t.Errorf("Expected error with substring: %v", tt.expectedErrSubstring)
- }
- })
+ runAllowlistCases(t, proxy, tests)
+}
+
+// Test_io_denied_by_default ensures that with no allowlist configured, all filesystem access is denied.
+func Test_io_denied_by_default(t *testing.T) {
+ code := `function envoy_on_response(h) io.open("/tmp/x", "w") end`
+ if err := NewLuaValidator(code, nil).Validate(); err == nil {
+ t.Errorf("Expected fail-closed denial with no allowlist, got no error")
+ } else if !strings.Contains(err.Error(), "io.open restricted for param") {
+ t.Errorf("Expected 'io.open restricted for param', got: %v", err)
}
}
-func Test_block_or_sanitize_os(t *testing.T) {
- type testCase struct {
+// Test_os_path_allowlist verifies the filesystem allowlist for the sanitized os functions.
+// Only /tmp is permitted; os.rename requires both source and destination to be allowed.
+func Test_os_path_allowlist(t *testing.T) {
+ proxy := allowlistProxy([]string{"/tmp"}, nil)
+
+ tests := []struct {
name string
code string
expectedErrSubstring string
- }
- tests := []testCase{
- // os.remove tests
- {
- name: "os.remove critical path /certs",
- code: `function envoy_on_response(response_handle)
- os.remove("/certs/tls.crt")
- end`,
- expectedErrSubstring: "critical path",
- },
+ }{
{
- name: "os.remove non-critical path /tmp",
- code: `function envoy_on_response(response_handle)
- os.remove("/tmp/tls.crt")
- end`,
+ name: "os.remove allowed /tmp",
+ code: `function envoy_on_response(h) os.remove("/tmp/x") end`,
expectedErrSubstring: "",
},
- // os.rename tests
{
- name: "os.rename critical path /certs",
- code: `function envoy_on_response(response_handle)
- os.rename("/certs/tls.crt", "/certs/tls.crt.bak")
- end`,
- expectedErrSubstring: "critical path",
+ name: "os.remove denied /certs",
+ code: `function envoy_on_response(h) os.remove("/certs/tls.crt") end`,
+ expectedErrSubstring: "os.remove restricted for param",
},
{
- name: "os.rename non-critical path /tmp",
- code: `function envoy_on_response(response_handle)
- os.rename("/tmp/tls.crt", "/tmp/tls.crt.bak")
- end`,
+ name: "os.rename allowed both /tmp",
+ code: `function envoy_on_response(h) os.rename("/tmp/a", "/tmp/b") end`,
expectedErrSubstring: "",
},
{
- name: "os.rename critical source",
- code: `function envoy_on_response(response_handle)
- os.rename("/certs/tls.crt", "/tmp/tls.crt.bak")
- end`,
- expectedErrSubstring: "critical path",
- },
- {
- name: "os.rename critical destination",
- code: `function envoy_on_response(response_handle)
- os.rename("/tmp/tls.crt", "/certs/tls.crt.bak")
- end`,
- expectedErrSubstring: "critical path",
+ name: "os.rename denied source",
+ code: `function envoy_on_response(h) os.rename("/certs/a", "/tmp/b") end`,
+ expectedErrSubstring: "os.rename restricted for param",
},
{
- name: "os.rename to critical path /etc",
- code: `function envoy_on_response(response_handle)
- os.rename("/tmp/file", "/certs/file")
- end`,
- expectedErrSubstring: "critical path",
+ name: "os.rename denied destination",
+ code: `function envoy_on_response(h) os.rename("/tmp/a", "/certs/b") end`,
+ expectedErrSubstring: "os.rename restricted for param",
},
+ }
+ runAllowlistCases(t, proxy, tests)
+}
+
+// Test_os_env_allowlist verifies the environment variable allowlist (exact, case-sensitive match)
+// for os.getenv and os.setenv.
+func Test_os_env_allowlist(t *testing.T) {
+ proxy := allowlistProxy(nil, []string{"LOG_LEVEL"})
+
+ tests := []struct {
+ name string
+ code string
+ expectedErrSubstring string
+ }{
{
- name: "os.rename from critical path /etc",
- code: `function envoy_on_response(response_handle)
- os.rename("/certs/file", "/tmp/file")
- end`,
- expectedErrSubstring: "critical path",
+ name: "os.getenv allowed LOG_LEVEL",
+ code: `function envoy_on_response(h) os.getenv("LOG_LEVEL") end`,
+ expectedErrSubstring: "",
},
- // os.getenv tests
{
- name: "os.getenv critical env var PWD",
- code: `function envoy_on_response(response_handle)
- local pwd = os.getenv("PWD")
- end`,
- expectedErrSubstring: "critical environment variable",
+ name: "os.getenv denied PWD",
+ code: `function envoy_on_response(h) os.getenv("PWD") end`,
+ expectedErrSubstring: "os.getenv restricted for param PWD",
},
{
- name: "os.getenv critical env var pwd (lowercase)",
- code: `function envoy_on_response(response_handle)
- local pwd = os.getenv("pwd")
- end`,
- expectedErrSubstring: "critical environment variable",
+ name: "os.getenv match is case-sensitive",
+ code: `function envoy_on_response(h) os.getenv("log_level") end`,
+ expectedErrSubstring: "os.getenv restricted for param log_level",
},
- // os.setenv tests
{
- name: "os.setenv non-critical env var allowed",
- code: `function envoy_on_response(response_handle)
- os.setenv("TEST", "value")
- end`,
+ name: "os.setenv allowed LOG_LEVEL",
+ code: `function envoy_on_response(h) os.setenv("LOG_LEVEL", "debug") end`,
expectedErrSubstring: "",
},
{
- name: "os.setenv critical env var PWD",
- code: `function envoy_on_response(response_handle)
- os.setenv("PWD", "/etc")
- end`,
- expectedErrSubstring: "setting critical environment variable",
- },
- {
- name: "os.setenv critical env var pwd (lowercase)",
- code: `function envoy_on_response(response_handle)
- os.setenv("pwd", "/etc")
- end`,
- expectedErrSubstring: "setting critical environment variable",
+ name: "os.setenv denied PWD",
+ code: `function envoy_on_response(h) os.setenv("PWD", "/etc") end`,
+ expectedErrSubstring: "os.setenv restricted for param PWD",
},
}
+ runAllowlistCases(t, proxy, tests)
+}
+
+// Test_env_denied_by_default ensures that with no allowlist configured, all env var access is denied.
+func Test_env_denied_by_default(t *testing.T) {
+ code := `function envoy_on_response(h) os.getenv("LOG_LEVEL") end`
+ if err := NewLuaValidator(code, nil).Validate(); err == nil {
+ t.Errorf("Expected fail-closed denial with no allowlist, got no error")
+ } else if !strings.Contains(err.Error(), "os.getenv restricted for param") {
+ t.Errorf("Expected 'os.getenv restricted for param', got: %v", err)
+ }
+}
+
+// runAllowlistCases runs a set of allowlist validation cases against the given proxy.
+func runAllowlistCases(t *testing.T, proxy *egv1a1.EnvoyProxy, tests []struct {
+ name string
+ code string
+ expectedErrSubstring string
+},
+) {
+ t.Helper()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- l := NewLuaValidator(tt.code, nil)
- if err := l.Validate(); err != nil && tt.expectedErrSubstring == "" {
+ err := NewLuaValidator(tt.code, proxy).Validate()
+ switch {
+ case err != nil && tt.expectedErrSubstring == "":
t.Errorf("Unexpected error: %v", err)
- } else if err != nil && !strings.Contains(err.Error(), tt.expectedErrSubstring) {
- t.Errorf("Expected substring in error: %v, got error: %v", tt.expectedErrSubstring, err)
- } else if err == nil && tt.expectedErrSubstring != "" {
- t.Errorf("Expected error with substring: %v", tt.expectedErrSubstring)
+ case err != nil && !strings.Contains(err.Error(), tt.expectedErrSubstring):
+ t.Errorf("Expected substring %q in error, got: %v", tt.expectedErrSubstring, err)
+ case err == nil && tt.expectedErrSubstring != "":
+ t.Errorf("Expected error with substring %q, got none", tt.expectedErrSubstring)
}
})
}
diff --git a/internal/gatewayapi/luavalidator/security.lua b/internal/gatewayapi/luavalidator/security.lua
index 645c0d71ab..691d9d99d3 100644
--- a/internal/gatewayapi/luavalidator/security.lua
+++ b/internal/gatewayapi/luavalidator/security.lua
@@ -1,27 +1,20 @@
--- Security sandbox for Lua execution in Envoy Gateway
--- Blocks dangerous functions and validates paths to prevent access to sensitive system resources
+-- Security sandbox for Lua execution in Envoy Gateway: blocks dangerous functions and enforces a
+-- fail-closed allowlist of filesystem paths and environment variables during validation.
+--
+-- The allowed sets are injected by the Go validator before this script runs, as the globals
+-- `__lua_allowed_paths` (array of path prefixes) and `__lua_allowed_env_vars` (map name -> true).
+-- An absent or empty table denies that entire category.
-- ============================================================================
--- CRITICAL PATHS
+-- ALLOWLISTS (injected by the Go validator; default to empty = deny all)
-- ============================================================================
-local critical_paths = {
- "/etc",
- "/proc",
- "/sys",
- "/certs",
- "/var/run/secrets",
- -- "/var/run" is a symlink to "/run" on Debian-derived (distroless) images.
- "/run/secrets",
-}
+local allowed_paths = __lua_allowed_paths or {}
+local allowed_env_vars = __lua_allowed_env_vars or {}
--- ============================================================================
--- CRITICAL ENVIRONMENT VARIABLES
--- ============================================================================
-
-local critical_env_vars = {
- ["PWD"] = true,
-}
+-- Remove the injected globals so user code cannot read or mutate the allowlists.
+__lua_allowed_paths = nil
+__lua_allowed_env_vars = nil
-- ============================================================================
-- HELPER FUNCTIONS
@@ -31,7 +24,7 @@ local function to_absolute_normalized_path(path)
if not path or type(path) ~= "string" then
return path
end
-
+
local normalized_separators = path:gsub("\\", "/")
local collapsed_separators = normalized_separators:gsub("/+", "/")
@@ -63,44 +56,51 @@ local function contains_traversal(path)
return false
end
-local function is_critical_path(path)
+-- is_allowed_path returns true when the path equals an allowed entry or falls within its subtree.
+-- Both sides are normalized so relative, backslash, and double-slash forms match consistently.
+-- The subtree check uses plain (non-pattern) string matching so allowed prefixes containing Lua
+-- magic characters (e.g. "." in "/var/lib/app.v1") are treated literally and define an exact boundary.
+local function is_allowed_path(path)
if not path or type(path) ~= "string" then
return false
end
-
+
local normalized = to_absolute_normalized_path(path)
-
- for _, critical_path in ipairs(critical_paths) do
- local normalized_critical = to_absolute_normalized_path(critical_path)
- local escaped_critical = normalized_critical:gsub("%-", "%%-")
-
- if normalized == normalized_critical or normalized:match("^" .. escaped_critical .. "/") then
+
+ for _, allowed in ipairs(allowed_paths) do
+ local normalized_allowed = to_absolute_normalized_path(allowed)
+
+ -- Skip blank entries: "" would match every absolute path and disable the sandbox.
+ if normalized_allowed ~= "" and
+ (normalized == normalized_allowed
+ or normalized:find(normalized_allowed .. "/", 1, true) == 1) then
return true
end
end
-
+
return false
end
-local function validate_path(path)
+-- validate_path rejects traversal segments unconditionally, then enforces the path allowlist.
+local function validate_path(fn_name, path)
if not path or type(path) ~= "string" then
return
end
-
+
if contains_traversal(path) then
error("path traversals are restricted for security")
end
-
- if is_critical_path(path) then
- error("access to critical path " .. path .. " is restricted for security")
+
+ if not is_allowed_path(path) then
+ error(fn_name .. " restricted for param " .. path)
end
end
-local function is_critical_env_var(env_var)
- if not env_var or type(env_var) ~= "string" then
- return false
+-- validate_env_var enforces the env var allowlist (exact, case-sensitive match).
+local function validate_env_var(fn_name, env_var)
+ if not env_var or type(env_var) ~= "string" or allowed_env_vars[env_var] ~= true then
+ error(fn_name .. " restricted for param " .. tostring(env_var))
end
- return critical_env_vars[env_var:upper()] == true
end
-- ============================================================================
@@ -125,7 +125,7 @@ setmetatable = nil
_G = nil
-- ============================================================================
--- SANITIZED IO FUNCTIONS (path validation)
+-- SANITIZED IO FUNCTIONS (path allowlist)
-- ============================================================================
do
@@ -135,7 +135,7 @@ do
local _unsafe_io_lines = io.lines
io.open = function(filename, mode)
- validate_path(filename)
+ validate_path("io.open", filename)
return _unsafe_io_open(filename, mode)
end
@@ -144,7 +144,7 @@ do
return _unsafe_io_input()
end
if type(file) == "string" then
- validate_path(file)
+ validate_path("io.input", file)
end
return _unsafe_io_input(file)
end
@@ -154,21 +154,21 @@ do
return _unsafe_io_output()
end
if type(file) == "string" then
- validate_path(file)
+ validate_path("io.output", file)
end
return _unsafe_io_output(file)
end
io.lines = function(filename)
if filename then
- validate_path(filename)
+ validate_path("io.lines", filename)
end
return _unsafe_io_lines(filename)
end
end
-- ============================================================================
--- SANITIZED OS FUNCTIONS (path/env var validation)
+-- SANITIZED OS FUNCTIONS (path / env var allowlist)
-- ============================================================================
do
@@ -178,27 +178,23 @@ do
local _unsafe_os_setenv = os.setenv
os.remove = function(pathname)
- validate_path(pathname)
+ validate_path("os.remove", pathname)
return _unsafe_os_remove(pathname)
end
os.rename = function(oldname, newname)
- validate_path(oldname)
- validate_path(newname)
+ validate_path("os.rename", oldname)
+ validate_path("os.rename", newname)
return _unsafe_os_rename(oldname, newname)
end
os.getenv = function(varname)
- if is_critical_env_var(varname) then
- error("access to critical environment variable " .. varname .. " is restricted for security")
- end
+ validate_env_var("os.getenv", varname)
return _unsafe_os_getenv(varname)
end
os.setenv = function(varname, value)
- if is_critical_env_var(varname) then
- error("setting critical environment variable " .. varname .. " is restricted for security")
- end
+ validate_env_var("os.setenv", varname)
return _unsafe_os_setenv(varname, value)
end
end
diff --git a/release-notes/current/breaking_changes/9220-lua-validation-allowlist-fail-closed.md b/release-notes/current/breaking_changes/9220-lua-validation-allowlist-fail-closed.md
new file mode 100644
index 0000000000..b954758fed
--- /dev/null
+++ b/release-notes/current/breaking_changes/9220-lua-validation-allowlist-fail-closed.md
@@ -0,0 +1 @@
+Strict Lua validation now enforces a fail-closed allowlist of filesystem paths and environment variables that Lua scripts may access during validation, replacing the previous fixed denylist of critical paths/variables. By default the allowlist is empty, so all filesystem and environment variable access during validation is denied. To permit specific paths or environment variables, configure `EnvoyProxy.spec.luaValidationAllowlist.allowedPaths` and `allowedEnvVars`. This only affects the `Strict` Lua validation mode; `InsecureSyntax` and `Disabled` are unchanged.
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index d59fec9715..fed6d0fdd7 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -2255,6 +2255,7 @@ _Appears in:_
| `ipFamily` | _[IPFamily](#ipfamily)_ | false | | IPFamily specifies the IP family for the EnvoyProxy fleet.
This setting only affects the Gateway listener port and does not impact
other aspects of the Envoy proxy configuration.
If not specified, the system will operate as follows:
- It defaults to IPv4 only.
- IPv6 and dual-stack environments are not supported in this default configuration.
Note: To enable IPv6 or dual-stack functionality, explicit configuration is required. |
| `preserveRouteOrder` | _boolean_ | false | | PreserveRouteOrder determines if the order of matching for HTTPRoutes is determined by Gateway-API
specification (https://gateway-api.sigs.k8s.io/reference/api-spec/main/spec/#httprouterule)
or preserves the order defined by users in the HTTPRoute's HTTPRouteRule list.
Default: False |
| `luaValidation` | _[LuaValidation](#luavalidation)_ | false | | LuaValidation determines strictness of the Lua script validation for Lua EnvoyExtensionPolicies
Default: Strict |
+| `luaStrictValidation` | _[LuaStrictValidation](#luastrictvalidation)_ | false | | LuaStrictValidation defines the filesystem paths and environment variables that Lua
scripts from EnvoyExtensionPolicy resources are permitted to access during Strict
validation in the gateway controller.
The allowlist is fail-closed: when unset or empty, Lua scripts are denied access to ALL
filesystem paths and environment variables during validation. Only entries that match the
allowlist are permitted.
This field only takes effect when LuaValidation is Strict (the default). It has no effect
for the InsecureSyntax or Disabled validation modes, which do not execute the security sandbox. |
| `dynamicModules` | _[DynamicModuleEntry](#dynamicmoduleentry) array_ | false | | DynamicModules defines the set of dynamic modules that are allowed to be
used by EnvoyExtensionPolicy resources and dynamic module load balancer
policies. Each entry registers a module by a logical name and specifies
the shared library that Envoy will load.
The EnvoyProxy owner is responsible for ensuring the module .so files are available
on the proxy container's filesystem (e.g., via init containers, custom images,
or shared volumes). |
| `geoIP` | _[EnvoyProxyGeoIP](#envoyproxygeoip)_ | false | | GeoIP defines shared GeoIP provider configuration for this EnvoyProxy fleet. |
| `mergeType` | _[MergeType](#mergetype)_ | false | | MergeType controls how this EnvoyProxy merges with less specific configurations
in the hierarchy (EnvoyGateway defaults < GatewayClass < Gateway).
If unset, this EnvoyProxy completely replaces less specific settings.
Note: this field has no effect when set in EnvoyGateway's default EnvoyProxySpec. |
@@ -4159,6 +4160,24 @@ _Appears in:_
| `filterContext` | _[JSON](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#json-v1-apiextensions-k8s-io)_ | false | | FilterContext is the filter context configuration for the Lua script.
This must be a JSON object (key/value pairs). The values are made available
to the Lua script via request_handle:filterContext(). This allows a shared
script to be parameterized differently per EnvoyExtensionPolicy/route. |
+#### LuaStrictValidation
+
+
+
+LuaStrictValidation defines the configuration that Strict Lua validation runs with.
+
+This configuration only applies to the Strict validation mode; it has no effect on the
+InsecureSyntax and Disabled modes.
+
+_Appears in:_
+- [EnvoyProxySpec](#envoyproxyspec)
+
+| Field | Type | Required | Default | Description |
+| --- | --- | --- | --- | --- |
+| `allowedPaths` | _string array_ | false | | AllowedPaths is the list of filesystem path prefixes that Lua scripts are permitted to
access during validation (via io.open, io.input, io.output, io.lines, os.remove, os.rename).
A path is allowed when it equals an entry or is contained within an entry's subtree
(e.g. "/tmp" allows "/tmp/file.txt"). Paths are normalized (separators collapsed, made
absolute) before matching, and any "." or ".." traversal segment is always rejected.
When empty, all filesystem access is denied. Blank or whitespace-only entries are rejected,
as they would otherwise match every path and disable the sandbox. |
+| `allowedEnvVars` | _string array_ | false | | AllowedEnvVars is the list of environment variable names that Lua scripts are permitted to
access during validation (via os.getenv, os.setenv). Matching is exact and case-sensitive.
When empty, access to all environment variables is denied. Blank or whitespace-only entries
are rejected. |
+
+
#### LuaValidation
_Underlying type:_ _string_
diff --git a/test/cel-validation/envoyproxy_test.go b/test/cel-validation/envoyproxy_test.go
index 20e425157c..78e1ffd812 100644
--- a/test/cel-validation/envoyproxy_test.go
+++ b/test/cel-validation/envoyproxy_test.go
@@ -2485,6 +2485,80 @@ func TestEnvoyProxyProvider(t *testing.T) {
},
wantErrors: []string{"If type is Remote, local field must not be set"},
},
+ {
+ desc: "luaStrictValidation-valid",
+ mutate: func(envoy *egv1a1.EnvoyProxy) {
+ envoy.Spec = egv1a1.EnvoyProxySpec{
+ LuaStrictValidation: &egv1a1.LuaStrictValidation{
+ AllowedPaths: []string{"/tmp"},
+ AllowedEnvVars: []string{"LOG_LEVEL"},
+ },
+ }
+ },
+ wantErrors: []string{},
+ },
+ {
+ desc: "luaStrictValidation-empty-path-rejected",
+ mutate: func(envoy *egv1a1.EnvoyProxy) {
+ envoy.Spec = egv1a1.EnvoyProxySpec{
+ LuaStrictValidation: &egv1a1.LuaStrictValidation{
+ AllowedPaths: []string{""},
+ },
+ }
+ },
+ wantErrors: []string{"should be at least 1 chars long"},
+ },
+ {
+ desc: "luaStrictValidation-whitespace-path-rejected",
+ mutate: func(envoy *egv1a1.EnvoyProxy) {
+ envoy.Spec = egv1a1.EnvoyProxySpec{
+ LuaStrictValidation: &egv1a1.LuaStrictValidation{
+ AllowedPaths: []string{" "},
+ },
+ }
+ },
+ wantErrors: []string{"allowedPaths entries must not be blank or whitespace-only"},
+ },
+ {
+ desc: "luaStrictValidation-whitespace-envvar-rejected",
+ mutate: func(envoy *egv1a1.EnvoyProxy) {
+ envoy.Spec = egv1a1.EnvoyProxySpec{
+ LuaStrictValidation: &egv1a1.LuaStrictValidation{
+ AllowedEnvVars: []string{" "},
+ },
+ }
+ },
+ wantErrors: []string{"allowedEnvVars entries must not be blank or whitespace-only"},
+ },
+ {
+ desc: "luaStrictValidation-with-explicit-strict-allowed",
+ mutate: func(envoy *egv1a1.EnvoyProxy) {
+ envoy.Spec = egv1a1.EnvoyProxySpec{
+ LuaValidation: new(egv1a1.LuaValidationStrict),
+ LuaStrictValidation: &egv1a1.LuaStrictValidation{AllowedPaths: []string{"/tmp"}},
+ }
+ },
+ wantErrors: []string{},
+ },
+ {
+ desc: "luaStrictValidation-with-unset-validation-allowed",
+ mutate: func(envoy *egv1a1.EnvoyProxy) {
+ envoy.Spec = egv1a1.EnvoyProxySpec{
+ LuaStrictValidation: &egv1a1.LuaStrictValidation{AllowedPaths: []string{"/tmp"}},
+ }
+ },
+ wantErrors: []string{},
+ },
+ {
+ desc: "luaStrictValidation-with-insecure-syntax-rejected",
+ mutate: func(envoy *egv1a1.EnvoyProxy) {
+ envoy.Spec = egv1a1.EnvoyProxySpec{
+ LuaValidation: new(egv1a1.LuaValidationInsecureSyntax),
+ LuaStrictValidation: &egv1a1.LuaStrictValidation{AllowedPaths: []string{"/tmp"}},
+ }
+ },
+ wantErrors: []string{"luaStrictValidation can only be set when luaValidation is Strict"},
+ },
}
for _, tc := range cases {
diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml
index 39fc1c10c4..6784a60c22 100644
--- a/test/helm/gateway-crds-helm/all.out.yaml
+++ b/test/helm/gateway-crds-helm/all.out.yaml
@@ -34511,6 +34511,53 @@ spec:
and the log level is the value. If unspecified, defaults to "default: warn".
type: object
type: object
+ luaStrictValidation:
+ description: |-
+ LuaStrictValidation defines the filesystem paths and environment variables that Lua
+ scripts from EnvoyExtensionPolicy resources are permitted to access during Strict
+ validation in the gateway controller.
+
+ The allowlist is fail-closed: when unset or empty, Lua scripts are denied access to ALL
+ filesystem paths and environment variables during validation. Only entries that match the
+ allowlist are permitted.
+
+ This field only takes effect when LuaValidation is Strict (the default). It has no effect
+ for the InsecureSyntax or Disabled validation modes, which do not execute the security sandbox.
+ properties:
+ allowedEnvVars:
+ description: |-
+ AllowedEnvVars is the list of environment variable names that Lua scripts are permitted to
+ access during validation (via os.getenv, os.setenv). Matching is exact and case-sensitive.
+ When empty, access to all environment variables is denied. Blank or whitespace-only entries
+ are rejected.
+ items:
+ maxLength: 256
+ minLength: 1
+ type: string
+ maxItems: 64
+ type: array
+ x-kubernetes-validations:
+ - message: allowedEnvVars entries must not be blank or whitespace-only
+ rule: self.all(e, e.trim() != '')
+ allowedPaths:
+ description: |-
+ AllowedPaths is the list of filesystem path prefixes that Lua scripts are permitted to
+ access during validation (via io.open, io.input, io.output, io.lines, os.remove, os.rename).
+ A path is allowed when it equals an entry or is contained within an entry's subtree
+ (e.g. "/tmp" allows "/tmp/file.txt"). Paths are normalized (separators collapsed, made
+ absolute) before matching, and any "." or ".." traversal segment is always rejected.
+ When empty, all filesystem access is denied. Blank or whitespace-only entries are rejected,
+ as they would otherwise match every path and disable the sandbox.
+ items:
+ maxLength: 4096
+ minLength: 1
+ type: string
+ maxItems: 64
+ type: array
+ x-kubernetes-validations:
+ - message: allowedPaths entries must not be blank or whitespace-only
+ rule: self.all(p, p.trim() != '')
+ type: object
luaValidation:
description: |-
LuaValidation determines strictness of the Lua script validation for Lua EnvoyExtensionPolicies
@@ -51854,6 +51901,10 @@ spec:
rule: '!(has(self.samplingRate) && has(self.samplingFraction))'
type: object
type: object
+ x-kubernetes-validations:
+ - message: luaStrictValidation can only be set when luaValidation is Strict
+ rule: '!has(self.luaStrictValidation) || !has(self.luaValidation) ||
+ self.luaValidation == ''Strict'''
status:
description: EnvoyProxyStatus defines the actual state of EnvoyProxy.
properties:
diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml
index 357137c6ee..e1ea3938d9 100644
--- a/test/helm/gateway-crds-helm/e2e.out.yaml
+++ b/test/helm/gateway-crds-helm/e2e.out.yaml
@@ -10449,6 +10449,53 @@ spec:
and the log level is the value. If unspecified, defaults to "default: warn".
type: object
type: object
+ luaStrictValidation:
+ description: |-
+ LuaStrictValidation defines the filesystem paths and environment variables that Lua
+ scripts from EnvoyExtensionPolicy resources are permitted to access during Strict
+ validation in the gateway controller.
+
+ The allowlist is fail-closed: when unset or empty, Lua scripts are denied access to ALL
+ filesystem paths and environment variables during validation. Only entries that match the
+ allowlist are permitted.
+
+ This field only takes effect when LuaValidation is Strict (the default). It has no effect
+ for the InsecureSyntax or Disabled validation modes, which do not execute the security sandbox.
+ properties:
+ allowedEnvVars:
+ description: |-
+ AllowedEnvVars is the list of environment variable names that Lua scripts are permitted to
+ access during validation (via os.getenv, os.setenv). Matching is exact and case-sensitive.
+ When empty, access to all environment variables is denied. Blank or whitespace-only entries
+ are rejected.
+ items:
+ maxLength: 256
+ minLength: 1
+ type: string
+ maxItems: 64
+ type: array
+ x-kubernetes-validations:
+ - message: allowedEnvVars entries must not be blank or whitespace-only
+ rule: self.all(e, e.trim() != '')
+ allowedPaths:
+ description: |-
+ AllowedPaths is the list of filesystem path prefixes that Lua scripts are permitted to
+ access during validation (via io.open, io.input, io.output, io.lines, os.remove, os.rename).
+ A path is allowed when it equals an entry or is contained within an entry's subtree
+ (e.g. "/tmp" allows "/tmp/file.txt"). Paths are normalized (separators collapsed, made
+ absolute) before matching, and any "." or ".." traversal segment is always rejected.
+ When empty, all filesystem access is denied. Blank or whitespace-only entries are rejected,
+ as they would otherwise match every path and disable the sandbox.
+ items:
+ maxLength: 4096
+ minLength: 1
+ type: string
+ maxItems: 64
+ type: array
+ x-kubernetes-validations:
+ - message: allowedPaths entries must not be blank or whitespace-only
+ rule: self.all(p, p.trim() != '')
+ type: object
luaValidation:
description: |-
LuaValidation determines strictness of the Lua script validation for Lua EnvoyExtensionPolicies
@@ -27792,6 +27839,10 @@ spec:
rule: '!(has(self.samplingRate) && has(self.samplingFraction))'
type: object
type: object
+ x-kubernetes-validations:
+ - message: luaStrictValidation can only be set when luaValidation is Strict
+ rule: '!has(self.luaStrictValidation) || !has(self.luaValidation) ||
+ self.luaValidation == ''Strict'''
status:
description: EnvoyProxyStatus defines the actual state of EnvoyProxy.
properties:
diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
index 44ab69394c..f7327ea843 100644
--- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
+++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
@@ -10449,6 +10449,53 @@ spec:
and the log level is the value. If unspecified, defaults to "default: warn".
type: object
type: object
+ luaStrictValidation:
+ description: |-
+ LuaStrictValidation defines the filesystem paths and environment variables that Lua
+ scripts from EnvoyExtensionPolicy resources are permitted to access during Strict
+ validation in the gateway controller.
+
+ The allowlist is fail-closed: when unset or empty, Lua scripts are denied access to ALL
+ filesystem paths and environment variables during validation. Only entries that match the
+ allowlist are permitted.
+
+ This field only takes effect when LuaValidation is Strict (the default). It has no effect
+ for the InsecureSyntax or Disabled validation modes, which do not execute the security sandbox.
+ properties:
+ allowedEnvVars:
+ description: |-
+ AllowedEnvVars is the list of environment variable names that Lua scripts are permitted to
+ access during validation (via os.getenv, os.setenv). Matching is exact and case-sensitive.
+ When empty, access to all environment variables is denied. Blank or whitespace-only entries
+ are rejected.
+ items:
+ maxLength: 256
+ minLength: 1
+ type: string
+ maxItems: 64
+ type: array
+ x-kubernetes-validations:
+ - message: allowedEnvVars entries must not be blank or whitespace-only
+ rule: self.all(e, e.trim() != '')
+ allowedPaths:
+ description: |-
+ AllowedPaths is the list of filesystem path prefixes that Lua scripts are permitted to
+ access during validation (via io.open, io.input, io.output, io.lines, os.remove, os.rename).
+ A path is allowed when it equals an entry or is contained within an entry's subtree
+ (e.g. "/tmp" allows "/tmp/file.txt"). Paths are normalized (separators collapsed, made
+ absolute) before matching, and any "." or ".." traversal segment is always rejected.
+ When empty, all filesystem access is denied. Blank or whitespace-only entries are rejected,
+ as they would otherwise match every path and disable the sandbox.
+ items:
+ maxLength: 4096
+ minLength: 1
+ type: string
+ maxItems: 64
+ type: array
+ x-kubernetes-validations:
+ - message: allowedPaths entries must not be blank or whitespace-only
+ rule: self.all(p, p.trim() != '')
+ type: object
luaValidation:
description: |-
LuaValidation determines strictness of the Lua script validation for Lua EnvoyExtensionPolicies
@@ -27792,6 +27839,10 @@ spec:
rule: '!(has(self.samplingRate) && has(self.samplingFraction))'
type: object
type: object
+ x-kubernetes-validations:
+ - message: luaStrictValidation can only be set when luaValidation is Strict
+ rule: '!has(self.luaStrictValidation) || !has(self.luaValidation) ||
+ self.luaValidation == ''Strict'''
status:
description: EnvoyProxyStatus defines the actual state of EnvoyProxy.
properties: