RAM GOPINATHAN
RAM GOPINATHAN
  • September 12, 2026
  • 11 min read

Deploying FlightCTL on EKS: Edge Fleet Management with Keycloak OIDC

In this post walks through deploying flightctl on EKS with Keycloak fronting it for OIDC auth with proper RBAC groups, and a dedicated Network Load Balancer for the one piece of traffic an ALB genuinely can't handle — mutual TLS device enrollment.

All the Terraform, Helm values, resource manifests files, and Ansible playbooks referenced below live in the edge-fleet-management repo, including a bootc image build pipeline for the test devices themselves. This post covers the FlightCTL-on-EKS path end to end.

What is FlightCTL?

FlightCTL is an open source, CNCF-adjacent project (backed by Red Hat) for managing large fleets of edge devices and the workloads running on them through a declarative, Kubernetes-like API. Instead of SSH-ing into boxes one at a time, you describe the desired OS image and application state for a fleet, and FlightCTL rolls it out with staged, budgeted deployments and automatic rollback on failure.

Its core building blocks are: bootc/rpm-ostree for transactional, image-based OS updates; TPM-backed device enrollment and mutual TLS for identity; and Podman/MicroShift as the workload runtimes on the device side. The pieces that matter for this post are the API server, the Postgres-backed control plane, and the mTLS-secured agent endpoint devices enroll against — all of which run as a Helm-installed workload on the EKS cluster we're about to stand up.

A few useful links before diving in:

Provisioning the cluster

The cluster itself is unremarkable — a standard EKS cluster provisioned with Terraform. FlightCTL doesn't need anything exotic from the control plane; the interesting decisions all happen in how you expose it.

terraform init
terraform plan -out=tfplan
terraform apply tfplan

Once the cluster creation is complete we can run the AWS cli command below to update our local kubeconfig so we can use kubectl commands to authenticate with the EKS cluster

aws eks update-kubeconfig --region us-west-2 --name edge-fleet-mgmt

Keycloak as the OIDC provider

FlightCTL supports several auth backends, but OIDC is the one that maps cleanly onto "different people should see different things" — admins, operators, and read-only viewers. Keycloak runs in-cluster, behind its own ALB ingress:

kubectl create namespace keycloak
kubectl apply -f keycloak.yaml -n keycloak

Create the ALB ingress for keycloak by running the command below

* Be sure to update the Certificate ARN to match your environment before running the command.*

kubectl apply -f deploy/keycloak/ingress.yaml

Create the flightctl realm

Navigate to the Keycloak URL in a browser and log in with the default admin credentials (admin:admin).

Select Manage realms from the left navigation bar, then click Create realm:

Create realm button in the Keycloak admin console

In the Create realm dialog, set Realm name to flightctl and click Create:

Setting the realm name to flightctl

Create groups and test users

Create three groups whose names FlightCTL's RBAC depends on matching exactly:

  • flightctl-admin
  • flightctl-operator
  • flightctl-viewer
Creating the flightctl-admin, flightctl-operator, and flightctl-viewer groups

Create three test users and assign each to one of the groups above, so RBAC can be verified end to end once FlightCTL is installed:

Test users assigned to each RBAC group

Create the UI client application

Go to Clients in the left navigation and click Create client to define the flightctl-ui application:

Creating the flightctl-ui client application

Under Capability config, turn off Client Authentication:

Turning off client authentication for the UI client

The Helm chart's values file has no field for a client secret, so a confidential client can't log in once FlightCTL is installed — the ConfigMap patch applied later on is what actually supplies that secret.

Open the Client scopes tab and select the client's dedicated scope to configure token mappers:

Opening the dedicated client scope for mappers

Add the standard claims using Add mapper → From predefined mappers:

Adding predefined claim mappers

Add a groups claim using Add mapper → By configuration:

Adding a mapper by configuration for the groups claim

Select Group Membership as the mapper type:

Selecting the Group Membership mapper

Turn off Full group path so only the group name — not the full hierarchical path — flows into the token's groups claim. This is exactly what FlightCTL's dynamic role assignment matches against:

Turning off full group path in the Group Membership mapper

With the realm, groups, users, and UI client in place, we're ready to install FlightCTL with Helm and point its OIDC config at this client.

Installing FlightCTL with Helm

FlightCTL ships an official Helm chart. Point the auth block at the Keycloak realm, set the expose method to none since we're building ingress ourselves, and set the database's fsGroup if you're using the built-in Postgres rather than something like RDS:

