diff --git a/api/v1alpha1/envoyproxy_types.go b/api/v1alpha1/envoyproxy_types.go
index 5d688099a6..e4f5eaa800 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.luaValidation) || !has(self.lua)",message="only one of luaValidation or lua may be set"
// +kubebuilder:validation:XValidation:message="mergeGateways and mergeBackends cannot both be enabled",rule="!(has(self.mergeGateways) && self.mergeGateways && has(self.mergeBackends))"
type EnvoyProxySpec struct {
// Provider defines the desired resource provider and provider-specific configuration.
@@ -200,9 +201,19 @@ type EnvoyProxySpec struct {
// LuaValidation determines strictness of the Lua script validation for Lua EnvoyExtensionPolicies
// Default: Strict
+ //
+ // Deprecated: Use Lua.ValidationType instead. This field will be removed in a future release.
// +optional
LuaValidation *LuaValidation `json:"luaValidation,omitempty"`
+ // Lua configures how Lua scripts from EnvoyExtensionPolicy resources are
+ // validated in the gateway controller. It selects the validation mode and, for the Strict
+ // mode, defines the filesystem paths and environment variables the scripts are permitted to
+ // access during validation.
+ //
+ // +optional
+ Lua *LuaValidationConfig `json:"lua,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
@@ -269,6 +280,66 @@ const (
LuaValidationDisabled LuaValidation = "Disabled"
)
+// LuaValidationConfig configures how Lua scripts from EnvoyExtensionPolicy resources are validated
+// in the gateway controller.
+//
+// +union
+// +kubebuilder:validation:XValidation:rule="!has(self.strictValidation) || !has(self.validationType) || self.validationType == 'Strict'",message="strictValidation can only be set when validationType is Strict"
+type LuaValidationConfig struct {
+ // ValidationType determines the strictness of the Lua script validation.
+ // Default: Strict
+ //
+ // +unionDiscriminator
+ // +kubebuilder:default=Strict
+ // +optional
+ ValidationType *LuaValidation `json:"validationType,omitempty"`
+
+ // StrictValidation configures the security sandbox that the Strict validation mode executes Lua
+ // scripts in, defining the filesystem paths and environment variables the scripts are permitted
+ // to access during validation.
+ //
+ // It has no effect for the InsecureSyntax or Disabled modes, which do not execute the security
+ // sandbox.
+ //
+ // +optional
+ StrictValidation *StrictValidation `json:"strictValidation,omitempty"`
+}
+
+// StrictValidation 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 StrictValidation 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. The filesystem root ("/")
+ // is likewise rejected, as it would allow access to the entire filesystem and defeat 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"
+ // +kubebuilder:validation:XValidation:rule="self.all(p, !p.matches('^/+$'))",message="allowedPaths entries must not be the filesystem root"
+ // +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
@@ -700,6 +771,8 @@ type EnvoyProxyConditionType string
const (
EnvoyProxyConditionAccepted EnvoyProxyConditionType = "Accepted"
+
+ EnvoyProxyConditionWarning EnvoyProxyConditionType = "Warning"
)
type EnvoyProxyConditionReason string
@@ -708,6 +781,8 @@ const (
EnvoyProxyReasonAccepted EnvoyProxyConditionReason = "Accepted"
EnvoyProxyReasonInvalidParameters EnvoyProxyConditionReason = "InvalidParameters"
+
+ EnvoyProxyReasonDeprecatedField EnvoyProxyConditionReason = "DeprecatedField"
)
// +kubebuilder:object:root=true
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 8cacd0bb56..bebc2e8137 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -3402,6 +3402,11 @@ func (in *EnvoyProxySpec) DeepCopyInto(out *EnvoyProxySpec) {
*out = new(LuaValidation)
**out = **in
}
+ if in.Lua != nil {
+ in, out := &in.Lua, &out.Lua
+ *out = new(LuaValidationConfig)
+ (*in).DeepCopyInto(*out)
+ }
if in.DynamicModules != nil {
in, out := &in.DynamicModules, &out.DynamicModules
*out = make([]DynamicModuleEntry, len(*in))
@@ -6114,6 +6119,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 *LuaValidationConfig) DeepCopyInto(out *LuaValidationConfig) {
+ *out = *in
+ if in.ValidationType != nil {
+ in, out := &in.ValidationType, &out.ValidationType
+ *out = new(LuaValidation)
+ **out = **in
+ }
+ if in.StrictValidation != nil {
+ in, out := &in.StrictValidation, &out.StrictValidation
+ *out = new(StrictValidation)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LuaValidationConfig.
+func (in *LuaValidationConfig) DeepCopy() *LuaValidationConfig {
+ if in == nil {
+ return nil
+ }
+ out := new(LuaValidationConfig)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MergeBackendsConfig) DeepCopyInto(out *MergeBackendsConfig) {
*out = *in
@@ -8394,6 +8424,31 @@ func (in *StatusCodeRange) DeepCopy() *StatusCodeRange {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *StrictValidation) DeepCopyInto(out *StrictValidation) {
+ *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 StrictValidation.
+func (in *StrictValidation) DeepCopy() *StrictValidation {
+ if in == nil {
+ return nil
+ }
+ out := new(StrictValidation)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StringMatch) DeepCopyInto(out *StringMatch) {
*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 55af585090..68b7937c13 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
@@ -772,10 +772,82 @@ spec:
and the log level is the value. If unspecified, defaults to "default: warn".
type: object
type: object
+ lua:
+ description: |-
+ Lua configures how Lua scripts from EnvoyExtensionPolicy resources are
+ validated in the gateway controller. It selects the validation mode and, for the Strict
+ mode, defines the filesystem paths and environment variables the scripts are permitted to
+ access during validation.
+ properties:
+ strictValidation:
+ description: |-
+ StrictValidation configures the security sandbox that the Strict validation mode executes Lua
+ scripts in, defining the filesystem paths and environment variables the scripts are permitted
+ to access during validation.
+
+ It has no effect for the InsecureSyntax or Disabled 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. The filesystem root ("/")
+ is likewise rejected, as it would allow access to the entire filesystem and defeat 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() != '')
+ - message: allowedPaths entries must not be the filesystem
+ root
+ rule: self.all(p, !p.matches('^/+$'))
+ type: object
+ validationType:
+ default: Strict
+ description: |-
+ ValidationType determines the strictness of the Lua script validation.
+ Default: Strict
+ enum:
+ - Strict
+ - InsecureSyntax
+ - Disabled
+ type: string
+ type: object
+ x-kubernetes-validations:
+ - message: strictValidation can only be set when validationType is
+ Strict
+ rule: '!has(self.strictValidation) || !has(self.validationType)
+ || self.validationType == ''Strict'''
luaValidation:
description: |-
LuaValidation determines strictness of the Lua script validation for Lua EnvoyExtensionPolicies
Default: Strict
+
+ Deprecated: Use Lua.ValidationType instead. This field will be removed in a future release.
enum:
- Strict
- InsecureSyntax
@@ -18254,6 +18326,8 @@ spec:
type: object
type: object
x-kubernetes-validations:
+ - message: only one of luaValidation or lua may be set
+ rule: '!has(self.luaValidation) || !has(self.lua)'
- message: mergeGateways and mergeBackends cannot both be enabled
rule: '!(has(self.mergeGateways) && self.mergeGateways && has(self.mergeBackends))'
status:
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 bbde1a6b0c..b1b7124499 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
@@ -771,10 +771,82 @@ spec:
and the log level is the value. If unspecified, defaults to "default: warn".
type: object
type: object
+ lua:
+ description: |-
+ Lua configures how Lua scripts from EnvoyExtensionPolicy resources are
+ validated in the gateway controller. It selects the validation mode and, for the Strict
+ mode, defines the filesystem paths and environment variables the scripts are permitted to
+ access during validation.
+ properties:
+ strictValidation:
+ description: |-
+ StrictValidation configures the security sandbox that the Strict validation mode executes Lua
+ scripts in, defining the filesystem paths and environment variables the scripts are permitted
+ to access during validation.
+
+ It has no effect for the InsecureSyntax or Disabled 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. The filesystem root ("/")
+ is likewise rejected, as it would allow access to the entire filesystem and defeat 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() != '')
+ - message: allowedPaths entries must not be the filesystem
+ root
+ rule: self.all(p, !p.matches('^/+$'))
+ type: object
+ validationType:
+ default: Strict
+ description: |-
+ ValidationType determines the strictness of the Lua script validation.
+ Default: Strict
+ enum:
+ - Strict
+ - InsecureSyntax
+ - Disabled
+ type: string
+ type: object
+ x-kubernetes-validations:
+ - message: strictValidation can only be set when validationType is
+ Strict
+ rule: '!has(self.strictValidation) || !has(self.validationType)
+ || self.validationType == ''Strict'''
luaValidation:
description: |-
LuaValidation determines strictness of the Lua script validation for Lua EnvoyExtensionPolicies
Default: Strict
+
+ Deprecated: Use Lua.ValidationType instead. This field will be removed in a future release.
enum:
- Strict
- InsecureSyntax
@@ -18253,6 +18325,8 @@ spec:
type: object
type: object
x-kubernetes-validations:
+ - message: only one of luaValidation or lua may be set
+ rule: '!has(self.luaValidation) || !has(self.lua)'
- message: mergeGateways and mergeBackends cannot both be enabled
rule: '!(has(self.mergeGateways) && self.mergeGateways && has(self.mergeBackends))'
status:
diff --git a/internal/gatewayapi/luavalidator/lua_validator.go b/internal/gatewayapi/luavalidator/lua_validator.go
index fbdc0316d2..2fe2bc71ea 100644
--- a/internal/gatewayapi/luavalidator/lua_validator.go
+++ b/internal/gatewayapi/luavalidator/lua_validator.go
@@ -82,14 +82,75 @@ func (l *LuaValidator) validate(code string) error {
}
}
-// getLuaValidation returns the Lua validation level, defaulting to strict if not configured
+// getLuaValidation returns the Lua validation level, defaulting to strict if not configured.
+// The union Lua.ValidationType takes precedence over the deprecated LuaValidation field.
func (l *LuaValidator) getLuaValidation() egv1a1.LuaValidation {
- if l.envoyProxy != nil && l.envoyProxy.Spec.LuaValidation != nil {
- return *l.envoyProxy.Spec.LuaValidation
+ if l.envoyProxy != nil {
+ if cfg := l.envoyProxy.Spec.Lua; cfg != nil && cfg.ValidationType != nil {
+ return *cfg.ValidationType
+ }
+ if l.envoyProxy.Spec.LuaValidation != nil {
+ return *l.envoyProxy.Spec.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 {
+ if cfg := l.envoyProxy.Spec.Lua; cfg != nil && cfg.StrictValidation != nil {
+ paths = cfg.StrictValidation.AllowedPaths
+ envVars = cfg.StrictValidation.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 +184,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..77dfac25c1 100644
--- a/internal/gatewayapi/luavalidator/lua_validator_test.go
+++ b/internal/gatewayapi/luavalidator/lua_validator_test.go
@@ -209,431 +209,317 @@ 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{
+ Lua: &egv1a1.LuaValidationConfig{
+ StrictValidation: &egv1a1.StrictValidation{
+ 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/internal/gatewayapi/status/envoyproxy.go b/internal/gatewayapi/status/envoyproxy.go
index 9f942d156f..b00ebd957e 100644
--- a/internal/gatewayapi/status/envoyproxy.go
+++ b/internal/gatewayapi/status/envoyproxy.go
@@ -42,3 +42,28 @@ func UpdateEnvoyProxyStatusAccepted(ep *egv1a1.EnvoyProxy, ancestor *gwapiv1.Par
},
})
}
+
+func SetEnvoyProxyDeprecatedFieldsWarning(ep *egv1a1.EnvoyProxy, ancestor *gwapiv1.ParentReference, deprecatedFields map[string]string) {
+ if ep == nil || ancestor == nil || len(deprecatedFields) == 0 {
+ return
+ }
+
+ cond := newCondition(string(egv1a1.EnvoyProxyConditionWarning), metav1.ConditionTrue,
+ string(egv1a1.EnvoyProxyReasonDeprecatedField), buildDeprecationWarningMessage(deprecatedFields), ep.Generation)
+
+ for i := range ep.Status.Ancestors {
+ item := ep.Status.Ancestors[i]
+ if ancestorRefsEqual(&item.AncestorRef, ancestor) {
+ ep.Status.Ancestors[i].Conditions = MergeConditions(item.Conditions, cond)
+ return
+ }
+ }
+
+ // ancestor not found, append a new one
+ ep.Status.Ancestors = append(ep.Status.Ancestors, egv1a1.EnvoyProxyAncestorStatus{
+ AncestorRef: *ancestor,
+ Conditions: []metav1.Condition{
+ cond,
+ },
+ })
+}
diff --git a/internal/gatewayapi/status/envoyproxy_test.go b/internal/gatewayapi/status/envoyproxy_test.go
new file mode 100644
index 0000000000..7e8b2cadbe
--- /dev/null
+++ b/internal/gatewayapi/status/envoyproxy_test.go
@@ -0,0 +1,53 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+package status
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
+
+ egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
+)
+
+func TestSetEnvoyProxyDeprecatedFieldsWarning(t *testing.T) {
+ ancestor := &gwapiv1.ParentReference{Name: gwapiv1.ObjectName("eg")}
+
+ t.Run("sets a warning condition when deprecated fields are used", func(t *testing.T) {
+ ep := &egv1a1.EnvoyProxy{}
+ SetEnvoyProxyDeprecatedFieldsWarning(ep, ancestor, map[string]string{
+ "spec.luaValidation": "spec.lua.validationType",
+ })
+
+ assert.Len(t, ep.Status.Ancestors, 1)
+ conds := ep.Status.Ancestors[0].Conditions
+ assert.Len(t, conds, 1)
+ assert.Equal(t, string(egv1a1.EnvoyProxyConditionWarning), conds[0].Type)
+ assert.Equal(t, metav1.ConditionTrue, conds[0].Status)
+ assert.Equal(t, string(egv1a1.EnvoyProxyReasonDeprecatedField), conds[0].Reason)
+ assert.Equal(t, "spec.luaValidation is deprecated, use spec.lua.validationType instead", conds[0].Message)
+ })
+
+ t.Run("no-op when no deprecated fields are used", func(t *testing.T) {
+ ep := &egv1a1.EnvoyProxy{}
+ SetEnvoyProxyDeprecatedFieldsWarning(ep, ancestor, nil)
+ assert.Empty(t, ep.Status.Ancestors)
+ })
+
+ t.Run("appends the warning alongside an existing Accepted condition on the same ancestor", func(t *testing.T) {
+ ep := &egv1a1.EnvoyProxy{}
+ UpdateEnvoyProxyStatusAccepted(ep, ancestor, egv1a1.EnvoyProxyReasonAccepted, "EnvoyProxy has been accepted.")
+ SetEnvoyProxyDeprecatedFieldsWarning(ep, ancestor, map[string]string{
+ "spec.luaValidation": "spec.lua.validationType",
+ })
+
+ assert.Len(t, ep.Status.Ancestors, 1)
+ conds := ep.Status.Ancestors[0].Conditions
+ assert.Len(t, conds, 2)
+ })
+}
diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua-validation-disabled.in.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua-validation-disabled.in.yaml
index ecfffbcfbb..a2c4b4ff5d 100644
--- a/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua-validation-disabled.in.yaml
+++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua-validation-disabled.in.yaml
@@ -1,3 +1,15 @@
+gatewayClass:
+ apiVersion: gateway.networking.k8s.io/v1
+ kind: GatewayClass
+ metadata:
+ name: envoy-gateway-class
+ spec:
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parametersRef:
+ group: gateway.envoyproxy.io
+ kind: EnvoyProxy
+ name: test
+ namespace: envoy-gateway-system
envoyProxyForGatewayClass:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua-validation-disabled.out.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua-validation-disabled.out.yaml
index 68b4e397ba..82ae56e314 100644
--- a/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua-validation-disabled.out.yaml
+++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua-validation-disabled.out.yaml
@@ -38,6 +38,51 @@ envoyExtensionPolicies:
status: "True"
type: Warning
controllerName: gateway.envoyproxy.io/gatewayclass-controller
+envoyProxyForGatewayClass:
+ apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: EnvoyProxy
+ metadata:
+ name: test
+ namespace: envoy-gateway-system
+ spec:
+ logging: {}
+ luaValidation: Disabled
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: GatewayClass
+ name: envoy-gateway-class
+ conditions:
+ - lastTransitionTime: null
+ message: EnvoyProxy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: spec.luaValidation is deprecated, use spec.lua.validationType instead
+ reason: DeprecatedField
+ status: "True"
+ type: Warning
+gatewayClass:
+ apiVersion: gateway.networking.k8s.io/v1
+ kind: GatewayClass
+ metadata:
+ name: envoy-gateway-class
+ spec:
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parametersRef:
+ group: gateway.envoyproxy.io
+ kind: EnvoyProxy
+ name: test
+ namespace: envoy-gateway-system
+ status:
+ conditions:
+ - lastTransitionTime: null
+ message: Valid GatewayClass
+ reason: Accepted
+ status: "True"
+ type: Accepted
gateways:
- apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
@@ -128,7 +173,24 @@ infraIR:
spec:
logging: {}
luaValidation: Disabled
- status: {}
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: GatewayClass
+ name: envoy-gateway-class
+ conditions:
+ - lastTransitionTime: null
+ message: EnvoyProxy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: spec.luaValidation is deprecated, use spec.lua.validationType
+ instead
+ reason: DeprecatedField
+ status: "True"
+ type: Warning
listeners:
- name: envoy-gateway/gateway-1/http
ports:
diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua-validation-syntax.in.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua-validation-syntax.in.yaml
index 6bce04172c..3360fffe4a 100644
--- a/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua-validation-syntax.in.yaml
+++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua-validation-syntax.in.yaml
@@ -1,3 +1,15 @@
+gatewayClass:
+ apiVersion: gateway.networking.k8s.io/v1
+ kind: GatewayClass
+ metadata:
+ name: envoy-gateway-class
+ spec:
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parametersRef:
+ group: gateway.envoyproxy.io
+ kind: EnvoyProxy
+ name: test
+ namespace: envoy-gateway-system
envoyProxyForGatewayClass:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua-validation-syntax.out.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua-validation-syntax.out.yaml
index b79ae5ac67..3e26a3cfd4 100644
--- a/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua-validation-syntax.out.yaml
+++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-invalid-lua-validation-syntax.out.yaml
@@ -91,6 +91,51 @@ envoyExtensionPolicies:
status: "True"
type: Warning
controllerName: gateway.envoyproxy.io/gatewayclass-controller
+envoyProxyForGatewayClass:
+ apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: EnvoyProxy
+ metadata:
+ name: test
+ namespace: envoy-gateway-system
+ spec:
+ logging: {}
+ luaValidation: InsecureSyntax
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: GatewayClass
+ name: envoy-gateway-class
+ conditions:
+ - lastTransitionTime: null
+ message: EnvoyProxy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: spec.luaValidation is deprecated, use spec.lua.validationType instead
+ reason: DeprecatedField
+ status: "True"
+ type: Warning
+gatewayClass:
+ apiVersion: gateway.networking.k8s.io/v1
+ kind: GatewayClass
+ metadata:
+ name: envoy-gateway-class
+ spec:
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parametersRef:
+ group: gateway.envoyproxy.io
+ kind: EnvoyProxy
+ name: test
+ namespace: envoy-gateway-system
+ status:
+ conditions:
+ - lastTransitionTime: null
+ message: Valid GatewayClass
+ reason: Accepted
+ status: "True"
+ type: Accepted
gateways:
- apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
@@ -218,7 +263,24 @@ infraIR:
spec:
logging: {}
luaValidation: InsecureSyntax
- status: {}
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: GatewayClass
+ name: envoy-gateway-class
+ conditions:
+ - lastTransitionTime: null
+ message: EnvoyProxy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: spec.luaValidation is deprecated, use spec.lua.validationType
+ instead
+ reason: DeprecatedField
+ status: "True"
+ type: Warning
listeners:
- name: envoy-gateway/gateway-1/http
ports:
diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-validation-config.in.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-validation-config.in.yaml
new file mode 100644
index 0000000000..aee16f48bd
--- /dev/null
+++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-validation-config.in.yaml
@@ -0,0 +1,76 @@
+gatewayClass:
+ apiVersion: gateway.networking.k8s.io/v1
+ kind: GatewayClass
+ metadata:
+ name: envoy-gateway-class
+ spec:
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parametersRef:
+ group: gateway.envoyproxy.io
+ kind: EnvoyProxy
+ name: test
+ namespace: envoy-gateway-system
+envoyProxyForGatewayClass:
+ apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: EnvoyProxy
+ metadata:
+ namespace: envoy-gateway-system
+ name: test
+ spec:
+ lua:
+ validationType: Disabled
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ namespace: envoy-gateway
+ name: gateway-1
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - name: http
+ protocol: HTTP
+ port: 80
+ allowedRoutes:
+ namespaces:
+ from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ namespace: default
+ name: httproute-1
+ spec:
+ hostnames:
+ - www.example.com
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-1
+ sectionName: http
+ rules:
+ - matches:
+ - path:
+ value: "/foo"
+ backendRefs:
+ - name: service-1
+ port: 8080
+envoyextensionpolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: EnvoyExtensionPolicy
+ metadata:
+ namespace: default
+ name: policy-for-http-route
+ spec:
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: httproute-1
+ lua:
+ - type: Inline # Invalid Lua syntax (missing then keyword in if statement) but should be accepted
+ inline: |
+ function envoy_on_response(response_handle)
+ local value = 10
+ if value > 5
+ print("Value is greater than 5")
+ end
+ end
diff --git a/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-validation-config.out.yaml b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-validation-config.out.yaml
new file mode 100644
index 0000000000..a8880d7ab8
--- /dev/null
+++ b/internal/gatewayapi/testdata/envoyextensionpolicy-with-lua-validation-config.out.yaml
@@ -0,0 +1,288 @@
+envoyExtensionPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: EnvoyExtensionPolicy
+ metadata:
+ name: policy-for-http-route
+ namespace: default
+ spec:
+ lua:
+ - inline: |
+ function envoy_on_response(response_handle)
+ local value = 10
+ if value > 5
+ print("Value is greater than 5")
+ end
+ end
+ type: Inline
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ name: httproute-1
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ conditions:
+ - lastTransitionTime: null
+ message: Policy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: spec.targetRef is deprecated, use spec.targetRefs instead
+ reason: DeprecatedField
+ status: "True"
+ type: Warning
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+envoyProxyForGatewayClass:
+ apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: EnvoyProxy
+ metadata:
+ name: test
+ namespace: envoy-gateway-system
+ spec:
+ logging: {}
+ lua:
+ validationType: Disabled
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: GatewayClass
+ name: envoy-gateway-class
+ conditions:
+ - lastTransitionTime: null
+ message: EnvoyProxy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+gatewayClass:
+ apiVersion: gateway.networking.k8s.io/v1
+ kind: GatewayClass
+ metadata:
+ name: envoy-gateway-class
+ spec:
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parametersRef:
+ group: gateway.envoyproxy.io
+ kind: EnvoyProxy
+ name: test
+ namespace: envoy-gateway-system
+ status:
+ conditions:
+ - lastTransitionTime: null
+ message: Valid GatewayClass
+ reason: Accepted
+ status: "True"
+ type: Accepted
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ name: gateway-1
+ namespace: envoy-gateway
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - allowedRoutes:
+ namespaces:
+ from: All
+ name: http
+ port: 80
+ protocol: HTTP
+ status:
+ listeners:
+ - attachedRoutes: 1
+ conditions:
+ - lastTransitionTime: null
+ message: Sending translated listener configuration to the data plane
+ reason: Programmed
+ status: "True"
+ type: Programmed
+ - lastTransitionTime: null
+ message: Listener has been successfully translated
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Listener references have been resolved
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ name: http
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ - group: gateway.networking.k8s.io
+ kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ name: httproute-1
+ namespace: default
+ spec:
+ hostnames:
+ - www.example.com
+ parentRefs:
+ - name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8080
+ matches:
+ - path:
+ value: /foo
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: Route is accepted
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Resolved all the Object references for the Route
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+infraIR:
+ envoy-gateway/gateway-1:
+ proxy:
+ config:
+ apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: EnvoyProxy
+ metadata:
+ name: test
+ namespace: envoy-gateway-system
+ spec:
+ logging: {}
+ lua:
+ validationType: Disabled
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: GatewayClass
+ name: envoy-gateway-class
+ conditions:
+ - lastTransitionTime: null
+ message: EnvoyProxy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ listeners:
+ - name: envoy-gateway/gateway-1/http
+ ports:
+ - containerPort: 10080
+ name: http-80
+ protocol: HTTP
+ servicePort: 80
+ metadata:
+ labels:
+ gateway.envoyproxy.io/owning-gateway-name: gateway-1
+ gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+ ownerReference:
+ kind: GatewayClass
+ name: envoy-gateway-class
+ name: envoy-gateway/gateway-1
+ namespace: envoy-gateway-system
+xdsIR:
+ envoy-gateway/gateway-1:
+ accessLog:
+ json:
+ - path: /dev/stdout
+ globalResources:
+ proxyServiceCluster:
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-1-196ae069
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-1
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.6.5.4
+ port: 8080
+ zone: zone1
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-1-196ae069
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-1
+ protocol: TCP
+ http:
+ - address: 0.0.0.0
+ externalPort: 80
+ hostnames:
+ - '*'
+ metadata:
+ kind: Gateway
+ name: gateway-1
+ namespace: envoy-gateway
+ sectionName: http
+ name: envoy-gateway/gateway-1/http
+ path:
+ escapedSlashesAction: UnescapeAndRedirect
+ mergeSlashes: true
+ port: 10080
+ routes:
+ - destination:
+ metadata:
+ kind: HTTPRoute
+ name: httproute-1
+ namespace: default
+ name: httproute/default/httproute-1/rule/0
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.7
+ port: 8080
+ metadata:
+ kind: Service
+ name: service-1
+ namespace: default
+ sectionName: "8080"
+ name: httproute/default/httproute-1/rule/0/backend/0
+ protocol: HTTP
+ weight: 1
+ envoyExtensions:
+ luas:
+ - Code: |
+ function envoy_on_response(response_handle)
+ local value = 10
+ if value > 5
+ print("Value is greater than 5")
+ end
+ end
+ FilterContext: null
+ Name: envoyextensionpolicy/default/policy-for-http-route/lua/0
+ hostname: www.example.com
+ isHTTP2: false
+ metadata:
+ kind: HTTPRoute
+ name: httproute-1
+ namespace: default
+ name: httproute/default/httproute-1/rule/0/match/0/www_example_com
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /foo
+ readyListener:
+ address: 0.0.0.0
+ ipFamily: IPv4
+ path: /ready
+ port: 19003
diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go
index 1259448c32..9776a5f40b 100644
--- a/internal/gatewayapi/translator.go
+++ b/internal/gatewayapi/translator.go
@@ -482,6 +482,7 @@ func (t *Translator) GetRelevantGateways(resources *resource.Resources) (
status.UpdateEnvoyProxyStatusAccepted(ep, ancestor,
egv1a1.EnvoyProxyReasonAccepted, "EnvoyProxy has been accepted.")
+ status.SetEnvoyProxyDeprecatedFieldsWarning(ep, ancestor, deprecatedFieldsUsedInEnvoyProxy(ep))
}
}
@@ -557,6 +558,7 @@ func (t *Translator) GetRelevantGateways(resources *resource.Resources) (
if gCtx.envoyProxyFromGateway {
status.UpdateEnvoyProxyStatusAccepted(ep, ancestor,
egv1a1.EnvoyProxyReasonAccepted, "EnvoyProxy has been accepted.")
+ status.SetEnvoyProxyDeprecatedFieldsWarning(ep, ancestor, deprecatedFieldsUsedInEnvoyProxy(ep))
}
}
@@ -587,6 +589,15 @@ func validateEnvoyProxy(ep *egv1a1.EnvoyProxy) error {
return nil
}
+func deprecatedFieldsUsedInEnvoyProxy(ep *egv1a1.EnvoyProxy) map[string]string {
+ deprecatedFields := make(map[string]string)
+ if ep.Spec.LuaValidation != nil {
+ deprecatedFields["spec.luaValidation"] = "spec.lua.validationType"
+ }
+
+ return deprecatedFields
+}
+
// InitIRs checks if mergeGateways is enabled in EnvoyProxy config and initializes XdsIR and InfraIR maps with adequate keys.
func (t *Translator) InitIRs(acceptedGateways, failedGateways []*GatewayContext) (map[string]*ir.Xds, map[string]*ir.Infra) {
xdsIR := make(resource.XdsIRMap)
diff --git a/internal/xds/translator/testdata/out/xds-ir/csrf.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/csrf.listeners.yaml
index 40c45e8f98..db199e33fb 100644
--- a/internal/xds/translator/testdata/out/xds-ir/csrf.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/csrf.listeners.yaml
@@ -29,6 +29,7 @@
rds:
configSource:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
routeConfigName: first-listener
serverHeaderTransformation: PASS_THROUGH
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-system-truststore-per-resource-secret.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-system-truststore-per-resource-secret.clusters.yaml
index e783538f33..f9b69e3e7e 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-system-truststore-per-resource-secret.clusters.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-system-truststore-per-resource-secret.clusters.yaml
@@ -44,6 +44,7 @@
name: policy-btls-1/policies-ca
sdsConfig:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
sni: backend-1.example.com
type: EDS
@@ -101,6 +102,7 @@
name: policy-btls-2/policies-ca
sdsConfig:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
sni: backend-2.example.com
type: EDS
@@ -158,6 +160,7 @@
name: policy-btls-3/policies-ca
sdsConfig:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
sni: backend-3.example.com
type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-system-truststore-per-resource-secret.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-system-truststore-per-resource-secret.listeners.yaml
index 86036a19d5..0214dd90f2 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-system-truststore-per-resource-secret.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-system-truststore-per-resource-secret.listeners.yaml
@@ -24,6 +24,7 @@
rds:
configSource:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
routeConfigName: envoy-gateway/gateway-btls/http
serverHeaderTransformation: PASS_THROUGH
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-system-truststore.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-system-truststore.clusters.yaml
index 6f130d2e01..51fabace61 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-system-truststore.clusters.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-system-truststore.clusters.yaml
@@ -44,6 +44,7 @@
name: system_ca_certificates
sdsConfig:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
sni: backend-1.example.com
type: EDS
@@ -101,6 +102,7 @@
name: system_ca_certificates
sdsConfig:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
sni: backend-2.example.com
type: EDS
@@ -158,6 +160,7 @@
name: system_ca_certificates
sdsConfig:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
sni: backend-3.example.com
type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-system-truststore.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-system-truststore.listeners.yaml
index 86036a19d5..0214dd90f2 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-system-truststore.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-system-truststore.listeners.yaml
@@ -24,6 +24,7 @@
rds:
configSource:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
routeConfigName: envoy-gateway/gateway-btls/http
serverHeaderTransformation: PASS_THROUGH
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-with-tls-system-truststore-per-resource-secret.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-with-tls-system-truststore-per-resource-secret.clusters.yaml
index f08220608d..666b15f596 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-with-tls-system-truststore-per-resource-secret.clusters.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-with-tls-system-truststore-per-resource-secret.clusters.yaml
@@ -44,6 +44,7 @@
name: policy-btls/policies-ca
sdsConfig:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
sni: example.com
type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-with-tls-system-truststore-per-resource-secret.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-with-tls-system-truststore-per-resource-secret.listeners.yaml
index 86036a19d5..0214dd90f2 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-with-tls-system-truststore-per-resource-secret.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-with-tls-system-truststore-per-resource-secret.listeners.yaml
@@ -24,6 +24,7 @@
rds:
configSource:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
routeConfigName: envoy-gateway/gateway-btls/http
serverHeaderTransformation: PASS_THROUGH
diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-system-truststore-enforcement.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-system-truststore-enforcement.clusters.yaml
index cf86938840..0d3758ee97 100644
--- a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-system-truststore-enforcement.clusters.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-system-truststore-enforcement.clusters.yaml
@@ -44,6 +44,7 @@
name: system_ca_certificates
sdsConfig:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
sni: example.com
type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-system-truststore-enforcement.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-system-truststore-enforcement.listeners.yaml
index 86036a19d5..0214dd90f2 100644
--- a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-system-truststore-enforcement.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-system-truststore-enforcement.listeners.yaml
@@ -24,6 +24,7 @@
rds:
configSource:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
routeConfigName: envoy-gateway/gateway-btls/http
serverHeaderTransformation: PASS_THROUGH
diff --git a/internal/xds/translator/testdata/out/xds-ir/merge-backends-duplicate-backend-weighted.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/merge-backends-duplicate-backend-weighted.listeners.yaml
index 4d0fe90c54..baf12bce7d 100644
--- a/internal/xds/translator/testdata/out/xds-ir/merge-backends-duplicate-backend-weighted.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/merge-backends-duplicate-backend-weighted.listeners.yaml
@@ -24,6 +24,7 @@
rds:
configSource:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
routeConfigName: envoy-gateway/gateway-1/http
serverHeaderTransformation: PASS_THROUGH
diff --git a/internal/xds/translator/testdata/out/xds-ir/merge-backends-gateway-traffic.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/merge-backends-gateway-traffic.listeners.yaml
index 4d0fe90c54..baf12bce7d 100644
--- a/internal/xds/translator/testdata/out/xds-ir/merge-backends-gateway-traffic.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/merge-backends-gateway-traffic.listeners.yaml
@@ -24,6 +24,7 @@
rds:
configSource:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
routeConfigName: envoy-gateway/gateway-1/http
serverHeaderTransformation: PASS_THROUGH
diff --git a/internal/xds/translator/testdata/out/xds-ir/merge-backends-healthcheck-hostname.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/merge-backends-healthcheck-hostname.listeners.yaml
index 1d204690e5..a1233a034b 100644
--- a/internal/xds/translator/testdata/out/xds-ir/merge-backends-healthcheck-hostname.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/merge-backends-healthcheck-hostname.listeners.yaml
@@ -22,6 +22,7 @@
rds:
configSource:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
routeConfigName: first-listener
serverHeaderTransformation: PASS_THROUGH
diff --git a/internal/xds/translator/testdata/out/xds-ir/merge-backends-mixed-merge-multi-filtered.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/merge-backends-mixed-merge-multi-filtered.listeners.yaml
index 4d0fe90c54..baf12bce7d 100644
--- a/internal/xds/translator/testdata/out/xds-ir/merge-backends-mixed-merge-multi-filtered.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/merge-backends-mixed-merge-multi-filtered.listeners.yaml
@@ -24,6 +24,7 @@
rds:
configSource:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
routeConfigName: envoy-gateway/gateway-1/http
serverHeaderTransformation: PASS_THROUGH
diff --git a/internal/xds/translator/testdata/out/xds-ir/merge-backends-mixed-merge-weighted.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/merge-backends-mixed-merge-weighted.listeners.yaml
index 4d0fe90c54..baf12bce7d 100644
--- a/internal/xds/translator/testdata/out/xds-ir/merge-backends-mixed-merge-weighted.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/merge-backends-mixed-merge-weighted.listeners.yaml
@@ -24,6 +24,7 @@
rds:
configSource:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
routeConfigName: envoy-gateway/gateway-1/http
serverHeaderTransformation: PASS_THROUGH
diff --git a/internal/xds/translator/testdata/out/xds-ir/merge-backends-shared-cluster-grpc.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/merge-backends-shared-cluster-grpc.listeners.yaml
index 1bf3c32ffb..616f42a930 100644
--- a/internal/xds/translator/testdata/out/xds-ir/merge-backends-shared-cluster-grpc.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/merge-backends-shared-cluster-grpc.listeners.yaml
@@ -32,6 +32,7 @@
rds:
configSource:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
routeConfigName: envoy-gateway/gateway-1/http
serverHeaderTransformation: PASS_THROUGH
diff --git a/internal/xds/translator/testdata/out/xds-ir/merge-backends-shared-cluster.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/merge-backends-shared-cluster.listeners.yaml
index 4d0fe90c54..baf12bce7d 100644
--- a/internal/xds/translator/testdata/out/xds-ir/merge-backends-shared-cluster.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/merge-backends-shared-cluster.listeners.yaml
@@ -24,6 +24,7 @@
rds:
configSource:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
routeConfigName: envoy-gateway/gateway-1/http
serverHeaderTransformation: PASS_THROUGH
diff --git a/internal/xds/translator/testdata/out/xds-ir/merge-backends-statname.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/merge-backends-statname.listeners.yaml
index 1d204690e5..a1233a034b 100644
--- a/internal/xds/translator/testdata/out/xds-ir/merge-backends-statname.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/merge-backends-statname.listeners.yaml
@@ -22,6 +22,7 @@
rds:
configSource:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
routeConfigName: first-listener
serverHeaderTransformation: PASS_THROUGH
diff --git a/internal/xds/translator/testdata/out/xds-ir/merge-backends-weight-per-route.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/merge-backends-weight-per-route.listeners.yaml
index 4d0fe90c54..baf12bce7d 100644
--- a/internal/xds/translator/testdata/out/xds-ir/merge-backends-weight-per-route.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/merge-backends-weight-per-route.listeners.yaml
@@ -24,6 +24,7 @@
rds:
configSource:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
routeConfigName: envoy-gateway/gateway-1/http
serverHeaderTransformation: PASS_THROUGH
diff --git a/internal/xds/translator/testdata/out/xds-ir/merge-backends-weighted-mixed.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/merge-backends-weighted-mixed.listeners.yaml
index a5bc288824..eba089bf26 100644
--- a/internal/xds/translator/testdata/out/xds-ir/merge-backends-weighted-mixed.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/merge-backends-weighted-mixed.listeners.yaml
@@ -24,6 +24,7 @@
rds:
configSource:
ads: {}
+ initialFetchTimeout: 0s
resourceApiVersion: V3
routeConfigName: first-listener
serverHeaderTransformation: PASS_THROUGH
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..170674b15c
--- /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.lua.strictValidation.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 bf46bbe6df..9c78c4d2be 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -2345,7 +2345,8 @@ _Appears in:_
| `backendTLS` | _[BackendTLSConfig](#backendtlsconfig)_ | false | | BackendTLS is the TLS configuration for the Envoy proxy to use when connecting to backends.
These settings are applied on backends for which TLS policies are specified. |
| `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 |
+| `luaValidation` | _[LuaValidation](#luavalidation)_ | false | | LuaValidation determines strictness of the Lua script validation for Lua EnvoyExtensionPolicies
Default: Strict
Deprecated: Use Lua.ValidationType instead. This field will be removed in a future release. |
+| `lua` | _[LuaValidationConfig](#luavalidationconfig)_ | false | | Lua configures how Lua scripts from EnvoyExtensionPolicy resources are
validated in the gateway controller. It selects the validation mode and, for the Strict
mode, defines the filesystem paths and environment variables the scripts are permitted to
access during validation. |
| `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. |
@@ -4272,6 +4273,7 @@ _Underlying type:_ _string_
_Appears in:_
- [EnvoyProxySpec](#envoyproxyspec)
+- [LuaValidationConfig](#luavalidationconfig)
| Value | Description |
| ----- | ----------- |
@@ -4280,6 +4282,22 @@ _Appears in:_
| `Disabled` | LuaValidationDisabled disables all Lua script validations.
WARNING: This mode does NOT offer any runtime or syntax validations, so no security measures are applied to validate Lua code safety.
Not recommended unless you completely trust all EnvoyExtensionPolicy resources.
|
+#### LuaValidationConfig
+
+
+
+LuaValidationConfig configures how Lua scripts from EnvoyExtensionPolicy resources are validated
+in the gateway controller.
+
+_Appears in:_
+- [EnvoyProxySpec](#envoyproxyspec)
+
+| Field | Type | Required | Default | Description |
+| --- | --- | --- | --- | --- |
+| `validationType` | _[LuaValidation](#luavalidation)_ | false | Strict | ValidationType determines the strictness of the Lua script validation.
Default: Strict |
+| `strictValidation` | _[StrictValidation](#strictvalidation)_ | false | | StrictValidation configures the security sandbox that the Strict validation mode executes Lua
scripts in, defining the filesystem paths and environment variables the scripts are permitted
to access during validation.
It has no effect for the InsecureSyntax or Disabled modes, which do not execute the security
sandbox. |
+
+
#### LuaValueType
_Underlying type:_ _string_
@@ -6213,6 +6231,24 @@ _Appears in:_
| `Range` | StatusCodeValueTypeRange defines the "Range" status code match type.
|
+#### StrictValidation
+
+
+
+StrictValidation 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:_
+- [LuaValidationConfig](#luavalidationconfig)
+
+| 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. The filesystem root ("/")
is likewise rejected, as it would allow access to the entire filesystem and defeat 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. |
+
+
#### StringMatch
diff --git a/test/cel-validation/envoyproxy_test.go b/test/cel-validation/envoyproxy_test.go
index 770dcc6494..8b812dd82a 100644
--- a/test/cel-validation/envoyproxy_test.go
+++ b/test/cel-validation/envoyproxy_test.go
@@ -2485,6 +2485,130 @@ func TestEnvoyProxyProvider(t *testing.T) {
},
wantErrors: []string{"If type is Remote, local field must not be set"},
},
+ {
+ desc: "lua-strict-valid",
+ mutate: func(envoy *egv1a1.EnvoyProxy) {
+ envoy.Spec = egv1a1.EnvoyProxySpec{
+ Lua: &egv1a1.LuaValidationConfig{
+ StrictValidation: &egv1a1.StrictValidation{
+ AllowedPaths: []string{"/tmp"},
+ AllowedEnvVars: []string{"LOG_LEVEL"},
+ },
+ },
+ }
+ },
+ wantErrors: []string{},
+ },
+ {
+ desc: "lua-empty-path-rejected",
+ mutate: func(envoy *egv1a1.EnvoyProxy) {
+ envoy.Spec = egv1a1.EnvoyProxySpec{
+ Lua: &egv1a1.LuaValidationConfig{
+ StrictValidation: &egv1a1.StrictValidation{
+ AllowedPaths: []string{""},
+ },
+ },
+ }
+ },
+ wantErrors: []string{"should be at least 1 chars long"},
+ },
+ {
+ desc: "lua-whitespace-path-rejected",
+ mutate: func(envoy *egv1a1.EnvoyProxy) {
+ envoy.Spec = egv1a1.EnvoyProxySpec{
+ Lua: &egv1a1.LuaValidationConfig{
+ StrictValidation: &egv1a1.StrictValidation{
+ AllowedPaths: []string{" "},
+ },
+ },
+ }
+ },
+ wantErrors: []string{"allowedPaths entries must not be blank or whitespace-only"},
+ },
+ {
+ desc: "lua-root-path-rejected",
+ mutate: func(envoy *egv1a1.EnvoyProxy) {
+ envoy.Spec = egv1a1.EnvoyProxySpec{
+ Lua: &egv1a1.LuaValidationConfig{
+ StrictValidation: &egv1a1.StrictValidation{
+ AllowedPaths: []string{"/"},
+ },
+ },
+ }
+ },
+ wantErrors: []string{"allowedPaths entries must not be the filesystem root"},
+ },
+ {
+ desc: "lua-multi-slash-root-path-rejected",
+ mutate: func(envoy *egv1a1.EnvoyProxy) {
+ envoy.Spec = egv1a1.EnvoyProxySpec{
+ Lua: &egv1a1.LuaValidationConfig{
+ StrictValidation: &egv1a1.StrictValidation{
+ AllowedPaths: []string{"//"},
+ },
+ },
+ }
+ },
+ wantErrors: []string{"allowedPaths entries must not be the filesystem root"},
+ },
+ {
+ desc: "lua-whitespace-envvar-rejected",
+ mutate: func(envoy *egv1a1.EnvoyProxy) {
+ envoy.Spec = egv1a1.EnvoyProxySpec{
+ Lua: &egv1a1.LuaValidationConfig{
+ StrictValidation: &egv1a1.StrictValidation{
+ AllowedEnvVars: []string{" "},
+ },
+ },
+ }
+ },
+ wantErrors: []string{"allowedEnvVars entries must not be blank or whitespace-only"},
+ },
+ {
+ desc: "lua-with-explicit-strict-type-allowed",
+ mutate: func(envoy *egv1a1.EnvoyProxy) {
+ envoy.Spec = egv1a1.EnvoyProxySpec{
+ Lua: &egv1a1.LuaValidationConfig{
+ ValidationType: new(egv1a1.LuaValidationStrict),
+ StrictValidation: &egv1a1.StrictValidation{AllowedPaths: []string{"/tmp"}},
+ },
+ }
+ },
+ wantErrors: []string{},
+ },
+ {
+ desc: "lua-with-unset-type-allowed",
+ mutate: func(envoy *egv1a1.EnvoyProxy) {
+ envoy.Spec = egv1a1.EnvoyProxySpec{
+ Lua: &egv1a1.LuaValidationConfig{
+ StrictValidation: &egv1a1.StrictValidation{AllowedPaths: []string{"/tmp"}},
+ },
+ }
+ },
+ wantErrors: []string{},
+ },
+ {
+ desc: "lua-strict-with-insecure-syntax-type-rejected",
+ mutate: func(envoy *egv1a1.EnvoyProxy) {
+ envoy.Spec = egv1a1.EnvoyProxySpec{
+ Lua: &egv1a1.LuaValidationConfig{
+ ValidationType: new(egv1a1.LuaValidationInsecureSyntax),
+ StrictValidation: &egv1a1.StrictValidation{AllowedPaths: []string{"/tmp"}},
+ },
+ }
+ },
+ wantErrors: []string{"strictValidation can only be set when validationType is Strict"},
+ },
+ {
+ desc: "luaValidation-and-lua-mutually-exclusive",
+ mutate: func(envoy *egv1a1.EnvoyProxy) {
+ envoy.Spec = egv1a1.EnvoyProxySpec{
+ LuaValidation: new(egv1a1.LuaValidationStrict),
+ Lua: &egv1a1.LuaValidationConfig{StrictValidation: &egv1a1.StrictValidation{AllowedPaths: []string{"/tmp"}}},
+ }
+ },
+ wantErrors: []string{"only one of luaValidation or lua may be set"},
+ },
{
desc: "mergeBackends present (empty) is valid",
mutate: func(envoy *egv1a1.EnvoyProxy) {
diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml
index f73490508a..38a8f5d80c 100644
--- a/test/helm/gateway-crds-helm/all.out.yaml
+++ b/test/helm/gateway-crds-helm/all.out.yaml
@@ -34600,10 +34600,82 @@ spec:
and the log level is the value. If unspecified, defaults to "default: warn".
type: object
type: object
+ lua:
+ description: |-
+ Lua configures how Lua scripts from EnvoyExtensionPolicy resources are
+ validated in the gateway controller. It selects the validation mode and, for the Strict
+ mode, defines the filesystem paths and environment variables the scripts are permitted to
+ access during validation.
+ properties:
+ strictValidation:
+ description: |-
+ StrictValidation configures the security sandbox that the Strict validation mode executes Lua
+ scripts in, defining the filesystem paths and environment variables the scripts are permitted
+ to access during validation.
+
+ It has no effect for the InsecureSyntax or Disabled 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. The filesystem root ("/")
+ is likewise rejected, as it would allow access to the entire filesystem and defeat 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() != '')
+ - message: allowedPaths entries must not be the filesystem
+ root
+ rule: self.all(p, !p.matches('^/+$'))
+ type: object
+ validationType:
+ default: Strict
+ description: |-
+ ValidationType determines the strictness of the Lua script validation.
+ Default: Strict
+ enum:
+ - Strict
+ - InsecureSyntax
+ - Disabled
+ type: string
+ type: object
+ x-kubernetes-validations:
+ - message: strictValidation can only be set when validationType is
+ Strict
+ rule: '!has(self.strictValidation) || !has(self.validationType)
+ || self.validationType == ''Strict'''
luaValidation:
description: |-
LuaValidation determines strictness of the Lua script validation for Lua EnvoyExtensionPolicies
Default: Strict
+
+ Deprecated: Use Lua.ValidationType instead. This field will be removed in a future release.
enum:
- Strict
- InsecureSyntax
@@ -52082,6 +52154,8 @@ spec:
type: object
type: object
x-kubernetes-validations:
+ - message: only one of luaValidation or lua may be set
+ rule: '!has(self.luaValidation) || !has(self.lua)'
- message: mergeGateways and mergeBackends cannot both be enabled
rule: '!(has(self.mergeGateways) && self.mergeGateways && has(self.mergeBackends))'
status:
diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml
index 265020d4e5..b93c88b0e6 100644
--- a/test/helm/gateway-crds-helm/e2e.out.yaml
+++ b/test/helm/gateway-crds-helm/e2e.out.yaml
@@ -10538,10 +10538,82 @@ spec:
and the log level is the value. If unspecified, defaults to "default: warn".
type: object
type: object
+ lua:
+ description: |-
+ Lua configures how Lua scripts from EnvoyExtensionPolicy resources are
+ validated in the gateway controller. It selects the validation mode and, for the Strict
+ mode, defines the filesystem paths and environment variables the scripts are permitted to
+ access during validation.
+ properties:
+ strictValidation:
+ description: |-
+ StrictValidation configures the security sandbox that the Strict validation mode executes Lua
+ scripts in, defining the filesystem paths and environment variables the scripts are permitted
+ to access during validation.
+
+ It has no effect for the InsecureSyntax or Disabled 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. The filesystem root ("/")
+ is likewise rejected, as it would allow access to the entire filesystem and defeat 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() != '')
+ - message: allowedPaths entries must not be the filesystem
+ root
+ rule: self.all(p, !p.matches('^/+$'))
+ type: object
+ validationType:
+ default: Strict
+ description: |-
+ ValidationType determines the strictness of the Lua script validation.
+ Default: Strict
+ enum:
+ - Strict
+ - InsecureSyntax
+ - Disabled
+ type: string
+ type: object
+ x-kubernetes-validations:
+ - message: strictValidation can only be set when validationType is
+ Strict
+ rule: '!has(self.strictValidation) || !has(self.validationType)
+ || self.validationType == ''Strict'''
luaValidation:
description: |-
LuaValidation determines strictness of the Lua script validation for Lua EnvoyExtensionPolicies
Default: Strict
+
+ Deprecated: Use Lua.ValidationType instead. This field will be removed in a future release.
enum:
- Strict
- InsecureSyntax
@@ -28020,6 +28092,8 @@ spec:
type: object
type: object
x-kubernetes-validations:
+ - message: only one of luaValidation or lua may be set
+ rule: '!has(self.luaValidation) || !has(self.lua)'
- message: mergeGateways and mergeBackends cannot both be enabled
rule: '!(has(self.mergeGateways) && self.mergeGateways && has(self.mergeBackends))'
status:
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 4a49f5c891..c1caf96d3f 100644
--- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
+++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
@@ -10538,10 +10538,82 @@ spec:
and the log level is the value. If unspecified, defaults to "default: warn".
type: object
type: object
+ lua:
+ description: |-
+ Lua configures how Lua scripts from EnvoyExtensionPolicy resources are
+ validated in the gateway controller. It selects the validation mode and, for the Strict
+ mode, defines the filesystem paths and environment variables the scripts are permitted to
+ access during validation.
+ properties:
+ strictValidation:
+ description: |-
+ StrictValidation configures the security sandbox that the Strict validation mode executes Lua
+ scripts in, defining the filesystem paths and environment variables the scripts are permitted
+ to access during validation.
+
+ It has no effect for the InsecureSyntax or Disabled 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. The filesystem root ("/")
+ is likewise rejected, as it would allow access to the entire filesystem and defeat 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() != '')
+ - message: allowedPaths entries must not be the filesystem
+ root
+ rule: self.all(p, !p.matches('^/+$'))
+ type: object
+ validationType:
+ default: Strict
+ description: |-
+ ValidationType determines the strictness of the Lua script validation.
+ Default: Strict
+ enum:
+ - Strict
+ - InsecureSyntax
+ - Disabled
+ type: string
+ type: object
+ x-kubernetes-validations:
+ - message: strictValidation can only be set when validationType is
+ Strict
+ rule: '!has(self.strictValidation) || !has(self.validationType)
+ || self.validationType == ''Strict'''
luaValidation:
description: |-
LuaValidation determines strictness of the Lua script validation for Lua EnvoyExtensionPolicies
Default: Strict
+
+ Deprecated: Use Lua.ValidationType instead. This field will be removed in a future release.
enum:
- Strict
- InsecureSyntax
@@ -28020,6 +28092,8 @@ spec:
type: object
type: object
x-kubernetes-validations:
+ - message: only one of luaValidation or lua may be set
+ rule: '!has(self.luaValidation) || !has(self.lua)'
- message: mergeGateways and mergeBackends cannot both be enabled
rule: '!(has(self.mergeGateways) && self.mergeGateways && has(self.mergeBackends))'
status: