
September 2026 Floci Update Summary: Local CA Support Added and IAM Managed Policies Now Evaluated
This page has been translated by machine translation. View original
Hello. I'm Takeda from the Service Development Division.
We have previously covered "Floci," an open-source AWS emulator that emerged as an alternative to LocalStack.
- Introduction and operational verification of major services (March 2026)
- One-month update summary since release (April 30, 2026)
- Another one-month update summary (May 30, 2026)
- A month with CloudFormation and Web Console support (June 30, 2026)
- A month with refined authorization and CloudTrail event recording (July 30, 2026)
- CloudFront delivery and AppSync GraphQL endpoint now working (August 29, 2026)
Another month has passed since the last article. This time, I will summarize the updates from this past month while running some of them locally.
This time the major version was bumped, with 2.0.0 and 2.1.0 released. The IAM managed policies, Logs Insights, and Cloud Control issues raised in the previous article have all received fixes.
Main changes over the past month (as of September 29, 2026)
Here is a summary of the changes from the previous article (at 1.7.0) to the current version (2.1.0).
- 3 releases (2.0.0 and 2.0.1 on September 1, 2.1.0 on September 15). 856 commits between 1.7.0 and 2.1.0
- The number of supported services in the README is 84 in 2.0.0. In 2.1.0, the total count notation was removed
- GitHub stars went from 22,504 → 26,051
- The number of compatibility tests in the README went from 2,506 → 2,576
- Docker image size (amd64, compressed) went from 119.7MB → 80.6MB
- IAM managed policies now return the actual AWS policy documents
- Logs Insights now returns queries using
>=,like, andstatsas failures - TLS now includes a local CA; HTTPS connections succeed if the CA is trusted. Verification fails with Python 3.13's default settings
- New services include Organizations, FIS, EFS, Redshift, Control Tower, Verified Permissions, and others
Release numbers and coverage figures
After 1.7.0 (August 18), 2.0.0 came out (September 1), with 2.0.1 containing one fix released the same day. Two weeks later, 2.1.0 was released (September 15). The release notes contain 146 Bug Fixes and 92 Features for 2.0.0, and 337 Bug Fixes and 156 Features for 2.1.0. Organizations, FIS, EFS, and others listed as "unreleased" in the previous article were included in 2.0.0.
The major version was bumped, but the only breaking change explicitly noted in the 2.0.0 release notes is one commit for Step Functions (#2699).
The number of supported services in the README went from 69 in 1.7.0 to 84 in 2.0.0. In 2.1.0, the total count notation was removed and replaced with the text "Broad AWS coverage. Free forever." and a link to the Services Overview. The service support table itself remains, with 91 rows in 2.1.0. Since some rows combine multiple services, the row count does not match the service count.
The number of compatibility tests was updated to 2,576 in 2.0.0 and remains the same in 2.1.0.
For Docker image sizes, based on Docker Hub tag information (amd64, compressed): 119.7MB for 1.7.0, 138.9MB for 2.0.0, and 80.6MB for 2.1.0. 2.1.0 includes a change to switch the base image to ubi9-micro (#3085). The "109MB to 78MB" figure in the release notes was measured using the arm64 nightly before and after the change, and differs from the measurement target of the amd64 release tag.
Previous homework
The environment setup is the same as before — prepare a docker-compose.yml and start it up.
services:
floci:
image: floci/floci:2.1.0
ports:
- "4566:4566"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
$ export AWS_ENDPOINT_URL=http://localhost:4566
$ export AWS_DEFAULT_REGION=us-east-1
$ export AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test
IAM managed policies: no longer Allow *
In the previous version, all 1,566 managed policies returned an Allow * document. With IAM enforcement mode enabled, a user with only AmazonS3ReadOnlyAccess could create buckets.
2.1.0 includes a change to use the actual AWS policy documents (#3237) and a change to return the actual default version (#3275). Let's retrieve the same policy.
$ aws iam get-policy --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \
--query 'Policy.DefaultVersionId' --output text
v3
$ aws iam get-policy-version --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess \
--version-id v3 --query 'PolicyVersion.Document.Statement'
[
{
"Action": [
"s3:Get*",
"s3:List*",
"s3:Describe*",
"s3-object-lambda:Get*",
"s3-object-lambda:List*"
],
"Effect": "Allow",
"Resource": "*"
}
]
The document now allows only read-type actions. The count remains at 1,566. Only the current default version is included; specifying v1 returns NoSuchEntity.
Let's perform the same operations as before with enforcement mode (FLOCI_SERVICES_IAM_ENFORCEMENT_ENABLED=true).
# Executed with access key for ro-user (AmazonS3ReadOnlyAccess only)
$ aws s3 ls s3://root-bucket
2026-09-29 11:06:14 5 x.txt
$ aws s3 mb s3://ro-made-this
make_bucket failed: s3://ro-made-this An error occurred (AccessDenied) when calling the CreateBucket operation: User is not authorized to perform: s3:CreateBucket
$ echo written-by-ro | aws s3 cp - s3://root-bucket/y.txt
upload failed: - to s3://root-bucket/y.txt An error occurred (AccessDenied) when calling the PutObject operation: User is not authorized to perform: s3:PutObject
$ aws sqs create-queue --queue-name ro-q
aws: [ERROR]: An error occurred (AccessDeniedException) when calling the CreateQueue operation: User is not authorized to perform: sqs:CreateQueue
Listing and retrieval succeeded, while bucket creation, object writing, and SQS queue creation were denied. These were three operations that all succeeded previously.
What was confirmed is the result when an IAM-registered user calls these APIs in an environment with enforcement mode enabled. Enforcement mode is disabled by default. Even when enabled, access key test or access keys not registered in IAM are allowed without evaluation.
#2042: >= and like in Logs Insights now result in query failures
Previously, filter using >= or like was silently ignored with a warning and returned all records. The issue point of "should unsupported syntax be an error" remained unresolved.
This issue was closed on September 6. The corresponding PR (#3139) is included in 2.1.0. Let's send the same queries as before to a log group with three log events.
$ aws logs start-query --log-group-name /hw/app \
--start-time $(( $(date +%s) - 3600 )) --end-time $(( $(date +%s) + 3600 )) \
--query-string 'fields @message | filter @message >= "x"' \
--query queryId --output text
e51ff11f-1e33-41f3-bdf0-3b2a3457ea2b
$ aws logs get-query-results --query-id e51ff11f-1e33-41f3-bdf0-3b2a3457ea2b \
--query '{status:status,count:length(results)}'
{
"status": "Failed",
"count": 0
}
StartQuery returns a query ID, and the status in GetQueryResults becomes Failed. Results per query are as follows.
| Query | 1.7.0 | 2.1.0 |
|---|---|---|
filter @message >= "x" |
All records with warning | Failed |
filter @message like /ERROR/ |
All records with warning | Failed |
stats count(*) by bin(5m) |
Ignored with warning | Failed |
filter @message = "beta INFO two" |
1 record | 1 record |
like and stats now also fail. If you have tests that include Logs Insights queries running on Floci, updating to 2.1.0 may cause those queries to start failing.
The reason for failure is not included in the API response but appears in the server log.
WARN [io.git.hec.flo.ser.clo.log.CloudWatchLogsService] Logs Insights query e51ff11f-... will fail: Unsupported filter expression: @message >= "x"
A filter with conditions joined by and still returns 0 results without an error in 2.1.0. This is the case I wrote about in the previous issue — returning 0 results without a warning.
# filter @message = "beta INFO two" and @message != "x"
{
"status": "Complete",
"count": 0
}
The Logs Insights section in docs/services/cloudwatch.md still reads "unsupported syntax does not cause query failure" even in 2.1.0. This is content I wrote in #2044 two articles ago. #3139 only changes the source and tests. I submitted a PR to rewrite the documentation to match the behavior (#4717).
#2043: Unsupported types in Cloud Control now return errors
The issue where ListResources for unsupported types returned an empty list was also closed on September 6. The corresponding PR (#3141) is included in 2.1.0.
$ aws cloudcontrol list-resources --type-name AWS::Lambda::Function
aws: [ERROR]: An error occurred (UnsupportedActionException) when calling the ListResources operation: ListResources is not supported for resource type AWS::Lambda::Function.
$ aws cloudcontrol list-resources --type-name AWS::S3::Bucket \
--query 'ResourceDescriptions[].Identifier'
[
"cf-origin"
]
The number of listable types is the same nine as before and has not increased. The "can be created but not listed" gap from before also remains. Testing with an SQS queue shows that creation and individual retrieval work, while listing returns an error.
$ aws cloudcontrol create-resource --type-name AWS::SQS::Queue \
--desired-state '{"QueueName":"cc-created-2"}' \
--query ProgressEvent.RequestToken --output text
# → GetResourceRequestStatus returns SUCCESS
$ aws cloudcontrol get-resource --type-name AWS::SQS::Queue \
--identifier http://localhost:4566/000000000000/cc-created-2 \
--query 'ResourceDescription.Properties' --output text
{"QueueName":"cc-created-2","Id":"http://localhost:4566/000000000000/cc-created-2"}
$ aws cloudcontrol list-resources --type-name AWS::SQS::Queue
aws: [ERROR]: An error occurred (UnsupportedActionException) when calling the ListResources operation: ListResources is not supported for resource type AWS::SQS::Queue.
When an empty list was returned, it could be read as "the queue doesn't exist," but in 2.1.0 the error clearly indicates that listing is not supported. This coverage gap is also explicitly documented in 2.1.0.
AppSync: API key authentication succeeds, resolvers are unimplemented in 2.1.0
The PR I submitted previously (#2645) and authentication Phase 7 (#2380) were included in 2.0.0. The id returned by create-api-key is now a key value starting with da2-, and the apiKey field has been removed from the response.
$ aws appsync create-api-key --api-id ${API_ID}
{
"apiKey": {
"id": "da2-1j1qi8dt7pl4cbpnuqxov6a1l2",
"expires": 1791252000,
"deletes": 1796436000
}
}
x-api-key |
Result |
|---|---|
| None | 401 (Missing authorization header) |
| Incorrect value | 401 (You are not authorized to make this call.) |
id from create-api-key |
200 |
2.1.0 also includes JWT signature and SigV4 verification (#3541).
Resolvers are still not executed in 2.1.0. Sending a query with a NONE data source resolver configured returns {"data":{"hello":null}} as before. In the 2.1.0 documentation, resolver dispatch is Phase 8 and data source connection is Phase 9, both still unimplemented.
Main now includes changes to execute VTL resolvers (#4356, #4423). Testing with the nightly image (floci/floci:nightly-09282026), a NONE data source resolver returned an error.
{"data":{"hello":null},"errors":[{"message":"VTL mapping template evaluation failed: io.github.hectorvent.floci.services.appsync.graphql.ReturnDirective","locations":[],"path":["hello"],"errorType":"MappingTemplate","errorInfo":null}]}
The server log shows a ClassNotFoundException for ReturnDirective. A query with a DynamoDB data source GetItem resolver also returned the same error. With the nightly from one day prior, even changing the response to a template with only a fixed value did not change the result.
JSONata definition validation changes in 2.0.0
The breaking change in 2.0.0 relates to JSONata expressions in Step Functions (#2699). Definitions that reference input using top-level names (such as name) are now rejected at creation time. AWS also rejects the same definitions.
Let's prepare a definition with a top-level reference in the Output of a Pass state.
{
"QueryLanguage": "JSONata",
"StartAt": "Greet",
"States": {
"Greet": {
"Type": "Pass",
"Output": { "message": "{% 'hello ' & name %}" },
"End": true
}
}
}
In 1.7.0, both validation and creation of this definition succeed. Passing {"name":"Floci"} as input to an execution produced the following result.
$ aws stepfunctions describe-execution --execution-arn ${EXECUTION_ARN} \
--query '{status:status,output:output}'
{
"status": "SUCCEEDED",
"output": "{\"message\":\"hello \"}"
}
The execution succeeded and the output was hello without the value of name. In 2.1.0, the same definition is rejected at validation. create-state-machine also fails with InvalidDefinition and the same message.
$ aws stepfunctions validate-state-machine-definition --definition file://bare.json
{
"result": "FAIL",
"diagnostics": [
{
"severity": "ERROR",
"code": "UNSUPPORTED_JSONATA_EXPRESSION",
"message": "Reference to 'name' at the top level is not supported.",
"location": "/States/Greet/Output/message"
}
],
"truncated": false
}
A definition with the expression changed to {% 'hello ' & $states.input.name %} can be created in both 1.7.0 and 2.1.0, and returns hello Floci.
How top-level references are evaluated in 1.7.0 depends on how the expression is written. In this expression the value was empty but the execution succeeded, though the same result may not apply to other expressions. If definition creation fails after updating to 2.0.0 or later, fix the expression at the location indicated in the error to use $states.input.<name>. For variables defined with Assign, use $<name>.
Header settings reflected in CloudFront delivery
In the previous article, we confirmed that CloudFront was delivering content from an S3 origin. At that time, response headers from S3 were returned as-is. 2.0.0 includes response header policy application (#1833) and origin custom header forwarding (#1832).
Response header policy
Let's create a policy with one custom header and three security headers.
{
"Name": "blog-headers",
"Comment": "custom + security headers",
"CustomHeadersConfig": {"Quantity": 1, "Items": [{"Header": "X-Policy", "Value": "applied", "Override": true}]},
"SecurityHeadersConfig": {
"ContentTypeOptions": {"Override": true},
"FrameOptions": {"FrameOption": "DENY", "Override": true},
"StrictTransportSecurity": {"AccessControlMaxAgeSec": 31536000, "IncludeSubdomains": true, "Override": true}
}
}
Specify the ID of this policy in DefaultCacheBehavior.ResponseHeadersPolicyId of the distribution. The origin is the same S3 bucket with index.html as before.
$ curl -s -i -H "Host: EI5ZKYEAL7U76Q.cloudfront.net" http://localhost:4566/
HTTP/1.1 200 OK
Content-Type: text/html
...
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-Policy: applied
<h1>hello from S3 origin</h1>
The four headers defined in the policy were attached. In 1.7.0, policy creation and association can be done with the same steps, but the headers do not appear in delivery responses.
AWS-managed response header policies are also available, with 5 entries including Managed-SecurityHeadersPolicy and Managed-SimpleCORS.
Origin custom headers
Origin custom headers are a setting that adds fixed headers to requests from CloudFront to the origin. They are commonly used in configurations where the origin validates these headers to reject access that doesn't go through CloudFront.
I prepared an HTTP server that returns received request headers as JSON as a custom origin. It is running in the same Docker network as Floci under the name echo-origin. Since hosts that resolve to private addresses are blocked by default, I allow them with a Floci environment variable.
environment:
FLOCI_SERVICES_CLOUDFRONT_ALLOWED_PRIVATE_ORIGIN_HOSTS: "echo-origin"
I specified X-Origin-Verify: shared-secret-42 in the origin settings, and sent a request from the viewer with the same header name but a different value.
$ curl -s -H "Host: EJXR0YT9QDEBOH.cloudfront.net" \
-H "X-Origin-Verify: forged" -H "X-Viewer-Only: v1" \
"http://localhost:4566/items?color=red"
{
"path": "/items",
"headers": {
"X-Origin-Verify": "shared-secret-42",
"Host": "echo-origin:8080",
"Connection": "keep-alive",
"User-Agent": "Apache-HttpClient (Java/25.0.4.1)"
}
}
The X-Origin-Verify received by the origin has the configured value, and the forged value sent by the viewer did not reach it.
This distribution had a policy with X-Frame-Options set to Override: false. The origin HTTP server was configured to return X-Frame-Options: SAMEORIGIN, and the origin's SAMEORIGIN was preserved in the delivery response.
The query string ?color=red and the viewer-supplied X-Viewer-Only did not reach the origin. According to the documentation, evaluation of cache policies and origin request policies during delivery remains unimplemented. Configurations that use policies to control what is forwarded to the origin cannot yet be verified.
When specifying a private origin that is not allowed, distribution creation succeeds and delivery returns 502. The server log only shows the exception name UnknownHostException. Since the implementation also throws this exception when a private address is detected after name resolution, it cannot be distinguished from a DNS failure in the logs.
TLS and local CA in 2.1.0
Floci's TLS has existed for some time and is enabled with FLOCI_TLS_ENABLED=true (disabled by default). Previously, the server certificate was self-signed, and the documentation examples showed disabling certificate verification on the client side. In 2.1.0, a mechanism was introduced where a local root CA signs the server certificate (#3078).
services:
floci:
image: floci/floci:2.1.0
ports:
- "4566:4566"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
FLOCI_TLS_ENABLED: "true"
Retrieving and trusting the CA
The CA certificate can be obtained from /_floci/ca.pem.
$ curl -s http://localhost:4566/_floci/ca.pem -o floci-root-ca.pem
$ openssl x509 -in floci-root-ca.pem -noout -subject -enddate
subject=CN=Floci Local CA
notAfter=Dec 31 23:59:59 2050 GMT
$ aws --endpoint-url https://localhost:4566 sts get-caller-identity
aws: [ERROR]: SSL validation failed for https://localhost:4566/ [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1082)
$ export AWS_CA_BUNDLE=$PWD/floci-root-ca.pem
$ aws --endpoint-url https://localhost:4566 sts get-caller-identity
{
"UserId": "000000000000",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
With the same settings in 1.7.0, the server certificate issuer is CN=localhost (self-signed), and /_floci/ca.pem returns 404.
Since AWS_CA_BUNDLE is a per-process setting, it can be used without adding the CA to the OS trust store. The documentation also presents the per-process environment variable approach first.
Certificates issued by ACM are also signed by the same CA. Running openssl verify -CAfile floci-root-ca.pem on a certificate created with request-certificate succeeded.
SANs increase when custom domains are created
The server certificate's SANs already include entries like localhost and *.localhost.floci.io. Since wildcards only cover one level, names like api.dev.localhost.floci.io are not covered. In 2.1.0, creating a custom domain in API Gateway or similar adds that name to the server certificate (#3086).
$ aws apigateway create-domain-name --domain-name api.dev.localhost.floci.io
Comparing the server certificate before and after creation, the serial number changed and DNS:api.dev.localhost.floci.io was added at the end of the SANs. Floci was not restarted. A request to https://api.dev.localhost.floci.io:4566/ — which previously resulted in a certificate error — now passes TLS connection after creation. Since no API is mapped to the domain, the response is 404.
Only names with local suffixes such as localhost or localhost.floci.io are added. Testing with api.example.com, domain creation succeeded, nothing was added to the certificate, and a warning appeared in the server log.
WARN [io.git.hec.flo.con.TlsCertificateManager] TLS: refusing to add api.example.com to the server certificate: not under a local suffix (...)
CA bundle passed into Lambda containers
Containers started by Floci receive the CA bundle (#3091). Printing environment variables inside a Lambda function showed five CA-related variables pointing to /etc/floci-ca-bundle.pem: SSL_CERT_FILE, AWS_CA_BUNDLE, REQUESTS_CA_BUNDLE, NODE_EXTRA_CA_CERTS, and CURL_CA_BUNDLE.
The bundle contains 119 certificates — the public CAs plus Floci's CA. HTTPS requests from Lambda to public sites also returned 200.
The AWS_ENDPOINT_URL passed to Lambda still pointed to http:// even with TLS enabled. Clients using this value connect via HTTP.
Floci certificates cannot be verified with Python 3.13's default settings
I made a request from inside Lambda to https://localhost.floci.io:4566/_floci/health using urllib. The python3.12 runtime returned 200. With python3.13, it results in CERTIFICATE_VERIFY_FAILED: Missing Authority Key Identifier. The same result occurs with Python 3.13 on the host.
import ssl, urllib.request
ctx = ssl.create_default_context(cafile="floci-root-ca.pem")
print(urllib.request.urlopen("https://localhost:4566/_floci/health", context=ctx).status)
In Python 3.13, ssl.create_default_context() enables VERIFY_X509_STRICT by default. Looking at Floci-issued certificates with openssl x509 -text, both key identifier extensions were absent: Authority Key Identifier (AKI) and Subject Key Identifier (SKI). This applies to the server certificate, the CA certificate, and ACM-issued certificates alike.
To determine which extensions are required, I created four combinations with a test CA and tested connections with Python 3.13.
| CA SKI | Server cert AKI | Result |
|---|---|---|
| None | None | Missing Authority Key Identifier |
| None | Present | Missing Subject Key Identifier |
| Present | None | Missing Authority Key Identifier |
| Present | Present | OK |
Adding only AKI to the server certificate was not enough; the CA certificate also needed SKI. Since existing persisted CAs lack SKI, even if new certificates are issued with the extension, the error will remain in existing environments.
I filed this as an issue (#4621). A fix PR (#4644) was submitted by someone else the same day. The PR proposes reissuing the certificate with the same key for the existing CA. Since the CA fingerprint changes, if you connect from Python 3.13 you will need to re-fetch ca.pem. Verification succeeds with curl --cacert, the AWS CLI, and Python 3.12. In Python 3.13 as well, removing VERIFY_X509_STRICT from the SSL context returned 200. Until the fix is included, connection options include HTTP and specifying a custom certificate via FLOCI_TLS_CERT_PATH.
Other updates
The following table lists updates noted in the release notes that I did not run locally.
| Category | Content | Version |
|---|---|---|
| IAM | SCP evaluation and aws:PrincipalArn (#2637). Both IAM and Organizations enforcement flags required |
2.0.0 |
| Lambda | Runner using Kubernetes as the execution platform (#1941) | 2.0.0 |
| Step Functions | Service integration mocking via SFN_MOCK_CONFIG (#2452), Retry (#2455) |
2.0.0 |
| S3 | Option to enable a global bucket namespace (#2640) | 2.0.0 |
| CloudFormation | Custom resources via CDK Provider Framework (#2688) | 2.0.0 |
| EC2 | Create Docker network per VPC on instance launch (#3272). VPC peering connection management API (#2654) | 2.1.0 |
| IoT | MQTT over TLS (#3142) and WebSocket (#3150), device certificate issuance and verification (#3079, #3180) | 2.1.0 |
| Redshift | COPY FROM s3 (#3100) and UNLOAD TO s3 (#3129), Data API (#3186) |
2.1.0 |
| Firehose | Parquet conversion of JSON records (#3333), Lambda-based transformation (#3393) | 2.1.0 |
| Bedrock | ConverseStream implementation (#2889). InvokeModelWithResponseStream remains 501 |
2.1.0 |
| Web console | Configuration for Floci to launch the console as a sidecar, and publication of the console-side contract (#3641) | 2.1.0 |
Enabling the S3 option allows buckets from other accounts to be referenced by name. Since both IAM enforcement mode and S3 authentication are disabled by default, enabling only this option removes the cross-account isolation confirmed in the previous article. To control access with policies, also enable FLOCI_SERVICES_IAM_ENFORCEMENT_ENABLED and FLOCI_SERVICES_S3_ENFORCE_AUTH.
Summary
When updating to 2.0.0 or later, existing tests may be affected by JSONata top-level references and Logs Insights queries using like or stats. In both cases, things that previously passed will now result in errors. If IAM enforcement mode is enabled, operations not permitted by managed policies will now be denied.
After 2.1.0, as of September 29, 644 commits have accumulated on main. AppSync resolvers are among them, but the September 28 nightly returned an error. I plan to check on the progress of #4621 and #4717 once the next release is out.
The figures and release content in this article are based on the following primary sources.
- floci-io/floci (GitHub repository)
- Releases (release notes for 2.0.0, 2.0.1, 2.1.0)
- Floci official documentation
- Operational verification was performed on
floci/floci:2.1.0. For comparison with previous behavior,floci/floci:1.7.0was used; for AppSync resolver verification,floci/floci:nightly-09282026was used.