auth:
  type: "oidc"
  insecureSkipTlsVerify: true
  oidc:
    clientId: "<client id from keycloak>"
    scopes: ["openid", "profile", "email", "roles", "offline_access"]
    issuer: "<keycloak endpoint>/realms/flightctl"
    externalOidcAuthority: "<keycloak endpoint>/realms/flightctl"
    organizationAssignment:
      type: "static"
      organizationName: "default"
    usernameClaim: ["upn"]
    roleAssignment:
      type: "dynamic"
      claimPath: ["groups"]

exposeServicesMethod: "none"
helm install edge-manager --namespace flightctl --create-namespace \
  oci://quay.io/flightctl/charts/flightctl -f ./deploy/flightctl/values.yaml

kubectl get pods -n flightctl

Ingress, and the client-secret workaround

With the ALB ingress resources applied for the UI and API, there's one gap the Helm values file can't close: it has no field for the OIDC client secret. Without it, login fails even though everything else is configured correctly. The fix is a plain ConfigMap patch applied after install:

kubectl apply -f ./deploy/flightctl/ingress.yml

kubectl describe ing edgemanager-alb-ingress-api -n flightctl
kubectl describe ing edgemanager-alb-ingress-ui -n flightctl
kubectl create configmap -n flightctl flightctl-api-config \
  --from-file=./deploy/flightctl/config.yaml -o yaml --dry-run=client | kubectl apply -f -

Setting up the CLI auth provider

The UI client's redirect URIs point at the ALB hostname, which doesn't work for a CLI login flow that redirects to localhost. Rather than juggling redirect URIs on one client, define a second Keycloak client for the CLI and register it with FlightCTL as its own auth provider:

Create this client the same way as flightctl-ui above — Clients → Create client — but give it its own Client ID (e.g. flightctl-cli), turn off Client Authentication, and set Valid redirect URIs to http://localhost:* so the CLI's local callback can complete:

Register this client as FlightCTL's CLI auth provider:

apiVersion: v1beta1
kind: AuthProvider
metadata:
  name: flightctl-cli-auth-provider
spec:
  providerType: oidc
  displayName: "FlightCTL CLI"
  issuer: "<keycloak endpoint>/realms/flightctl"
  clientId: "<client id from keycloak>"
  enabled: true
  scopes: ["openid", "profile", "email", "roles", "offline_access"]
  usernameClaim: ["upn"]
  roleAssignment:
    type: dynamic
    claimPath: ["groups"]
    separator: ':'

The first login below still runs against the default provider — the flightctl-ui client — before the CLI provider exists, and that client's redirect URIs point at the ALB hostname, not localhost. Temporarily add http://localhost:* to flightctl-ui's redirect URIs so this bootstrap login can complete:

Updating the flightctl-ui client's redirect URIs to allow the CLI bootstrap login
flightctl login <flightctl api server endpoint> --web
flightctl apply -f ./deploy/flightctl/cli-auth.yaml
flightctl login <flightctl api server endpoint> --web --provider flightctl-cli-auth-provider

Once the CLI provider works, revert flightctl-ui's redirect URIs back to just the ALB hostname.

Exposing the agent API for mTLS

This is the part that trips people up. Device agents authenticate to the agent-facing API on flightctl-api-agent:7443 using mutual TLS — each device presents a client certificate from FlightCTL's own enrollment CA, and flightctl-api terminates TLS and validates it directly. An ALB can't sit in front of that: it terminates TLS before the backend ever sees the connection, so its "mTLS passthrough" mode only forwards the client cert as HTTP headers rather than preserving the actual handshake. The agent API needs a Network Load Balancer doing raw TCP passthrough straight to the pod instead:

kubectl apply -f deploy/flightctl/agent-api-nlb-service.yml

kubectl get svc flightctl-api-agent-nlb -n flightctl
dig <agent api endpoint>

Generate enrollment certificate

Generate an enrollment certificate that gets injected into each device so its FlightCTL agent can enroll:

flightctl certificate request --signer=enrollment --expiration=365d --output=embedded > config.yaml

Verifying RBAC in the FlightCTL UI

With Keycloak and FlightCTL wired together, we can confirm the RBAC groups actually take effect by logging in as the two test users created earlier.

Login as Ram, since Ram is an admin he should have full permissions to add/edit/delete resources:

FlightCTL UI logged in as Ram, an admin with full permissions

Login as Kelly, since Kelly has only viewer rights she should be able to view resources but not make any changes:

FlightCTL UI logged in as Kelly, a viewer with read-only permissions

In a follow up post we will look at enrolling devices and setting up fleet.

EksEdgeKubernetesAWSDeviceSecurityIaC