Compare commits

..
3 Commits
Author SHA1 Message Date
jdesouza be3401cc9d Merge branch 'master' into js/gateway 2026-08-28 15:21:42 -03:00
jdesouza 8f61e0c68a Merge branch 'master' into js/gateway 2026-08-28 15:11:37 -03:00
jdesouza b860eb798b Add support to Gateway checks in Polaris 2026-08-28 10:39:40 -03:00
39 changed files with 1176 additions and 5 deletions
+17
View File
@@ -24,6 +24,15 @@ key | default | description
`hostNetworkSet` | `warning` | Fails when `hostNetwork` attribute is configured.
`hostPortSet` | `warning` | Fails when `hostPort` attribute is configured.
`tlsSettingsMissing` | `warning` | Fails when an Ingress lacks TLS settings.
`gatewayTLSMissing` | `warning` | Fails when an HTTPS, GRPC, or terminating TLS Gateway listener lacks certificate references.
`gatewayAllowedRoutesAll` | `warning` | Fails when a Gateway listener allows Routes from every namespace.
`gatewayInsecureFrontendValidation` | `warning` | Fails when Gateway frontend client certificate validation allows insecure fallback.
`gatewayCrossNamespaceCertificateRef` | `warning` | Fails when a Gateway references a certificate in another namespace without a matching ReferenceGrant. Cluster audits only.
`httpRouteWildcardOrEmptyHost` | `warning` | Fails when an HTTPRoute omits hostnames or uses a wildcard hostname.
`httpRouteInsecureListener` | `warning` | Fails when an HTTPRoute serves application traffic over HTTP without a full HTTPS redirect. Cluster audits only.
`httpRouteCrossNamespaceBackendRef` | `warning` | Fails when an HTTPRoute references a backend in another namespace without a matching ReferenceGrant. Cluster audits only.
`httpRouteBackendTLSMissing` | `warning` | Fails when an HTTPRoute TLS backend lacks a BackendTLSPolicy or kgateway BackendConfigPolicy. Cluster audits only.
`kgatewayBackendTLSVerificationDisabled` | `warning` | Fails when a kgateway BackendConfigPolicy disables TLS certificate verification.
`sensitiveContainerEnvVar` | `danger` | Fails when the container sets potentially sensitive environment variables.
`sensitiveConfigmapContent` | `danger` | Fails when potentially sensitive content is detected in the ConfigMap keys or values.
`missingNetworkPolicy` | `warning`
@@ -40,6 +49,14 @@ key | default | description
Securing workloads in Kubernetes is an important part of overall cluster security. The overall goal should be to ensure that containers are running with as minimal privileges as possible. This includes avoiding privilege escalation, not running containers with a root user, not giving excessive access to the host network, and using read only file systems wherever possible.
### Gateway API
Gateway API separates listeners, routes, and backend TLS policy across different resources. Polaris checks standard `Gateway` and `HTTPRoute` resources for listener TLS, namespace isolation, host specificity, HTTPS redirects, cross-namespace authorization, and backend TLS. These checks work with conformant implementations such as kgateway.
`httpRouteBackendTLSMissing` also recognizes kgateway's `Backend` and `BackendConfigPolicy` resources. It identifies TLS backends from ports 443 and 8443, Service port names and `appProtocol`, and kgateway static Backend ports. `kgatewayBackendTLSVerificationDisabled` checks the kgateway-specific `insecureSkipVerify` setting. Authentication, authorization, and rate-limiting requirements are organization-specific and should be implemented as custom checks.
Checks marked "Cluster audits only" need related resources that are not available when Polaris evaluates a single admission request. They pass without a resource provider rather than rejecting an object without enough context.
A pod running with the `hostNetwork` attribute enabled will have access to the loopback device, services listening on localhost, and could be used to snoop on network activity of other pods on the same node. There are certain examples where setting `hostNetwork` to true is required, such as deploying a networking plugin like Flannel.
Setting the `hostPort` attribute on a container will ensure that it is accessible on that specific port on each node it is deployed to. Unfortunately when this is specified, it limits where a pod can actually be scheduled in a cluster.
+9
View File
@@ -57,6 +57,15 @@ var (
"sensitiveContainerEnvVar",
// Other checks
"tlsSettingsMissing",
"gatewayTLSMissing",
"gatewayAllowedRoutesAll",
"gatewayInsecureFrontendValidation",
"gatewayCrossNamespaceCertificateRef",
"httpRouteWildcardOrEmptyHost",
"httpRouteInsecureListener",
"httpRouteCrossNamespaceBackendRef",
"httpRouteBackendTLSMissing",
"kgatewayBackendTLSVerificationDisabled",
"pdbDisruptionsIsZero",
"metadataAndInstanceMismatched",
"missingPodDisruptionBudget",
@@ -0,0 +1,25 @@
successMessage: Gateway listeners restrict route attachment by namespace
failureMessage: Gateway listeners should not allow routes from all namespaces
category: Security
target: gateway.networking.k8s.io/Gateway
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
type: object
properties:
spec:
type: object
properties:
listeners:
type: array
items:
type: object
properties:
allowedRoutes:
type: object
properties:
namespaces:
type: object
properties:
from:
not:
const: All
@@ -0,0 +1,6 @@
successMessage: Gateway cross-namespace certificate references are authorized
failureMessage: Gateway cross-namespace certificate references should have a matching ReferenceGrant
category: Security
target: gateway.networking.k8s.io/Gateway
relatedKinds:
- gateway.networking.k8s.io/ReferenceGrant
@@ -0,0 +1,40 @@
successMessage: Gateway frontend client certificate validation fails closed
failureMessage: Gateway frontend client certificate validation should not allow insecure fallback
category: Security
target: gateway.networking.k8s.io/Gateway
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
type: object
properties:
spec:
type: object
properties:
tls:
type: object
properties:
frontend:
type: object
properties:
default:
type: object
properties:
validation:
type: object
properties:
mode:
not:
const: AllowInsecureFallback
perPort:
type: array
items:
type: object
properties:
tls:
type: object
properties:
validation:
type: object
properties:
mode:
not:
const: AllowInsecureFallback
+41
View File
@@ -0,0 +1,41 @@
successMessage: Gateway TLS listeners have certificates configured
failureMessage: Gateway HTTPS, GRPC, and terminating TLS listeners should configure certificateRefs
category: Security
target: gateway.networking.k8s.io/Gateway
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
type: object
properties:
spec:
type: object
properties:
listeners:
type: array
items:
type: object
if:
anyOf:
- required: [protocol]
properties:
protocol:
enum: [HTTPS, GRPC]
- required: [protocol]
properties:
protocol:
const: TLS
tls:
type: object
properties:
mode:
not:
const: Passthrough
then:
required: [tls]
properties:
tls:
type: object
required: [certificateRefs]
properties:
certificateRefs:
type: array
minItems: 1
@@ -0,0 +1,9 @@
successMessage: HTTPRoute TLS backends have TLS origination configured
failureMessage: HTTPRoute backends on TLS ports should have a BackendTLSPolicy or kgateway BackendConfigPolicy
category: Security
target: gateway.networking.k8s.io/HTTPRoute
relatedKinds:
- Service
- gateway.networking.k8s.io/BackendTLSPolicy
- gateway.kgateway.dev/Backend
- gateway.kgateway.dev/BackendConfigPolicy
@@ -0,0 +1,6 @@
successMessage: HTTPRoute cross-namespace backend references are authorized
failureMessage: HTTPRoute cross-namespace backend references should have a matching ReferenceGrant
category: Security
target: gateway.networking.k8s.io/HTTPRoute
relatedKinds:
- gateway.networking.k8s.io/ReferenceGrant
@@ -0,0 +1,6 @@
successMessage: HTTPRoute uses secure listeners or redirects HTTP to HTTPS
failureMessage: HTTPRoute should not serve application traffic over an HTTP listener
category: Security
target: gateway.networking.k8s.io/HTTPRoute
relatedKinds:
- gateway.networking.k8s.io/Gateway
@@ -0,0 +1,19 @@
successMessage: HTTPRoute uses explicit hostnames
failureMessage: HTTPRoute should use explicit hostnames instead of matching every hostname
category: Security
target: gateway.networking.k8s.io/HTTPRoute
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
type: object
required: [spec]
properties:
spec:
type: object
required: [hostnames]
properties:
hostnames:
type: array
minItems: 1
items:
type: string
pattern: '^[^*]+$'
@@ -0,0 +1,17 @@
successMessage: kgateway backend TLS certificate verification is enabled
failureMessage: kgateway BackendConfigPolicy should not disable TLS certificate verification
category: Security
target: gateway.kgateway.dev/BackendConfigPolicy
schema:
'$schema': https://json-schema.org/draft/2019-09/schema
type: object
properties:
spec:
type: object
properties:
tls:
type: object
properties:
insecureSkipVerify:
not:
const: true
+9
View File
@@ -38,6 +38,15 @@ checks:
hostNetworkSet: danger
hostPortSet: warning
tlsSettingsMissing: warning
gatewayTLSMissing: warning
gatewayAllowedRoutesAll: warning
gatewayInsecureFrontendValidation: warning
gatewayCrossNamespaceCertificateRef: warning
httpRouteWildcardOrEmptyHost: warning
httpRouteInsecureListener: warning
httpRouteCrossNamespaceBackendRef: warning
httpRouteBackendTLSMissing: warning
kgatewayBackendTLSVerificationDisabled: warning
sensitiveContainerEnvVar: danger
sensitiveConfigmapContent: danger
clusterrolePodExecAttach: danger
+9
View File
@@ -38,6 +38,15 @@ checks:
hostNetworkSet: danger
hostPortSet: warning
tlsSettingsMissing: warning
gatewayTLSMissing: warning
gatewayAllowedRoutesAll: warning
gatewayInsecureFrontendValidation: warning
gatewayCrossNamespaceCertificateRef: warning
httpRouteWildcardOrEmptyHost: warning
httpRouteInsecureListener: warning
httpRouteCrossNamespaceBackendRef: warning
httpRouteBackendTLSMissing: warning
kgatewayBackendTLSVerificationDisabled: warning
sensitiveContainerEnvVar: danger
sensitiveConfigmapContent: danger
clusterrolePodExecAttach: danger
+1
View File
@@ -80,6 +80,7 @@ type SchemaCheck struct {
AdditionalSchemas map[string]map[string]any `yaml:"additionalSchemas" json:"additionalSchemas"`
AdditionalSchemaStrings map[string]string `yaml:"additionalSchemaStrings" json:"additionalSchemaStrings"`
AdditionalValidators map[string]jsonschema.Schema `yaml:"-" json:"-"`
RelatedKinds []TargetKind `yaml:"relatedKinds" json:"relatedKinds"`
Mutations []Mutation `yaml:"mutations" json:"mutations"`
}
+15 -5
View File
@@ -325,16 +325,22 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
}
restMapper := restmapper.NewDiscoveryRESTMapper(resources)
allChecks := []conf.SchemaCheck{}
for _, check := range c.CustomChecks {
allChecks = append(allChecks, check)
for checkID, check := range c.CustomChecks {
severity, enabled := c.Checks[checkID]
if enabled && severity.IsActionable() {
allChecks = append(allChecks, check)
}
}
for _, check := range conf.BuiltInChecks {
allChecks = append(allChecks, check)
for checkID, check := range conf.BuiltInChecks {
severity, enabled := c.Checks[checkID]
if enabled && severity.IsActionable() {
allChecks = append(allChecks, check)
}
}
var additionalKinds []conf.TargetKind
for _, check := range allChecks {
neededKinds := []conf.TargetKind{check.Target}
neededKinds := append([]conf.TargetKind{check.Target}, check.RelatedKinds...)
for key := range check.AdditionalSchemas {
neededKinds = append(neededKinds, conf.TargetKind(key))
}
@@ -353,6 +359,10 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac
groupKind := parseGroupKind(maybeTransformKindIntoGroupKind(string(kind)))
mapping, err := restMapper.RESTMapping(groupKind)
if err != nil {
if meta.IsNoMatchError(err) {
logrus.Infof("Skipping unavailable Kind %s", kind)
continue
}
logrus.Warnf("error retrieving mapping of Kind %s because of error: %v", kind, err)
return nil, err
}
+42
View File
@@ -176,3 +176,45 @@ func TestGetResourceFromAPI(t *testing.T) {
})
}
}
func TestAdditionalKindLoading(t *testing.T) {
ingress := test.MockIngress()
k8s, dynamicInterface := test.SetupTestAPI(append(test.GetMockControllers("test"), &ingress)...)
enabled := conf.Configuration{
Checks: map[string]conf.Severity{
"customIngress": conf.SeverityWarning,
},
CustomChecks: map[string]conf.SchemaCheck{
"customIngress": {Target: "networking.k8s.io/Ingress"},
},
}
resources, err := CreateResourceProviderFromAPI(context.Background(), k8s, "test", dynamicInterface, enabled)
if assert.NoError(t, err) {
assert.Len(t, resources.Resources["networking.k8s.io/Ingress"], 1)
}
ignored := enabled
ignored.Checks = map[string]conf.Severity{
"customIngress": conf.SeverityIgnore,
}
resources, err = CreateResourceProviderFromAPI(context.Background(), k8s, "test", dynamicInterface, ignored)
if assert.NoError(t, err) {
assert.Empty(t, resources.Resources["networking.k8s.io/Ingress"])
}
}
func TestUnavailableAdditionalKindDoesNotFailAudit(t *testing.T) {
k8s, dynamicInterface := test.SetupTestAPI(test.GetMockControllers("test")...)
config := conf.Configuration{
Checks: map[string]conf.Severity{
"optionalCRD": conf.SeverityWarning,
},
CustomChecks: map[string]conf.SchemaCheck{
"optionalCRD": {Target: "example.com/OptionalResource"},
},
}
_, err := CreateResourceProviderFromAPI(context.Background(), k8s, "test", dynamicInterface, config)
assert.NoError(t, err)
}
+485
View File
@@ -0,0 +1,485 @@
package validator
import (
"encoding/json"
"fmt"
"strings"
"github.com/fairwindsops/polaris/pkg/kube"
"github.com/qri-io/jsonschema"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
)
const (
gatewayAPIGroup = "gateway.networking.k8s.io"
kgatewayAPIGroup = "gateway.kgateway.dev"
)
type objectReference struct {
group string
kind string
name string
namespace string
sectionName string
port int64
}
func init() {
registerCustomChecks("httpRouteInsecureListener", httpRouteInsecureListener)
registerCustomChecks("gatewayCrossNamespaceCertificateRef", gatewayCrossNamespaceCertificateRef)
registerCustomChecks("httpRouteCrossNamespaceBackendRef", httpRouteCrossNamespaceBackendRef)
registerCustomChecks("httpRouteBackendTLSMissing", httpRouteBackendTLSMissing)
}
func httpRouteInsecureListener(test schemaTestCase) (bool, []jsonschema.KeyError, error) {
if isFullHTTPSRedirect(test.Resource.Resource.Object) || test.ResourceProvider == nil {
return true, nil, nil
}
routeNamespace := test.Resource.ObjectMeta.GetNamespace()
for _, parent := range referencesAt(test.Resource.Resource.Object, "spec", "parentRefs") {
parent = withDefaults(parent, gatewayAPIGroup, "Gateway", routeNamespace)
if parent.group != gatewayAPIGroup || parent.kind != "Gateway" {
continue
}
gateway := findResource(test.ResourceProvider.Resources[gatewayAPIGroup+"/Gateway"], parent.namespace, parent.name)
if gateway == nil {
continue
}
listeners := nestedSlice(gateway.Resource.Object, "spec", "listeners")
for _, rawListener := range listeners {
listener, ok := rawListener.(map[string]any)
if !ok || (parent.sectionName != "" && stringValue(listener["name"]) != parent.sectionName) {
continue
}
if stringValue(listener["protocol"]) == "HTTP" && listenerAcceptsHTTPRoute(listener, parent.namespace, test.Resource, test.ResourceProvider) {
return gatewayFailure("spec.parentRefs", fmt.Sprintf("HTTPRoute references HTTP listener %q on Gateway %s/%s without a full HTTPS redirect", stringValue(listener["name"]), parent.namespace, parent.name))
}
}
}
return true, nil, nil
}
func gatewayCrossNamespaceCertificateRef(test schemaTestCase) (bool, []jsonschema.KeyError, error) {
if test.ResourceProvider == nil {
return true, nil, nil
}
sourceNamespace := test.Resource.ObjectMeta.GetNamespace()
listeners := nestedSlice(test.Resource.Resource.Object, "spec", "listeners")
for _, rawListener := range listeners {
listener, ok := rawListener.(map[string]any)
if !ok {
continue
}
for _, ref := range referencesAt(listener, "tls", "certificateRefs") {
ref = withDefaults(ref, "", "Secret", sourceNamespace)
if ref.namespace != sourceNamespace && !hasReferenceGrant(test.ResourceProvider, sourceNamespace, "Gateway", ref) {
return gatewayFailure("spec.listeners.tls.certificateRefs", fmt.Sprintf("Gateway %s/%s references %s %s/%s without a matching ReferenceGrant", sourceNamespace, test.Resource.ObjectMeta.GetName(), ref.kind, ref.namespace, ref.name))
}
}
}
return true, nil, nil
}
func httpRouteCrossNamespaceBackendRef(test schemaTestCase) (bool, []jsonschema.KeyError, error) {
if test.ResourceProvider == nil {
return true, nil, nil
}
sourceNamespace := test.Resource.ObjectMeta.GetNamespace()
for _, ref := range httpRouteBackendRefs(test.Resource.Resource.Object) {
ref = withDefaults(ref, "", "Service", sourceNamespace)
if ref.namespace != sourceNamespace && !hasReferenceGrant(test.ResourceProvider, sourceNamespace, "HTTPRoute", ref) {
return gatewayFailure("spec.rules.backendRefs", fmt.Sprintf("HTTPRoute %s/%s references %s %s/%s without a matching ReferenceGrant", sourceNamespace, test.Resource.ObjectMeta.GetName(), ref.kind, ref.namespace, ref.name))
}
}
return true, nil, nil
}
func httpRouteBackendTLSMissing(test schemaTestCase) (bool, []jsonschema.KeyError, error) {
if test.ResourceProvider == nil {
return true, nil, nil
}
routeNamespace := test.Resource.ObjectMeta.GetNamespace()
for _, ref := range httpRouteBackendRefs(test.Resource.Resource.Object) {
ref = withDefaults(ref, "", "Service", routeNamespace)
if !backendUsesTLS(test.ResourceProvider, ref) {
continue
}
if hasBackendTLSPolicy(test.ResourceProvider, ref) || hasKgatewayBackendTLSPolicy(test.ResourceProvider, ref) {
continue
}
return gatewayFailure("spec.rules.backendRefs", fmt.Sprintf("HTTPRoute backend %s %s/%s appears to use TLS but has no BackendTLSPolicy or kgateway BackendConfigPolicy", ref.kind, ref.namespace, ref.name))
}
return true, nil, nil
}
func isFullHTTPSRedirect(object map[string]any) bool {
rules := nestedSlice(object, "spec", "rules")
if len(rules) == 0 {
return false
}
for _, rawRule := range rules {
rule, ok := rawRule.(map[string]any)
if !ok || len(referencesAt(rule, "backendRefs")) > 0 || !ruleMatchesAllTraffic(rule) || !hasHTTPSRedirect(rule) {
return false
}
}
return true
}
func ruleMatchesAllTraffic(rule map[string]any) bool {
matches := nestedSlice(rule, "matches")
if len(matches) == 0 {
return true
}
for _, rawMatch := range matches {
match, ok := rawMatch.(map[string]any)
if !ok || len(match) != 1 {
continue
}
path, ok := match["path"].(map[string]any)
if ok && (stringValue(path["type"]) == "" || stringValue(path["type"]) == "PathPrefix") && stringValue(path["value"]) == "/" {
return true
}
}
return false
}
func hasHTTPSRedirect(rule map[string]any) bool {
filters := nestedSlice(rule, "filters")
for _, rawFilter := range filters {
filter, ok := rawFilter.(map[string]any)
if !ok || stringValue(filter["type"]) != "RequestRedirect" {
continue
}
redirect, ok := filter["requestRedirect"].(map[string]any)
if ok && strings.EqualFold(stringValue(redirect["scheme"]), "https") {
return true
}
}
return false
}
func listenerAcceptsHTTPRoute(listener map[string]any, gatewayNamespace string, route kube.GenericResource, provider *kube.ResourceProvider) bool {
if !listenerHostnameIntersectsRoute(listener, route.Resource.Object) {
return false
}
allowedRoutes, ok := listener["allowedRoutes"].(map[string]any)
if !ok {
return route.ObjectMeta.GetNamespace() == gatewayNamespace
}
if kinds := nestedSlice(allowedRoutes, "kinds"); len(kinds) > 0 {
allowsHTTPRoute := false
for _, rawKind := range kinds {
kind, ok := rawKind.(map[string]any)
if ok && withDefaultString(stringValue(kind["group"]), gatewayAPIGroup) == gatewayAPIGroup && stringValue(kind["kind"]) == "HTTPRoute" {
allowsHTTPRoute = true
break
}
}
if !allowsHTTPRoute {
return false
}
}
namespaces, ok := allowedRoutes["namespaces"].(map[string]any)
if !ok || stringValue(namespaces["from"]) == "" || stringValue(namespaces["from"]) == "Same" {
return route.ObjectMeta.GetNamespace() == gatewayNamespace
}
if stringValue(namespaces["from"]) == "All" {
return true
}
if stringValue(namespaces["from"]) != "Selector" {
return false
}
selectorMap, ok := namespaces["selector"].(map[string]any)
if !ok {
return false
}
selector := &metav1.LabelSelector{}
selectorJSON, err := json.Marshal(selectorMap)
if err != nil {
return false
}
if err := json.Unmarshal(selectorJSON, selector); err != nil {
return false
}
compiled, err := metav1.LabelSelectorAsSelector(selector)
if err != nil {
return false
}
for _, namespace := range provider.Namespaces {
if namespace.Name == route.ObjectMeta.GetNamespace() {
return compiled.Matches(labels.Set(namespace.Labels))
}
}
return false
}
func listenerHostnameIntersectsRoute(listener, route map[string]any) bool {
listenerHostname := stringValue(listener["hostname"])
routeHostnames := nestedSlice(route, "spec", "hostnames")
if len(routeHostnames) == 0 || listenerHostname == "" {
return true
}
for _, routeHostname := range routeHostnames {
if hostnamesIntersect(listenerHostname, stringValue(routeHostname)) {
return true
}
}
return false
}
func hostnamesIntersect(left, right string) bool {
if left == "" || right == "" || left == "*" || right == "*" || strings.EqualFold(left, right) {
return true
}
leftSuffix, leftWildcard := strings.CutPrefix(strings.ToLower(left), "*.")
rightSuffix, rightWildcard := strings.CutPrefix(strings.ToLower(right), "*.")
switch {
case leftWildcard && rightWildcard:
return leftSuffix == rightSuffix || strings.HasSuffix(leftSuffix, "."+rightSuffix) || strings.HasSuffix(rightSuffix, "."+leftSuffix)
case leftWildcard:
return strings.HasSuffix(strings.ToLower(right), "."+leftSuffix)
case rightWildcard:
return strings.HasSuffix(strings.ToLower(left), "."+rightSuffix)
default:
return false
}
}
func httpRouteBackendRefs(object map[string]any) []objectReference {
var refs []objectReference
rules := nestedSlice(object, "spec", "rules")
for _, rawRule := range rules {
rule, ok := rawRule.(map[string]any)
if !ok {
continue
}
refs = append(refs, referencesAt(rule, "backendRefs")...)
filters := nestedSlice(rule, "filters")
for _, rawFilter := range filters {
filter, ok := rawFilter.(map[string]any)
if !ok || stringValue(filter["type"]) != "RequestMirror" {
continue
}
mirror, ok := filter["requestMirror"].(map[string]any)
if !ok {
continue
}
if backend, ok := mirror["backendRef"].(map[string]any); ok {
refs = append(refs, referenceFromMap(backend))
}
}
}
return refs
}
func referencesAt(object map[string]any, fields ...string) []objectReference {
items := nestedSlice(object, fields...)
if len(items) == 0 {
return nil
}
refs := make([]objectReference, 0, len(items))
for _, item := range items {
if ref, ok := item.(map[string]any); ok {
refs = append(refs, referenceFromMap(ref))
}
}
return refs
}
func referenceFromMap(ref map[string]any) objectReference {
return objectReference{
group: stringValue(ref["group"]),
kind: stringValue(ref["kind"]),
name: stringValue(ref["name"]),
namespace: stringValue(ref["namespace"]),
sectionName: stringValue(ref["sectionName"]),
port: int64Value(ref["port"]),
}
}
func withDefaults(ref objectReference, group, kind, namespace string) objectReference {
if ref.group == "" {
ref.group = group
}
if ref.kind == "" {
ref.kind = kind
}
if ref.namespace == "" {
ref.namespace = namespace
}
return ref
}
func withDefaultString(value, defaultValue string) string {
if value == "" {
return defaultValue
}
return value
}
func hasReferenceGrant(provider *kube.ResourceProvider, sourceNamespace, sourceKind string, target objectReference) bool {
for _, grant := range provider.Resources[gatewayAPIGroup+"/ReferenceGrant"] {
if grant.ObjectMeta.GetNamespace() != target.namespace {
continue
}
fromMatches := false
for _, from := range referencesAt(grant.Resource.Object, "spec", "from") {
if from.group == gatewayAPIGroup && from.kind == sourceKind && from.namespace == sourceNamespace {
fromMatches = true
break
}
}
if !fromMatches {
continue
}
for _, to := range referencesAt(grant.Resource.Object, "spec", "to") {
if to.group == target.group && to.kind == target.kind && (to.name == "" || to.name == target.name) {
return true
}
}
}
return false
}
func backendUsesTLS(provider *kube.ResourceProvider, ref objectReference) bool {
// ponytail: infer TLS from conventional ports and backend metadata; replace
// this with controller status or an implementation graph when Polaris has one.
if ref.port == 443 || ref.port == 8443 {
return true
}
groupKind := ref.kind
if ref.group != "" {
groupKind = ref.group + "/" + ref.kind
}
backend := findResource(provider.Resources[groupKind], ref.namespace, ref.name)
if backend == nil {
return false
}
if ref.group == "" && ref.kind == "Service" {
ports := nestedSlice(backend.Resource.Object, "spec", "ports")
for _, rawPort := range ports {
port, ok := rawPort.(map[string]any)
if !ok || (ref.port != 0 && int64Value(port["port"]) != ref.port) {
continue
}
name := strings.ToLower(stringValue(port["name"]))
appProtocol := strings.ToLower(stringValue(port["appProtocol"]))
if name == "https" || strings.HasPrefix(name, "https-") || appProtocol == "https" || strings.HasSuffix(appProtocol, "/https") {
return true
}
}
}
if ref.group == kgatewayAPIGroup && ref.kind == "Backend" {
hosts := nestedSlice(backend.Resource.Object, "spec", "static", "hosts")
for _, rawHost := range hosts {
host, ok := rawHost.(map[string]any)
if ok && (int64Value(host["port"]) == 443 || int64Value(host["port"]) == 8443) {
return true
}
}
}
return false
}
func hasBackendTLSPolicy(provider *kube.ResourceProvider, ref objectReference) bool {
if ref.group != "" || ref.kind != "Service" {
return false
}
for _, policy := range provider.Resources[gatewayAPIGroup+"/BackendTLSPolicy"] {
if policy.ObjectMeta.GetNamespace() == ref.namespace && policyTargets(policy, ref) {
return true
}
}
return false
}
func hasKgatewayBackendTLSPolicy(provider *kube.ResourceProvider, ref objectReference) bool {
for _, policy := range provider.Resources[kgatewayAPIGroup+"/BackendConfigPolicy"] {
if policy.ObjectMeta.GetNamespace() != ref.namespace {
continue
}
if _, found := nestedValue(policy.Resource.Object, "spec", "tls"); found && policyTargets(policy, ref) {
return true
}
}
return false
}
func policyTargets(policy kube.GenericResource, target objectReference) bool {
for _, ref := range referencesAt(policy.Resource.Object, "spec", "targetRefs") {
ref = withDefaults(ref, "", "Service", policy.ObjectMeta.GetNamespace())
if ref.group == target.group && ref.kind == target.kind && ref.name == target.name {
return true
}
}
return false
}
func findResource(resources []kube.GenericResource, namespace, name string) *kube.GenericResource {
for i := range resources {
if resources[i].ObjectMeta.GetNamespace() == namespace && resources[i].ObjectMeta.GetName() == name {
return &resources[i]
}
}
return nil
}
func stringValue(value any) string {
valueString, _ := value.(string)
return valueString
}
func int64Value(value any) int64 {
switch number := value.(type) {
case int:
return int64(number)
case int32:
return int64(number)
case int64:
return number
case float64:
return int64(number)
default:
return 0
}
}
func nestedSlice(object map[string]any, fields ...string) []any {
value, found := nestedValue(object, fields...)
if !found {
return nil
}
items, _ := value.([]any)
return items
}
func nestedValue(object map[string]any, fields ...string) (any, bool) {
var current any = object
for _, field := range fields {
currentMap, ok := current.(map[string]any)
if !ok {
return nil, false
}
current, ok = currentMap[field]
if !ok {
return nil, false
}
}
return current, true
}
func gatewayFailure(path, message string) (bool, []jsonschema.KeyError, error) {
return false, []jsonschema.KeyError{{
PropertyPath: path,
Message: message,
}}, nil
}
@@ -0,0 +1,13 @@
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: unrestricted
spec:
gatewayClassName: kgateway
listeners:
- name: https
protocol: HTTPS
port: 443
allowedRoutes:
namespaces:
from: All
@@ -0,0 +1,16 @@
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: restricted
spec:
gatewayClassName: kgateway
listeners:
- name: https
protocol: HTTPS
port: 443
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
gateway-access: "true"
@@ -0,0 +1,15 @@
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: public
namespace: infra
spec:
gatewayClassName: kgateway
listeners:
- name: https
protocol: HTTPS
port: 443
tls:
certificateRefs:
- name: wildcard
namespace: certificates
@@ -0,0 +1,30 @@
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: public
namespace: infra
spec:
gatewayClassName: kgateway
listeners:
- name: https
protocol: HTTPS
port: 443
tls:
certificateRefs:
- name: wildcard
namespace: certificates
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-infra-gateway
namespace: certificates
spec:
from:
- group: gateway.networking.k8s.io
kind: Gateway
namespace: infra
to:
- group: ""
kind: Secret
name: wildcard
@@ -0,0 +1,15 @@
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: fail-open
spec:
gatewayClassName: kgateway
tls:
frontend:
default:
validation:
mode: AllowInsecureFallback
listeners:
- name: https
protocol: HTTPS
port: 443
@@ -0,0 +1,15 @@
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: fail-closed
spec:
gatewayClassName: kgateway
tls:
frontend:
default:
validation:
mode: AllowValidOnly
listeners:
- name: https
protocol: HTTPS
port: 443
@@ -0,0 +1,10 @@
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: missing-tls
spec:
gatewayClassName: kgateway
listeners:
- name: https
protocol: HTTPS
port: 443
@@ -0,0 +1,13 @@
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: insecure
spec:
gatewayClassName: kgateway
listeners:
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs: []
@@ -0,0 +1,22 @@
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: secure
spec:
gatewayClassName: kgateway
listeners:
- name: http
protocol: HTTP
port: 80
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- name: example-tls
- name: passthrough
protocol: TLS
port: 8443
tls:
mode: Passthrough
@@ -0,0 +1,12 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: plaintext-to-tls-backend
namespace: app
spec:
hostnames:
- app.example.com
rules:
- backendRefs:
- name: api
port: 443
@@ -0,0 +1,39 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: secure-kgateway-backend
namespace: app
spec:
hostnames:
- app.example.com
rules:
- backendRefs:
- group: gateway.kgateway.dev
kind: Backend
name: external-api
---
apiVersion: gateway.kgateway.dev/v1alpha1
kind: Backend
metadata:
name: external-api
namespace: app
spec:
type: Static
static:
hosts:
- host: api.example.com
port: 443
---
apiVersion: gateway.kgateway.dev/v1alpha1
kind: BackendConfigPolicy
metadata:
name: external-api-tls
namespace: app
spec:
targetRefs:
- group: gateway.kgateway.dev
kind: Backend
name: external-api
tls:
sni: api.example.com
wellKnownCACertificates: System
@@ -0,0 +1,26 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: secure-backend
namespace: app
spec:
hostnames:
- app.example.com
rules:
- backendRefs:
- name: api
port: 443
---
apiVersion: gateway.networking.k8s.io/v1
kind: BackendTLSPolicy
metadata:
name: api-tls
namespace: app
spec:
targetRefs:
- group: ""
kind: Service
name: api
validation:
hostname: api.app.svc.cluster.local
wellKnownCACertificates: System
@@ -0,0 +1,13 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shared-api
namespace: app
spec:
hostnames:
- app.example.com
rules:
- backendRefs:
- name: api
namespace: shared
port: 8080
@@ -0,0 +1,28 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shared-api
namespace: app
spec:
hostnames:
- app.example.com
rules:
- backendRefs:
- name: api
namespace: shared
port: 8080
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
name: allow-app-route
namespace: shared
spec:
from:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: app
to:
- group: ""
kind: Service
name: api
@@ -0,0 +1,31 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: plaintext
namespace: app
spec:
parentRefs:
- name: public
namespace: infra
sectionName: http
hostnames:
- app.example.com
rules:
- backendRefs:
- name: app
port: 8080
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: public
namespace: infra
spec:
gatewayClassName: kgateway
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All
@@ -0,0 +1,31 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: secure-host-only
namespace: app
spec:
parentRefs:
- name: public
hostnames:
- app.example.com
rules:
- backendRefs:
- name: app
port: 8080
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: public
namespace: app
spec:
gatewayClassName: kgateway
listeners:
- name: unrelated-http
hostname: other.example.com
protocol: HTTP
port: 80
- name: app-https
hostname: app.example.com
protocol: HTTPS
port: 443
@@ -0,0 +1,37 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: redirect
namespace: app
spec:
parentRefs:
- name: public
namespace: infra
sectionName: http
hostnames:
- app.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /
filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: public
namespace: infra
spec:
gatewayClassName: kgateway
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All
@@ -0,0 +1,9 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: every-host
spec:
rules:
- backendRefs:
- name: api
port: 8080
@@ -0,0 +1,11 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: wildcard-host
spec:
hostnames:
- "*.example.com"
rules:
- backendRefs:
- name: api
port: 8080
@@ -0,0 +1,11 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: explicit-host
spec:
hostnames:
- api.example.com
rules:
- backendRefs:
- name: api
port: 8080
@@ -0,0 +1,11 @@
apiVersion: gateway.kgateway.dev/v1alpha1
kind: BackendConfigPolicy
metadata:
name: unverified
spec:
targetRefs:
- group: ""
kind: Service
name: api
tls:
insecureSkipVerify: true
@@ -0,0 +1,12 @@
apiVersion: gateway.kgateway.dev/v1alpha1
kind: BackendConfigPolicy
metadata:
name: verified
spec:
targetRefs:
- group: ""
kind: Service
name: api
tls:
sni: api.example.com
wellKnownCACertificates: System