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.
| 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 |
CreatePodIdentityAssociation |
Create a pod identity association between a service account and IAM role |
DescribePodIdentityAssociation |
Describe a pod identity association by association ID |
ListPodIdentityAssociations |
List pod identity associations in a cluster with optional filtering and pagination |
UpdatePodIdentityAssociation |
Update the IAM role, target role, or session tags for a pod identity association |
DeletePodIdentityAssociation |
Delete a pod identity association |
CreateAddon |
Create an addon for an EKS cluster |
DescribeAddon |
Describe an addon by cluster and addon name |
ListAddons |
List addon names installed in a cluster with pagination |
UpdateAddon |
Update addon configuration, version, or service account role |
DescribeUpdate |
Describe an update for an addon |
DeleteAddon |
Delete an addon from a cluster |
DescribeAddonVersions |
Describe supported addon versions by Kubernetes version or addon name |
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 |
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.
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.
!!! note "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_LINUXFloci supports the EKS Pod Identity association management plane. You can map a Kubernetes (namespace, serviceAccount) pair directly to an IAM role without configuring OpenID Connect (OIDC) identity providers or mutating ServiceAccount annotations.
- Creation:
CreatePodIdentityAssociationassociates a Kubernetes service account in a specific namespace with an IAM role ARN. The referenced IAM role must exist in IAM. A cluster cannot have duplicate associations for the same(namespace, serviceAccount)pair (rejected with HTTP 409ResourceInUseException). Requests support idempotency viaclientRequestToken. - Retrieval:
DescribePodIdentityAssociationretrieves full association details by cluster name and association ID (a-followed by 17 alphanumeric characters). - Listing:
ListPodIdentityAssociationsreturns summaries of associations for a cluster, with optional filtering bynamespaceandserviceAccount, plus pagination usingmaxResults(1-100) and cluster-boundnextToken. - Updating:
UpdatePodIdentityAssociationmodifies the associatedroleArn,targetRoleArn,disableSessionTags, orpolicy. - Deletion:
DeletePodIdentityAssociationremoves an association and returns its final metadata. Deleting an EKS cluster automatically purges all of its pod identity associations.
Floci registers a MutatingWebhookConfiguration in every real-mode cluster, scoped to pod CREATE. When a pod is created, Floci looks up an association matching the pod's namespace and its spec.serviceAccountName. A pod that matches nothing is admitted unchanged.
Injection happens only when the eks-pod-identity-agent addon is installed on the cluster, which is what real EKS requires too. Install it with CreateAddon before expecting pods to be mutated.
A matching pod is patched exactly the way real EKS patches it. Every init container and container gets:
| What | Value |
|---|---|
AWS_CONTAINER_CREDENTIALS_FULL_URI |
http://169.254.170.23/v1/credentials |
AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE |
/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token |
| Volume mount | /var/run/secrets/pods.eks.amazonaws.com/serviceaccount, read-only, from volume eks-pod-identity-token |
The volume is a projected service account token with audience pods.eks.amazonaws.com, expirationSeconds: 86400 and path eks-pod-identity-token. A name the pod already carries (a volume, a mount, or either environment variable) is left as the workload set it.
Floci serves pod identity credentials through a link-local relay bound to 169.254.170.23:80 inside the cluster container, forwarding requests to Floci's credentials endpoint (GET /v1/credentials). Workloads reach this endpoint using the injected AWS_CONTAINER_CREDENTIALS_FULL_URI environment variable, passing their projected token in the Authorization header (Authorization: <token> or Authorization: Bearer <token>).
The credentials endpoint performs the following validations:
- Validates token format and extracts the issuer URL without verifying signatures yet.
- Identifies the matching EKS cluster by comparing the token issuer URL to cluster OIDC issuer URLs.
- Validates the token signature against the cluster OIDC public key, confirming the token is unexpired and carries audience
pods.eks.amazonaws.com. - Extracts the subject claim (
system:serviceaccount:<namespace>:<serviceAccount>) and looks up the pod identity association for the cluster, namespace, and service account. - Issues temporary session credentials (
ASIA...) for the associated IAM role and returns them in standard AWS container credentials format (AccessKeyId,SecretAccessKey,Token,AccountId,Expiration).
Kubernetes only accepts an https clientConfig.url on a MutatingWebhookConfiguration, together with a caBundle the API server trusts. Floci serves HTTP with the default floci.tls.enabled=false, so the webhook is skipped with a warning in that case and pods start unmutated. Set FLOCI_TLS_ENABLED=true to get injection; the caBundle is then Floci's local CA, the same one served at GET /_floci/ca.pem.
The webhook URL uses the hostname containers reach Floci on. When Floci runs natively that is host.docker.internal, which the self-signed certificate covers. When Floci itself runs in a container the hostname is its container IP, and the self-signed certificate carries no SAN for it, so the API server rejects the handshake. Pods still start, unmutated. Point FLOCI_SERVICES_LAMBDA_DOCKER_HOST_OVERRIDE at a name the certificate covers (add it with FLOCI_HOSTNAME) to make the webhook reachable from that setup.
The webhook carries failurePolicy: Ignore, and Floci admits the pod unchanged on any internal error, so a webhook that cannot be registered or cannot be reached never prevents a pod from being created. Registration failures log a warning and leave the cluster running.
The manifest is written into the cluster's k3s server manifests directory (/var/lib/rancher/k3s/server/manifests) before the container starts, so k3s applies it as the API server comes up.
Floci supports the EKS cluster addon management plane for AWS SDKs and Terraform (aws_eks_addon resource).
- Creation:
CreateAddoncreates an addon on an ACTIVE cluster. Supported addons includevpc-cni,coredns,kube-proxy, andeks-pod-identity-agent. IfaddonVersionis omitted, the default version compatible with the cluster Kubernetes version is resolved automatically. ReferencedserviceAccountRoleArnmust exist in IAM. Idempotency is supported viaclientRequestToken. - Retrieval:
DescribeAddonreturns the complete addon resource shape, including ARN, cluster name, version, status (ACTIVE), health issues, tags, service account role ARN, configuration values, pod identity associations, owner, and publisher. - Listing:
ListAddonslists installed addon names with pagination (maxResultsandnextToken). - Updating:
UpdateAddonupdates the addon version, configuration values, service account role ARN, or resolve-conflicts strategy. It returns anUpdatetracking object and updates the addon metadata. - Update tracking:
DescribeUpdatedescribes the status of an addon update (such as an in-place addon version update queried by Terraform). - Deletion:
DeleteAddonmarks the addon asDELETINGand removes it from the cluster. Deleting a cluster automatically cleans up all associated addons. - Supported versions:
DescribeAddonVersionsqueries the addon version catalog with optional filtering byaddonNameandkubernetesVersion, supporting pagination.
Addons in Floci are recorded metadata only. Creating or updating an addon does not install or reconcile Kubernetes DaemonSets, Deployments, or custom resources inside the cluster container.
When an EKS cluster is created with a VPC (either specified directly or resolved from subnets), Floci provisions a dedicated EC2 cluster security group and populates its ID in cluster.resourcesVpcConfig.clusterSecurityGroupId on both CreateCluster and DescribeCluster responses.
The generated security group name follows the AWS pattern:
eks-cluster-sg-<cluster-name>-<suffix>
where <suffix> is an 8-character random hexadecimal string. The security group description is set to:
EKS created security group applied to ENI that is attached to EKS Control Plane master nodes, as well as any managed workloads.
Floci tags the cluster security group with the standard AWS system tags:
Name:eks-cluster-sg-<cluster-name>-<suffix>kubernetes.io/cluster/<cluster-name>:ownedaws:eks:cluster-name:<cluster-name>
- Creation: The security group is created in the resolved cluster VPC when the cluster is created. Both real and mock clusters receive a security group. If a cluster is created without any VPC or subnets,
clusterSecurityGroupIdis omitted from the response. If a caller explicitly provides aresourcesVpcConfig.vpcIdthat does not exist in EC2, the request is rejected withInvalidParameterException(HTTP 400). - Deletion: When the cluster is deleted via
DeleteCluster, Floci deletes the associated cluster security group from EC2. If the security group was already removed out-of-band, the deletion succeeds idempotently without error. - Backfill: Existing persisted clusters that were created before this feature receive an auto-generated cluster security group during startup backfill if their VPC is present.
- Rules: In accordance with AWS EKS behavior, the cluster security group includes a self-referencing inbound rule allowing all traffic from members of the same security group to enable control plane and node communication, a self-referencing outbound rule for Elastic Fabric Adapter (EFA) traffic, and the standard EC2 outbound rule.
EKS clusters support configuring KMS envelope encryption for secrets and control plane logging export.
CreateCluster accepts an encryptionConfig array specifying the encryption provider for Kubernetes resources:
{
"encryptionConfig": [
{
"resources": ["secrets"],
"provider": {
"keyArn": "arn:aws:kms:us-east-1:000000000000:key/12345678-1234-1234-1234-123456789012"
}
}
]
}- Supported resources: Only
["secrets"]is supported by AWS EKS. Specifying any other resource or leaving resources empty causesInvalidParameterException(HTTP 400). - Provider:
provider.keyArnmust be non-empty. - Cardinality: AWS allows at most one encryption configuration entry. Specifying more than one causes
InvalidParameterException(HTTP 400). - Omission: When omitted at creation time,
encryptionConfigis not populated and is omitted fromDescribeClusterresponses (null or omitted, no synthetic default). - Scope: Floci stores and returns the encryption configuration faithfully. Envelope encryption of Secrets at the Kubernetes storage layer and KMS cryptographic operations are out of scope.
CreateCluster accepts control plane logging configuration in logging.clusterLogging:
{
"logging": {
"clusterLogging": [
{
"types": ["api", "audit"],
"enabled": true
}
]
}
}- Supported log types: The valid EKS log types are
api,audit,authenticator,controllerManager, andscheduler. Specifying an unrecognized type causesInvalidParameterException(HTTP 400). - Response normalization: AWS EKS always returns the status of all five log types. Enabled types are returned first (
enabled: true), followed by disabled types (enabled: false). - Default logging: When omitted or empty at creation time, Floci returns all five log types disabled in a single entry (
enabled: false). - Backfill: Existing persisted clusters created before this feature was introduced are automatically backfilled on startup with default disabled logging.
- CloudWatch Logs delivery:
- When the
apilog type is enabled on a cluster, Floci creates the CloudWatch Logs log group/aws/eks/<cluster-name>/clusterand streams control plane container output to a log stream namedkube-apiserver-<hash>, where<hash>is the first 32 characters of the container ID. - When the
auditlog type is enabled on a cluster, Floci configures k3s with the Amazon EKS control plane audit policy, writes audit logs to/var/log/audit.loginside the container, and streams audit records to a log stream namedkube-apiserver-audit-<hash>. - When both
apiandauditare enabled, both streams are created and populated under/aws/eks/<cluster-name>/cluster. When neither is enabled, no log group or streams are created.
- When the
- Component streams deviation: AWS EKS provisions separate log streams for each component (
kube-apiserver-*,kube-apiserver-audit-*,kube-controller-manager-*,kube-scheduler-*,authenticator-*). Because Floci runs clusters on k3s, which embeds the Kubernetes API server, controller manager, and scheduler within a single process, control plane container logs are delivered to the singlekube-apiserver-<hash>stream whenapiis enabled, and API server audit logs are delivered tokube-apiserver-audit-<hash>whenauditis enabled. Other control plane log types (authenticator,controllerManager,scheduler) do not provision separate streams. - Authenticator logs: Authenticator webhook authentication events are logged directly by Floci.
CreateNodegroup accepts the full documented input set and stores every member, so CreateNodegroup and DescribeNodegroup both return exactly what was supplied.
nodegroupName, nodeRole, subnets, version, releaseVersion, amiType, capacityType, diskSize, instanceTypes, scalingConfig, labels, tags, and clientRequestToken. Several of these receive an AWS-shaped default when omitted: capacityType becomes ON_DEMAND, amiType becomes AL2_x86_64, diskSize becomes 20, instanceTypes becomes ["t3.medium"], version is inherited from the cluster, and releaseVersion becomes <version>-eks-1.
launchTemplate, remoteAccess, taints, updateConfig, nodeRepairConfig, and warmPoolConfig are recorded as supplied and echoed back verbatim, including nested members:
{
"nodegroupName": "my-nodegroup",
"nodeRole": "arn:aws:iam::000000000000:role/eks-node-role",
"subnets": ["subnet-123"],
"launchTemplate": {
"id": "lt-0a1b2c3d4e5f60718",
"version": "3"
},
"remoteAccess": {
"ec2SshKey": "my-keypair",
"sourceSecurityGroups": ["sg-0123456789abcdef0"]
},
"taints": [
{"key": "dedicated", "value": "gpu", "effect": "NO_SCHEDULE"}
],
"nodeRepairConfig": {"enabled": true},
"warmPoolConfig": {"enabled": true, "minSize": 2, "poolState": "Stopped"}
}- Omission: When a structured input is not supplied, it is omitted from
CreateNodegroupandDescribeNodegroupresponses rather than serialized as an explicitnull. The exception isupdateConfig, which receives the AWS default of{"maxUnavailable": 1}. - Validation: When
launchTemplateis supplied, Floci validates it against EC2. Requests must specify eitheridorname, but not both; supplying neither or both is rejected withInvalidParameterException(HTTP 400). The template and requested version (defaulting to the template's default version when omitted) must exist in EC2, otherwise the request is rejected withInvalidParameterException(HTTP 400). The other structured inputs (remoteAccess,taints,nodeRepairConfig,warmPoolConfig) are not validated against external resources. - No backfill: Node groups created before this was supported genuinely had no launch template, taints, remote access, node repair config, or warm pool config, so there is nothing to reconstruct. They continue to omit those members, which is the correct answer for them.
When a node group specifies a launchTemplate, Floci resolves the launch template from EC2 (by ID or name, and version) and executes any provided user data inside the cluster's running k3s container. If the referenced launch template carries no user data, execution is skipped.
- Execution target: Floci runs one k3s container per cluster. Launch template user data executes inside this cluster container via
docker exec. - Single-container mapping rule:
- The first node group specifying user data executes its scripts in the container.
- Subsequent node groups on the same cluster with identical user data skip execution as a no-op.
- Subsequent node groups on the same cluster with differing user data log a warning and skip execution without failing the node group.
- Timing and tradeoffs: User data runs post-start when
CreateNodegroupis invoked. Because the k3s process is already running, bootstrap configurations requiring pre-kubelet drop-ins (such as/etc/rancher/k3s/config.yaml) cannot be reloaded dynamically without restarting the container. - Failure handling: If any user data script exits with a non-zero status or times out (30-minute limit), the node group status is set to
CREATE_FAILEDandhealth.issuesis populated with codeNodeCreationFailure, the failure message, and the node group name inresourceIds. - Differences from EC2: Scripts run inside the existing shared k3s container rather than an isolated VM instance. Other launch template settings (AMI, instance type, block device mappings, network interfaces) remain metadata-only and do not alter container provisioning.
The remaining structured inputs are recorded as metadata:
remoteAccessdoes not open SSH access, and the referenced key pair and security groups are not wired up.taintsare not applied to Kubernetes nodes, so pods are not repelled from them.nodeRepairConfigstarts no repair loop, andwarmPoolConfigpre-initializes no instances.
The value of storing them is state fidelity: infrastructure tools read DescribeNodegroup back and compare it against their declared configuration. Dropping launchTemplate in particular made the OpenTofu AWS provider see a node group with no launch template where one was declared, which is a ForceNew difference and caused a destroy and recreate on every plan.
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.
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.
The standard AWS flow works end to end:
aws eks update-kubeconfig --name my-cluster
kubectl get nodesaws 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 15-minute token lifetime matching aws-iam-authenticator rather than the presigned 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).
!!! note "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**.
!!! note "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!!! note "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.
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.
In real mode, cluster nodes carry a Kubernetes spec.providerID matching the AWS format:
aws:///<availability-zone>/<instance-id>
For example, aws:///us-east-1a/i-0123456789abcdef0. Floci derives this identifier deterministically before container startup and passes --kubelet-arg=provider-id=<providerId> to k3s.
The derived availability zone and instance ID match the synthetic EC2 node instance created for link-local IMDS, ensuring consistent identity across node metadata and kubelet registration. The provider ID enables controllers that reconcile nodes against EC2 (such as CSI drivers) to extract the instance ID. Note that nothing yet resolves that synthetic instance through the EC2 API.
| 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 fallback |
FLOCI_SERVICES_EKS_IMAGE_TEMPLATE |
(unset) | Format string for custom k3s images (e.g. myregistry.io/k3s:v%s), taking cluster version |
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_POD_IDENTITY_WEBHOOK |
true |
Register a mutating admission webhook that injects pod identity credentials. Needs FLOCI_TLS_ENABLED=true |
FLOCI_SERVICES_EKS_IMDS |
false |
Enable link-local IMDS (169.254.169.254) proxy in cluster containers |
Floci supports standard AWS EKS Kubernetes versions (1.xx, 1.28 and above). Known releases are mapped to stable pinned k3s images, while newer or unpinned versions dynamically resolve to upstream k3s release images (rancher/k3s:v<version>.0-k3s1):
1.28(rancher/k3s:v1.28.15-k3s1)1.29(rancher/k3s:v1.29.14-k3s1, default version metadata)1.30(rancher/k3s:v1.30.10-k3s1)1.31(rancher/k3s:v1.31.5-k3s1)1.32(rancher/k3s:v1.32.2-k3s1)1.33(rancher/k3s:v1.33.1-k3s1)1.34(rancher/k3s:v1.34.1-k3s1)1.35(rancher/k3s:v1.35.0-k3s1)1.36(rancher/k3s:v1.36.0-k3s1)1.37+(dynamically resolved torancher/k3s:v<version>.0-k3s1)
When --version is omitted, the cluster version defaults to 1.29 and the container image resolution uses FLOCI_SERVICES_EKS_DEFAULT_IMAGE (rancher/k3s:latest by default). When a version is explicitly requested, it maps to the corresponding pinned image or template.
You can specify standard EKS version and kubernetesNetworkConfig.serviceIpv4Cidr when creating a cluster:
aws --endpoint-url http://localhost:4566 eks create-cluster \
--name my-cluster \
--role-arn arn:aws:iam::000000000000:role/eks-role \
--version 1.31 \
--kubernetes-network-config serviceIpv4Cidr=172.20.0.0/16Floci validates that serviceIpv4Cidr falls within RFC 1918 private address ranges (10.0.0.0/8, 172.16.0.0/12, or 192.168.0.0/16), has a prefix length between /12 and /24, and does not overlap with the VPC CIDR. When omitted, Floci defaults serviceIpv4Cidr to 10.100.0.0/16 (or 172.20.0.0/16 if 10.100.0.0/16 overlaps with the VPC). Internal pod CIDR (--cluster-cidr) defaults to 10.42.0.0/16 to avoid collisions with Docker bridge networks.
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=v1Requirements 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>
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"The cluster container is registered as a synthesized EC2 instance node (type m5.large, image ami-eks-k3s) regardless of whether IMDS is enabled, making it visible to the EC2 service (DescribeInstances, DescribeInstanceStatus, volume attachments, tagging).
When IMDS is 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 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-idThe 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. - Default isolation for ordinary pods: By default, pods running in separate pod network namespaces cannot reach
169.254.169.254. CNI portmap rules on the node intercept local traffic, preserving node credential isolation for multi-tenant or untrusted workloads. - Opt-in pod network routing: When
floci.services.eks.imds-pod-network=trueis set alongsidefloci.services.eks.imds=true, iptables DNAT rules are programmed in a customFLOCI-LINK-LOCALchain at the head ofPREROUTING. Traffic originating from the pod CIDR (10.42.0.0/16) targeting169.254.169.254:80matches first and is routed directly to the local node listener, preempting CNI portmap rules. This enables standard AWS SDK credential resolution inside ordinary pods without requiringhostNetwork: true.
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.
Pod network reachability is also disabled by default (floci.services.eks.imds-pod-network=false) to maintain pod isolation. Set FLOCI_SERVICES_EKS_IMDS_POD_NETWORK=true together with FLOCI_SERVICES_EKS_IMDS=true to route pod traffic to the link-local proxy.
A failure to configure the proxy (for example on custom minimal images lacking network utilities) logs a warning and allows cluster startup to continue.
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.
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.
Floci supports two ways to obtain IRSA service account tokens:
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.comTokens 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.
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.
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"
}
}
}]
}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.
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:aws:eks:<region>:<accountId>:cluster/<clusterName>
Node groups use:
arn:aws:eks:<region>:<accountId>:nodegroup/<clusterName>/<nodegroupName>/<id>
Fargate profiles use:
arn:aws:eks:<region>:<accountId>:fargateprofile/<clusterName>/<fargateProfileName>/<id>
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-clusterEksClient 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"));The following EKS features are not yet supported:
UpdateClusterConfig/UpdateClusterVersionUpdateNodegroupConfig/UpdateNodegroupVersion- Add-ons (
CreateAddon,DescribeAddon,ListAddons) - Identity provider configs
- Access policies