Introduction
AWS recently announced the general availability of Amazon Web Services (AWS) Load Balancer Controller support for Kubernetes Gateway API.
Furthermore, AWS added custom resource definitions (CRDs) to implement ELB listeners and target rules via a declarative approach.
Let's put these new possibilities into practice.
Since exposing a public endpoint to an EKS cluster is considered unsafe, I am going to add OIDC authorization to my EKS cluster and add JWT token validation with the ALB controller.
In this setup, I will place all security responsibilities for validation and authentication on the AWS side (at no extra cost) and only run Envoy to proxy the Kubernetes API at Layer 4.
ALB installation
Here are the full instructions, and here is my deployment via FluxCD.
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: eks
namespace: kube-system
spec:
url: https://aws.github.io/eks-charts
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: aws-load-balancer-controller
namespace: kube-system
spec:
chart:
spec:
chart: aws-load-balancer-controller
version: "3.x"
sourceRef:
kind: HelmRepository
name: eks
interval: 30m
releaseName: aws-load-balancer-controller
values:
replicaCount: 1
serviceAccount:
create: true
name: aws-load-balancer-controller
clusterName: demo-infra-eks-cluster
defaultTargetType: ip
defaultLoadBalancerScheme: internal
ingressClassConfig:
default: true
ALB controller AWS api access granted via pod identity.
Here is a terraform/tofu example to provision it.
Gateway class
Default Gateway settings. A full working example can be found here.
apiVersion: gateway.k8s.aws/v1
kind: TargetGroupConfiguration
metadata:
name: tg-config-default
namespace: kube-system
spec:
defaultConfiguration:
targetType: ip
---
# GatewayClass LBC references it
apiVersion: gateway.k8s.aws/v1
kind: LoadBalancerConfiguration
metadata:
name: lb-internet-default
namespace: kube-system
spec:
mergingMode: prefer-gateway
scheme: internet-facing
ipAddressType: dualstack
defaultTargetGroupConfiguration:
name: tg-config-default
---
# GatewayClass points to the LBC
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: default
spec:
controllerName: gateway.k8s.aws/alb
parametersRef:
group: gateway.k8s.aws
kind: LoadBalancerConfiguration
name: lb-internet-default
namespace: kube-system
Keycloak
I am going to use Keycloak as an OIDC identity provider. Cloud-IAM lets you have a managed Keycloak instance for 1 realm and 100 users for free. It is ideal for a testing playground.
Keycloak configuration is beyond the scope of this article for the sake of simplicity. I promise to publish how to do it next!
EKS OIDC provider
Instructions from AWS can be found here.
Terraform/tofu example here.
resource "aws_eks_identity_provider_config" "eks_oidc_provider" {
cluster_name = var.eks_cluster_name
oidc {
identity_provider_config_name = "demo-infra-keycloak"
client_id = "demo-infra-kube-api"
issuer_url = "https://<keycloak_url>/auth/realms/<keycloak_realm>"
groups_claim = "roles"
groups_prefix = "oidc:"
username_claim = "username"
username_prefix = "oidc-"
}
}
Envoy
A full working example can be found here.
apiVersion: apps/v1
kind: Deployment
metadata:
name: envoy-kube-proxy
labels:
app: envoy-kube-proxy
spec:
replicas: 1
selector:
matchLabels:
app: envoy-kube-proxy
template:
metadata:
labels:
app: envoy-kube-proxy
spec:
containers:
- name: envoy
image: envoyproxy/envoy:v1.37-latest
ports:
- containerPort: 8443
name: proxy
- containerPort: 9901
name: admin
volumeMounts:
- name: config-volume
mountPath: /etc/envoy
volumes:
- name: config-volume
configMap:
name: envoy-kube-proxy-config
---
apiVersion: v1
kind: Service
metadata:
name: envoy-kube-proxy
spec:
selector:
app: envoy-kube-proxy
ports:
- name: proxy
port: 8443
targetPort: 8443
- name: admin
port: 9901
targetPort: 9901
envoy-kube-proxy-config
admin:
address:
socket_address:
address: 0.0.0.0
port_value: 9901
allow_paths:
- exact: /ready
- prefix: /stats
static_resources:
listeners:
- name: k8s_tls_passthrough_listener
address:
socket_address:
address: 0.0.0.0
port_value: 8443
filter_chains:
- filters:
- name: envoy.filters.network.tcp_proxy
typed_config:
"@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy"
stat_prefix: k8s_passthrough_proxy
cluster: kubernetes_api_passthrough_cluster
idle_timeout: 0s
clusters:
- name: kubernetes_api_passthrough_cluster
type: STRICT_DNS
lb_policy: ROUND_ROBIN
# Envoy will not attempt to negotiate TLS; it forwards raw TCP data.
load_assignment:
cluster_name: kubernetes_api_passthrough_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: kubernetes.default.svc.cluster.local
port_value: 443
Gateway
A fully working example can be found here.
apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
name: envoy-kube-proxy-gateway
spec:
gatewayClassName: default
listeners:
- name: https
protocol: HTTPS
port: 8443
allowedRoutes:
namespaces:
from: Same
---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: envoy-kube-proxy-httproute
labels:
app: envoy-kube-proxy
spec:
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: envoy-kube-proxy-gateway
sectionName: https
hostnames:
- "envoy-kube-proxy.example.com"
rules:
- backendRefs:
- name: envoy-kube-proxy
port: 8443
filters:
- extensionRef:
group: gateway.k8s.aws
kind: ListenerRuleConfiguration
name: envoy-kube-proxy-jwt-validation
type: ExtensionRef
TargetGroupConfiguration is needed to set up the backend as an HTTPS target rather than an HTTP target.
apiVersion: gateway.k8s.aws/v1
kind: TargetGroupConfiguration
metadata:
name: envoy-kube-proxy-tgc
spec:
targetReference:
name: envoy-kube-proxy
defaultConfiguration:
protocol: HTTPS
healthCheckConfig:
healthCheckPath: /ready
healthCheckPort: '9901'
healthCheckProtocol: HTTP
apiVersion: gateway.k8s.aws/v1
kind: ListenerRuleConfiguration
metadata:
name: envoy-kube-proxy-jwt-validation
spec:
actions:
- type: "jwt-validation"
jwtValidationConfig:
jwksEndpoint: "https://<keycloak_url>/auth/realms/<keycloak_realm>/protocol/openid-connect/certs"
issuer: "https://<keycloak_url>/auth/realms/<keycloak_realm>"
additionalClaims:
- name: "aud"
format: "single-string"
values: ["demo-infra-kube-api"]
Validation
An OIDC token can be obtained with Postman.
Set up the request in Postman.
Auth Type --> OAuth2
Auth URL --> from the Keycloak .well-known/openid-configuration
Access Token URL --> from the Keycloak .well-known/openid-configuration
Client ID --> from the client configuration
Client Secret --> from the client configuration
and click on "Get new access token"
Adding Postman's callback URL to the client "Valid redirect URIs" should not be forgotten.
This token can be used with kubectl like this.
kubectl auth whoami --token <oidc_token>
If everything were set correctly, kubectl returns the username and group with OIDC prefixes similar to this,
Username oidc-keyuser1
Groups [oidc:kube-admin system:authenticated]
and the ALB endpoint should not return a 401 response code.
Conclusion
Kubelogin plugin can be used now to provide OIDC access tokens for kubectl.
Moreover, it is now possible to authenticate a GitHub Actions token using Keycloak as an identity broker middleware with no static credentials.
And I have an interesting idea to implement SSO and JWT authentication on the ALB next time. Follow me!
Feel free to leave a comment with your feedback.
Top comments (0)