EKS (Elastic Kubernetes Service)
Protocol: REST-JSON
Endpoint: http://localhost:4566/ (path-routed via JAX-RS)
EKS uses a standard REST API with JSON bodies — not the JSON 1.1 (X-Amz-Target) or Query protocol.
Supported Operations
| Operation | Description |
|---|---|
CreateCluster |
Create a new EKS cluster |
DescribeCluster |
Describe a cluster by name |
ListClusters |
List all cluster names |
DeleteCluster |
Delete a cluster |
CreateAccessEntry |
Create STANDARD or EC2_LINUX access-entry metadata |
DescribeAccessEntry |
Describe an access entry by IAM principal ARN |
ListAccessEntries |
List principal ARNs with pagination |
DeleteAccessEntry |
Delete access-entry metadata |
CreateNodegroup |
Create node group metadata for a cluster |
DescribeNodegroup |
Describe a node group by cluster and name |
ListNodegroups |
List node group names for a cluster |
DeleteNodegroup |
Delete a node group |
CreateFargateProfile |
Create Fargate profile metadata for a cluster |
DescribeFargateProfile |
Describe a Fargate profile by cluster and name |
ListFargateProfiles |
List Fargate profile names for a cluster |
DeleteFargateProfile |
Delete a Fargate profile |
TagResource |
Add tags to a cluster |
UntagResource |
Remove tags from a cluster |
ListTagsForResource |
List tags on a cluster |
Access-entry management
CreateCluster accepts accessConfig.authenticationMode (CONFIG_MAP, API_AND_CONFIG_MAP, or API) and bootstrapClusterCreatorAdminPermissions. The API default is CONFIG_MAP; access-entry operations require an ACTIVE cluster created with API or API_AND_CONFIG_MAP.
The four access-entry management operations support STANDARD (the default) and EC2_LINUX. STANDARD accepts existing IAM users or roles, including principals in another account. EC2_LINUX requires a role in the cluster account and generates system:node:{{EC2PrivateDNSName}}; custom usernames and Kubernetes groups are not accepted for node entries. Tags supplied at creation are preserved. Repeating a create with the same client token and normalized parameters returns the existing entry. Listing accepts maxResults from 1 to 100 and cluster-specific nextToken values.
Access entries use EKS storage and retain the IAM principal's stable ID internally. Cluster deletion removes its entries, and a cluster recreated with the same name does not inherit previous entries or pagination tokens.
EC2 Linux worker authentication
IMDS instance-profile credentials authenticate through a matching EC2_LINUX entry as
system:node:<private-DNS-name> with system:bootstrappers and system:nodes groups.
The webhook verifies the signed token, cluster account/region/incarnation, stored role ID,
running EC2 instance, and attached instance profile. Missing/deleted entries, recreated roles,
revoked sessions and terminated instances are rejected without falling back to administrator access.
The existing k3s webhook cache can retain a successful authentication for up to 30 seconds.
New cluster webhook configurations carry the target account, region and creation timestamp in the URL path. Kubernetes client-go replaces server URL query parameters when sending TokenReview requests, so worker scope must not depend on those parameters. Recreate older local clusters before using worker authentication; a legacy unscoped webhook rejects instance credentials. Obtain fresh IMDS credentials after upgrading so the session includes the stable role ID.
Authentication scope
Non-worker IAM users and ordinary STS sessions retain the existing cluster-admin compatibility behavior. STANDARD entries, access policies, aws-auth ConfigMap, the cluster-creator bootstrap flag and automatic managed-node entries are not enforced by this change. Updating authentication mode, UpdateAccessEntry, access-policy association, and entry tag updates remain unimplemented. Native AL2023 images, bootstrap RBAC/CSR approval, CNI and worker networking are separate requirements for registration and Ready.
aws --endpoint-url http://localhost:4566 eks create-cluster \
--name local-nodes --role-arn arn:aws:iam::000000000000:role/cluster \
--resources-vpc-config '{}' \
--access-config authenticationMode=API,bootstrapClusterCreatorAdminPermissions=false
# The IAM role must already exist.
aws --endpoint-url http://localhost:4566 eks create-access-entry \
--cluster-name local-nodes --principal-arn arn:aws:iam::000000000000:role/worker \
--type EC2_LINUX
Modes
Mock mode (mock: true)
Cluster metadata is stored in-process. No Docker containers are started. The cluster transitions directly to ACTIVE on creation. Use this in CI or whenever you only need the EKS API shape, not a real Kubernetes API server.
Real mode (mock: false, default)
Floci starts a k3s (rancher/k3s) container for each cluster. The k3s API server is exposed on a host port from the configured range (6500–6599). Once /readyz responds, the cluster transitions to ACTIVE and the CA certificate is extracted from the kubeconfig.
By default describe-cluster returns a host-reachable endpoint (https://localhost:<hostPort>); the k3s server certificate includes a localhost SAN, so it verifies against the CA in cluster.certificateAuthority.data. Set endpoint-mode: network to return the container DNS name (https://floci-eks-<name>:6443) instead — reachable from other containers on the Docker network (the pre-#1118 behaviour). In network mode the endpoint falls back to the host-reachable form when Floci runs natively, since there is no container DNS name a host client could use.
Connecting with kubectl (native AWS workflow)
The standard AWS flow works end to end:
aws eks update-kubeconfig wires aws eks get-token into the kubeconfig as an exec credential. The bearer token contains a SigV4-presigned STS GetCallerIdentity request. Floci validates its signature and 60-second presign expiry, then verifies the signed x-k8s-aws-id header against the cluster-specific /_floci/eks/clusters/<cluster-name>/token-webhook endpoint before resolving the caller identity. Instance-profile sessions require an EC2_LINUX access entry as described above; non-worker callers retain the system:masters mapping (bound to cluster-admin). No aws-iam-authenticator is required.
Create an IAM access key before using EKS authentication. The public local-development pairs test/test and floci/floci are deliberately rejected because the webhook grants cluster-admin access.
Temporary IAM credentials must include their AWS_SESSION_TOKEN when signing the token.
This webhook is enabled by default (iam-auth-webhook: true). Set it to false to start k3s without it (in which case aws eks get-token tokens are rejected with 401).
Webhook reachability & networking
The k3s API server must be able to reach Floci's webhook URL. When Floci runs natively, k3s containers reach it via host.docker.internal; when Floci runs in a container (floci start), Floci and the k3s containers share a Docker network. The k3s network is taken from FLOCI_SERVICES_EKS_DOCKER_NETWORK if set, otherwise the global FLOCI_SERVICES_DOCKER_NETWORK, otherwise the network Floci is itself attached to (auto-detected) — so no EKS-specific network configuration is required in the standard compose setup.
The webhook kubeconfig is copied into the k3s container via the Docker API (not bind-mounted), so the token-webhook works the same in native and Docker-in-Docker modes with no host-path / host-persistent-path configuration.
Docker socket required
Real mode starts privileged Docker containers. Mount the Docker socket and set the Docker network so containers can reach each other.
services:
floci:
image: floci/floci:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
ports:
- "4566:4566"
environment:
FLOCI_SERVICES_EKS_DOCKER_NETWORK: my_project_default
No port mapping needed for k3s ports
k3s containers bind their API server port (6500–6599) directly on the host via Docker — no ports: entry is required in docker-compose.yml. See Ports Reference for the full explanation.
Clusters survive a restart
With a persistent storage mode (the default), clusters recorded in
eks-clusters.json are re-latched to their k3s containers when Floci starts:
- A surviving container (for example after a Docker Desktop / daemon reboot) is adopted and started in place, keeping its published API server port and data volume — deployments come back as they were.
- A missing container is recreated. Its named k3s data volume (
floci-eks-<name>; for a non-default account,floci-eks-<account>.<name>) is reused if it survived; volumes follow the global prune policy (FLOCI_STORAGE_PRUNE_VOLUMES_ON_DELETE, defaultfalse), so they are retained when the container is stopped or the cluster deleted, except inmemorystorage mode. - A non-default-account cluster created before account-qualified naming keeps its historical
floci-eks-<name>container and volume: restoration adopts the surviving container when itsio.floci.accountlabel matches the owning account, so pre-upgrade workloads are not orphaned.
A restored cluster reports CREATING until its API server answers again, then returns to
ACTIVE with a freshly extracted certificate authority. If the container cannot be brought
back (for example Docker is unavailable), the cluster is marked FAILED instead of appearing
ACTIVE while unreachable.
Configuration
| Variable | Default | Description |
|---|---|---|
FLOCI_SERVICES_EKS_ENABLED |
true |
Enable the EKS service |
FLOCI_SERVICES_EKS_MOCK |
false |
Metadata-only mode (no Docker) |
FLOCI_SERVICES_EKS_DEFAULT_IMAGE |
rancher/k3s:latest |
k3s Docker image |
FLOCI_SERVICES_EKS_API_SERVER_BASE_PORT |
6500 |
First port in the k3s API server range |
FLOCI_SERVICES_EKS_API_SERVER_MAX_PORT |
6599 |
Last port in the k3s API server range |
FLOCI_SERVICES_EKS_DATA_PATH |
./data/eks |
Host bind-mount root for cluster data |
FLOCI_SERVICES_EKS_DOCKER_NETWORK |
(unset) | Docker network for k3s containers (falls back to the global FLOCI_SERVICES_DOCKER_NETWORK, then Floci's own network) |
FLOCI_SERVICES_EKS_KEEP_RUNNING_ON_SHUTDOWN |
false |
Leave k3s containers running after Floci stops |
FLOCI_SERVICES_EKS_ENDPOINT_MODE |
host |
describe-cluster endpoint: host (localhost:<hostPort>) or network (container DNS) |
FLOCI_SERVICES_EKS_IAM_AUTH_WEBHOOK |
true |
Wire a token-auth webhook into k3s so aws eks get-token works |
FLOCI_SERVICES_EKS_ECR_REGISTRY_MIRROR |
true |
Inject a containerd registries.yaml so pods can pull images pushed to Floci ECR |
FLOCI_SERVICES_EKS_IRSA_SIGNING_KEY |
true |
Pass the cluster OIDC signing key to k3s so in-cluster projected service account tokens can assume IAM roles via Floci STS |
FLOCI_SERVICES_EKS_IMDS |
false |
Enable link-local IMDS (169.254.169.254) proxy in cluster containers |
Pulling images from Floci ECR
Images pushed to the Floci ECR registry use localhost-based repository URIs
(for example 000000000000.dkr.ecr.us-east-1.localhost:4566/my-repo:tag). Inside a k3s
cluster that hostname would resolve to the k3s container itself, and containerd insists
on HTTPS for anything it doesn't recognize as loopback — so, out of the box, k3s cannot
pull from the registry even though docker push from the host works.
Floci solves this at cluster creation: each new k3s container gets a generated
/etc/rancher/k3s/registries.yaml that mirrors every repository hostname the emulator
can mint: the default account across the full region catalog, plus the path-style
localhost:<port> form used by FLOCI_SERVICES_ECR_URI_STYLE=path, to Floci's
in-network data plane. The same image
reference then works for the host-side push and the in-cluster pull, with no retagging
and no manual containerd configuration:
aws ecr create-repository --repository-name my-repo
docker build -t 000000000000.dkr.ecr.us-east-1.localhost:4566/my-repo:v1 .
docker push 000000000000.dkr.ecr.us-east-1.localhost:4566/my-repo:v1
aws eks create-cluster --name demo ...
helm install my-app ./chart \
--set image.repository=000000000000.dkr.ecr.us-east-1.localhost:4566/my-repo \
--set image.tag=v1
Requirements and limits:
- The k3s container must reach Floci's Docker-network address. Set the global
FLOCI_SERVICES_DOCKER_NETWORKas in the standarddocker-compose.ymlor the EKS service network variable. - Only Floci-mintable hostnames are mirrored; public registries (docker.io, ghcr.io, …) are never touched.
- Repository URIs using a non-default
registryId(account) are not covered. - The mirror set is snapshotted when the cluster is created. Clusters created before this feature can be fixed manually because the k3s container filesystem survives a restart:
docker cp registries.yaml floci-eks-<cluster>:/etc/rancher/k3s/registries.yaml
docker restart floci-eks-<cluster>
Mock mode (CI / tests)
Use FLOCI_SERVICES_EKS_MOCK=true when you only need the API shape:
# docker-compose.yml — CI / test environment
services:
floci:
image: floci/floci:latest
environment:
FLOCI_SERVICES_EKS_MOCK: "true"
Instance Metadata Service (IMDS)
When enabled (FLOCI_SERVICES_EKS_IMDS=true), each k3s cluster container exposes the AWS Instance Metadata Service on the link-local address 169.254.169.254:80. Inside the container, Floci adds 169.254.169.254/32 to the loopback interface (lo) and runs a lightweight socat TCP relay forwarding metadata requests to Floci's IMDS server.
Both IMDSv1 and IMDSv2 (PUT /latest/api/token) are supported. The cluster container is registered as a synthesized EC2 instance node (type m5.large, image ami-eks-k3s) associated with the cluster's IAM role:
# IMDSv2: obtain token
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
-H "x-aws-ec2-metadata-token-ttl-seconds: 21600")
# Read node instance ID
curl -s -H "x-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/instance-id
Reachability and network namespaces
The link-local proxy attaches to the loopback interface of the k3s container network namespace (the node network namespace).
- Reachable from node network namespace: Workloads configured with
hostNetwork: trueshare the node network namespace and can reach169.254.169.254:80. This allows local testing of host-network security policies, intrusion-detection rules, and credential exfiltration defenses. - Not reachable from ordinary pods: Pods running in separate pod network namespaces cannot reach
169.254.169.254through this loopback alias because link-local addresses are non-routable across network namespaces. Pod-CIDR DNAT routing is not implemented.
Configuration
IMDS proxy initialization is disabled by default (floci.services.eks.imds=false). Set FLOCI_SERVICES_EKS_IMDS=true to enable the proxy setup inside the k3s container.
A failure to configure the proxy (for example on custom minimal images lacking network utilities) logs a warning and allows cluster startup to continue.
Hop limits
EC2 metadata options store HttpPutResponseHopLimit, but the userspace TCP proxy relay terminates the incoming connection and opens a new connection to Floci, regenerating the IP packet TTL. Hop limits are recorded on the instance metadata options model but are not enforced by the userspace proxy.
IRSA (IAM Roles for Service Accounts)
Every cluster gets an OIDC identity provider, so the full IRSA flow (trust policy, service-account token, sts:AssumeRoleWithWebIdentity) works end to end without mocked or hardcoded tokens.
CreateCluster generates an RSA-2048 signing keypair and an AWS-shaped issuer URL, returned by DescribeCluster:
{
"cluster": {
"name": "my-cluster",
"identity": { "oidc": { "issuer": "https://oidc.eks.us-east-1.amazonaws.com/id/3F8A…" } }
}
}
The issuer URL is a faithful string for building trust policies, but it is not fetched: Floci's STS resolves the signing key in-process. The private key is held in storage separate from the cluster model and is never returned by any API.
Service-account tokens
Floci supports two ways to obtain IRSA service account tokens:
1. In-cluster projected tokens (real k3s mode)
In real mode (mock: false), Floci starts a k3s container for the cluster. When IRSA signing key support is enabled (FLOCI_SERVICES_EKS_IRSA_SIGNING_KEY=true, default true), Floci exports the cluster RSA keypair to /etc/rancher/k3s/sa-signing-key.pem and /etc/rancher/k3s/sa-public-key.pem with restrictive filesystem permissions (0600) before k3s starts.
The k3s API server is configured with:
- --kube-apiserver-arg=service-account-signing-key-file=/etc/rancher/k3s/sa-signing-key.pem
- --kube-apiserver-arg=service-account-key-file=/etc/rancher/k3s/sa-public-key.pem
- --kube-apiserver-arg=service-account-issuer=<cluster-oidc-issuer>
- --kube-apiserver-arg=service-account-issuer=https://kubernetes.default.svc.cluster.local
- --kube-apiserver-arg=api-audiences=https://kubernetes.default.svc.cluster.local,sts.amazonaws.com
This enables in-cluster pods to project service account tokens directly using Kubernetes standard projected volumes:
apiVersion: v1
kind: Pod
metadata:
name: irsa-workload
namespace: default
spec:
serviceAccountName: my-service-account
containers:
- name: workload
image: my-workload:latest
env:
- name: AWS_ROLE_ARN
value: arn:aws:iam::000000000000:role/my-irsa-role
- name: AWS_WEB_IDENTITY_TOKEN_FILE
value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
volumeMounts:
- mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount
name: aws-iam-token
readOnly: true
volumes:
- name: aws-iam-token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 86400
audience: sts.amazonaws.com
Tokens projected with audience sts.amazonaws.com are signed by k3s using the cluster key and advertise the cluster issuer URL. When the AWS SDK inside the container calls sts:AssumeRoleWithWebIdentity, Floci verifies the signature against the cluster public key and checks the trust policy conditions. In-cluster components that do not specify an audience receive tokens with both the in-cluster audience and STS audience, allowing normal Kubernetes operation without disruption.
2. Minting a service-account token via HTTP (mock mode & out-of-cluster testing)
For local development harnesses running outside Kubernetes or when using mock mode (FLOCI_SERVICES_EKS_MOCK=true), tokens can be requested directly from Floci via HTTP. Signing happens server-side, so the private key never leaves Floci:
curl -sX POST http://localhost:4566/_floci/eks/clusters/my-cluster/oidc-token \
-H 'Content-Type: application/json' \
-d '{"namespace":"my-namespace","serviceAccount":"my-service-account"}'
{
"token": "eyJhbGciOiJSUzI1NiIs…",
"issuer": "https://oidc.eks.us-east-1.amazonaws.com/id/3F8A…",
"subject": "system:serviceaccount:my-namespace:my-service-account",
"audience": "sts.amazonaws.com"
}
audience (default sts.amazonaws.com) and expirySeconds (default 24h, max 7d) are optional. This is Floci plumbing under _floci/…, not an AWS API.
Trust policy
Note that the OIDC provider ARN and the condition keys use the issuer with the scheme stripped oidc.eks.<region>.amazonaws.com/id/<id>, which is how the AWS console, eksctl, and Terraform all render it.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::000000000000:oidc-provider/<oidcProvider>"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"<oidcProvider>:sub": "system:serviceaccount:<namespace>:<serviceAccount>",
"<oidcProvider>:aud": "sts.amazonaws.com"
}
}
}]
}
What STS validates
When sts:AssumeRoleWithWebIdentity receives a token whose iss names an issuer Floci hosts, it enforces all of:
- the RS256 signature, against that cluster's public key
issmatches the cluster's issuer exactlyaudcontainssts.amazonaws.comexp/nbf, with 60s of clock-skew tolerance- the role's trust policy:
Principal.Federatedand theConditionblock, comparingoidc:sub/oidc:audwith exact, case-sensitive equality
The response then carries the token's real claims in SubjectFromWebIdentityToken, Provider, and Audience. Failures return InvalidIdentityToken (400) for a bad token, ExpiredTokenException (400) for an expired one, or AccessDenied (403) when the trust policy does not permit the subject.
A token whose issuer Floci does not host is treated as opaque and accepted, since Floci cannot adjudicate a third-party provider. Validation is therefore automatic for Floci-issued tokens and requires no configuration flag.
OIDC discovery endpoints
Served for fidelity and debugging, nothing in the IRSA flow dereferences them:
| Route | Description |
|---|---|
GET /_floci/eks/clusters/<name>/oidc/.well-known/openid-configuration |
OIDC discovery document |
GET /_floci/eks/clusters/<name>/oidc/keys |
JWKS containing the cluster's public key |
These live under _floci/… rather than at the AWS-shaped issuer path, which Floci's embedded DNS does not resolve and which would collide with S3's path-style routing.
ARN Format
Node groups use:
Fargate profiles use:
Examples
export AWS_ENDPOINT_URL=http://localhost:4566
export AWS_DEFAULT_REGION=us-east-1
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
# Create a cluster
aws eks create-cluster \
--name my-cluster \
--role-arn arn:aws:iam::000000000000:role/eks-role \
--resources-vpc-config subnetIds=[],securityGroupIds=[] \
--kubernetes-version 1.29
# Describe the cluster
aws eks describe-cluster --name my-cluster
# List clusters
aws eks list-clusters
# Create a node group
curl -s -X POST "$AWS_ENDPOINT_URL/clusters/my-cluster/node-groups" \
-H "Content-Type: application/json" \
-d '{
"nodegroupName": "my-nodegroup",
"nodeRole": "arn:aws:iam::000000000000:role/eks-node-role",
"subnets": ["subnet-123", "subnet-456"],
"instanceTypes": ["t3.medium"],
"scalingConfig": {
"minSize": 1,
"maxSize": 3,
"desiredSize": 1
}
}'
# Describe the node group
curl -s "$AWS_ENDPOINT_URL/clusters/my-cluster/node-groups/my-nodegroup"
# List node groups
curl -s "$AWS_ENDPOINT_URL/clusters/my-cluster/node-groups"
# Delete the node group
curl -s -X DELETE "$AWS_ENDPOINT_URL/clusters/my-cluster/node-groups/my-nodegroup"
# Create a Fargate profile
curl -s -X POST "$AWS_ENDPOINT_URL/clusters/my-cluster/fargate-profiles" \
-H "Content-Type: application/json" \
-d '{
"fargateProfileName": "my-fargate-profile",
"podExecutionRoleArn": "arn:aws:iam::000000000000:role/eks-fargate-role",
"subnets": ["subnet-123", "subnet-456"],
"selectors": [
{
"namespace": "default",
"labels": {
"app": "api"
}
}
],
"tags": {
"env": "dev"
}
}'
# Describe the Fargate profile
curl -s "$AWS_ENDPOINT_URL/clusters/my-cluster/fargate-profiles/my-fargate-profile"
# List Fargate profiles
curl -s "$AWS_ENDPOINT_URL/clusters/my-cluster/fargate-profiles"
# Delete the Fargate profile
curl -s -X DELETE "$AWS_ENDPOINT_URL/clusters/my-cluster/fargate-profiles/my-fargate-profile"
# Tag a cluster
aws eks tag-resource \
--resource-arn arn:aws:eks:us-east-1:000000000000:cluster/my-cluster \
--tags env=dev,team=platform
# Delete a cluster
aws eks delete-cluster --name my-cluster
Java SDK Example
EksClient eks = EksClient.builder()
.endpointOverride(URI.create("http://localhost:4566"))
.region(Region.US_EAST_1)
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create("test", "test")))
.build();
// Create cluster
CreateClusterResponse created = eks.createCluster(r -> r
.name("my-cluster")
.roleArn("arn:aws:iam::000000000000:role/eks-role")
.resourcesVpcConfig(v -> v
.subnetIds(List.of())
.securityGroupIds(List.of()))
.version("1.29")
.tags(Map.of("env", "dev")));
// Describe cluster
DescribeClusterResponse described = eks.describeCluster(r -> r
.name("my-cluster"));
System.out.println(described.cluster().status()); // ACTIVE
// List clusters
List<String> names = eks.listClusters(r -> {}).clusters();
// Node group and Fargate profile support are currently exposed through the REST paths.
// Tag resource
eks.tagResource(r -> r
.resourceArn(created.cluster().arn())
.tags(Map.of("team", "platform")));
// Delete cluster
eks.deleteCluster(r -> r.name("my-cluster"));
Not Implemented (Phase 1)
The following EKS features are not yet supported:
UpdateClusterConfig/UpdateClusterVersion- Add-ons (
CreateAddon,DescribeAddon,ListAddons) - Identity provider configs
- Access entries and policies
- Encryption config