Skip to content

Latest commit

 

History

History
492 lines (365 loc) · 30.6 KB

File metadata and controls

492 lines (365 loc) · 30.6 KB

S3

Protocol: REST XML Endpoint: http://localhost:4566/{bucket}/{key}

Supported Operations

Category Operations
Buckets ListBuckets, CreateBucket, HeadBucket, DeleteBucket, GetBucketLocation
Objects PutObject, GetObject, GetObjectAttributes, HeadObject, DeleteObject, DeleteObjects, CopyObject
Listing ListObjects, ListObjectsV2, ListObjectVersions
Multipart CreateMultipartUpload, UploadPart, CompleteMultipartUpload, AbortMultipartUpload, ListMultipartUploads
Versioning PutBucketVersioning, GetBucketVersioning
Tagging PutBucketTagging, GetBucketTagging, PutObjectTagging, GetObjectTagging, DeleteObjectTagging
Annotations PutObjectAnnotation, GetObjectAnnotation, ListObjectAnnotations, DeleteObjectAnnotation
Policy PutBucketPolicy, GetBucketPolicy, DeleteBucketPolicy
CORS PutBucketCors, GetBucketCors, DeleteBucketCors
Lifecycle PutBucketLifecycle, GetBucketLifecycle, DeleteBucketLifecycle
ACL PutBucketAcl, GetBucketAcl, PutObjectAcl, GetObjectAcl
Encryption PutBucketEncryption, GetBucketEncryption, DeleteBucketEncryption
Notifications PutBucketNotification, GetBucketNotification
Object Lock PutObjectLockConfiguration, GetObjectLockConfiguration, PutObjectRetention, GetObjectRetention, PutObjectLegalHold, GetObjectLegalHold
Website PutBucketWebsite, GetBucketWebsite, DeleteBucketWebsite
Pre-signed URLs Generates and validates pre-signed GET/PUT URLs
S3 Select SelectObjectContent
Public Access Block PutPublicAccessBlock, GetPublicAccessBlock, DeletePublicAccessBlock
Metrics PutBucketMetricsConfiguration, GetBucketMetricsConfiguration, ListBucketMetricsConfigurations, DeleteBucketMetricsConfiguration
Intelligent-Tiering PutBucketIntelligentTieringConfiguration, GetBucketIntelligentTieringConfiguration, ListBucketIntelligentTieringConfigurations, DeleteBucketIntelligentTieringConfiguration
Analytics PutBucketAnalyticsConfiguration, GetBucketAnalyticsConfiguration, ListBucketAnalyticsConfigurations, DeleteBucketAnalyticsConfiguration
Inventory PutBucketInventoryConfiguration, GetBucketInventoryConfiguration, ListBucketInventoryConfigurations, DeleteBucketInventoryConfiguration
Replication PutBucketReplication, GetBucketReplication, DeleteBucketReplication

RestoreObject is accepted but stubbed: Floci validates the request and returns 202 Accepted, but no restore state machine runs.

Replication is configuration-only: the replication configuration is stored on the bucket and round-trips through GetBucketReplication, but no objects are actually replicated.

Event Notifications

Browser and presigned POST uploads emit s3:ObjectCreated:Post, matching AWS S3. They do not emit s3:ObjectCreated:Put; use s3:ObjectCreated:* to subscribe to objects created by either method. Annotation changes emit s3:ObjectAnnotation:Put and s3:ObjectAnnotation:Delete.

Object Annotations

Annotations are named UTF-8 text payloads (up to 1 MiB each, 1,000 per object version) attached to a specific object version through the four ?annotation operations. Notes on the emulation:

  • Annotation names allow letters (any language), digits, _, ., and -; names longer than 512 bytes, empty or whitespace-only names, names with other characters, and names starting with aws or s3 (case-insensitive) are rejected.
  • Payloads must be valid UTF-8 text between 1 byte and 1 MiB; anything else is rejected with 400, and non-UTF-8 payloads return 415 UnsupportedMediaType.
  • x-amz-object-if-match is validated against the parent object's ETag on put and delete.
  • Versioning semantics match AWS: annotations attach to one object version, new versions do not inherit them, overwriting a non-versioned object or deleting it drops its annotations, a delete marker preserves the underlying version's annotations, and deleting a specific version deletes its annotations. Annotation deletion is permanent.
  • CopyObject copies annotations by default; the x-amz-object-annotation-directive header (as the AWS SDK sends it; x-amz-annotation-directive is also accepted) set to EXCLUDE skips them.
  • Checksums are per-annotation and independent of the object checksum. The default algorithm is CRC64NVME. Supported: CRC32, CRC32C, CRC64NVME, SHA1, SHA256. SHA512, XXHASH64, XXHASH3, XXHASH128, and MD5 are rejected as unsupported.
  • Annotations on SSE-C encrypted objects are rejected, as on AWS, and are not copied onto SSE-C copy destinations.
  • Annotation operations serialize against object writes on the same bucket. On Object Lock-protected versions, annotation put and delete follow the same rules as object delete: governance retention requires x-amz-bypass-governance-retention (put never takes the bypass), compliance and legal hold always block.
  • The literal versionId=null (as reported by ListObjectVersions for pre-versioning objects) addresses the pre-versioning entry.
  • Annotations are stored per AWS account. With globalBucketNamespace enabled, object reads resolve cross-account but annotation reads, writes, and listings stay in the caller's account.
  • S3 Metadata annotation tables and annotation replication are not implemented.

Website Hosting

Static website requests use a bucket website hostname such as http://my-bucket.s3-website-us-east-1.localhost:4566/. Floci supports:

  • GET and HEAD requests through website hostnames
  • index-document resolution for the site root and slash-terminated prefixes
  • 302 redirects that append a slash when an index document exists below a prefix
  • configured error documents and the default website error response

HEAD returns the same status and object metadata as GET without a response body. Redirect-all and advanced routing-rule configurations are not implemented.

Range Reads

GetObject supports standard Range: bytes= requests. Partial responses return 206, Content-Range, Content-Length, Accept-Ranges: bytes, and object metadata. Whole-object checksum headers are omitted on ranged responses because the stored checksum covers the full object, not the returned byte slice. Full-object and ranged reads stream object bytes from storage instead of materializing the response body first.

Suffix ranges against empty objects return an empty 200 response, matching AWS behavior used by clients that issue tail reads.

S3 Select

SelectObjectContent runs SQL queries directly against S3 objects without downloading the entire file. Floci supports CSV, JSON Lines, JSON arrays, and Parquet inputs. The SQL dialect follows AWS S3 Select SQL reference.

Execution modes

Floci chooses the execution engine automatically based on the input format and whether the floci-duck sidecar is running:

Condition Engine Notes
Input is Parquet floci-duck (required) DuckDB's read_parquet — sidecar must be available
Input is CSV with FileHeaderInfo=USE and floci-duck is running floci-duck Full DuckDB SQL: all operators, LIKE, BETWEEN, IN, IS NULL, AND/OR/NOT
Input is JSON and floci-duck is running floci-duck read_json_auto — supports JSON Lines and JSON arrays
Input is CSV with FileHeaderInfo=NONE or IGNORE, or floci-duck is not running Java evaluator Supports SELECT *, column projection, simple WHERE with =, !=, <, >, <=, >=, LIKE, BETWEEN, IN, IS NULL, AND/OR/NOT, LIMIT

The floci-duck sidecar starts lazily on the first Athena query. Until then, isAvailable() returns false and S3 Select falls back to the Java evaluator for CSV and JSON. Once the sidecar is running, subsequent S3 Select calls route through DuckDB automatically.

If floci-duck is not running and the object is Parquet, S3 Select returns an error — Parquet decoding requires DuckDB.

FileHeaderInfo modes (CSV)

Value Behavior
USE First row is the header; column names are available in WHERE and SELECT
IGNORE First row is skipped and not included in output; only positional _N references work
NONE All rows are data; only positional _N references work (e.g. WHERE _1 = 'Alice')

Supported SQL operators

When using the Java evaluator (no floci-duck, or CSV with FileHeaderInfo=NONE/IGNORE):

  • Comparison: =, !=, <>, <, >, <=, >=
  • Pattern matching: LIKE (supports % and _ wildcards)
  • Range: BETWEEN ... AND ...
  • Set membership: IN (...)
  • Null checks: IS NULL, IS NOT NULL
  • Logical: AND, OR, NOT
  • Clauses: SELECT *, column projection, LIMIT

Output formats

S3 Select supports cross-format output: a CSV object can produce JSON output and vice versa.

Input Output Format
CSV CSV Default — comma-separated values
CSV JSON One JSON object per row: {"col1":"val1","col2":"val2"}
JSON JSON Default — one JSON object per line
JSON CSV Comma-separated values, values quoted when they contain commas or newlines

Example

export AWS_ENDPOINT_URL=http://localhost:4566

# Upload a CSV file
printf 'name,age,city\nAlice,30,New York\nBob,25,\nCharlie,35,London\n' \
  | aws s3 cp - s3://my-bucket/people.csv

# Query with WHERE and column projection
aws s3api select-object-content \
  --bucket my-bucket \
  --key people.csv \
  --expression "SELECT name, city FROM S3Object WHERE age >= 30" \
  --expression-type SQL \
  --input-serialization '{"CSV":{"FileHeaderInfo":"USE"}}' \
  --output-serialization '{"CSV":{}}' \
  /dev/stdout

# IS NULL check
aws s3api select-object-content \
  --bucket my-bucket \
  --key people.csv \
  --expression "SELECT name FROM S3Object WHERE city IS NULL" \
  --expression-type SQL \
  --input-serialization '{"CSV":{"FileHeaderInfo":"USE"}}' \
  --output-serialization '{"CSV":{}}' \
  /dev/stdout

# JSON Lines input
printf '{"name":"Alice","score":95}\n{"name":"Bob","score":72}\n' \
  | aws s3 cp - s3://my-bucket/scores.json

aws s3api select-object-content \
  --bucket my-bucket \
  --key scores.json \
  --expression "SELECT * FROM S3Object WHERE score > 80" \
  --expression-type SQL \
  --input-serialization '{"JSON":{"Type":"LINES"}}' \
  --output-serialization '{"JSON":{}}' \
  /dev/stdout

Mock mode note

When FLOCI_SERVICES_ATHENA_MOCK=true is set, Athena queries are stubbed but floci-duck does not start. In that configuration, S3 Select uses the Java evaluator for CSV and JSON. Parquet queries will fail unless FLOCI_SERVICES_DUCK_URL points to an already-running floci-duck instance.

Bucket Names

Floci does not hold bucket names to AWS's full DNS naming rules, so a name AWS would refuse is usually accepted here. The exception is a name that would not stay a single directory under its account's storage root: an empty name, ., .., a name containing a path separator, or one of Floci's reserved storage directories (.accounts, .versions, .annotations) is rejected with InvalidBucketName. On the persistent and hybrid backends such a name either climbs out of the owning account's directory or collides with object version and annotation data.

Global Bucket Namespace

By default S3 buckets are isolated per account (see Multi-Account Isolation), so two accounts can each hold a bucket with the same name. Set FLOCI_SERVICES_S3_GLOBAL_BUCKET_NAMESPACE=true to make bucket and object resolution span every account's partition — a bucket created in one account then resolves cross-account, matching real S3 where bucket names are globally unique. Enable this when a workload (for example an LZA log-archive or CDK asset bucket) is created in one account and read from another.

Why the flag exists, and what it trades away. Floci partitions every resource by the caller's account, which is the right model for services whose names are account-scoped in AWS. S3 bucket names are not: they are unique across all accounts, a bucket lives in exactly one owning account, and whether another account may touch it is decided by policy rather than by the name being unreachable. A hard per-account partition therefore lets two accounts hold different buckets under one name — a state AWS cannot reach — and makes a legitimate cross-account call fail with NoSuchBucket instead of being authorized or denied on its merits. With the flag on, a caller that names another account's bucket resolves it and only IAM and bucket-policy evaluation stand between the caller and the object: AWS-faithful, but stricter than the implicit isolation Floci gives you elsewhere — provided both FLOCI_SERVICES_IAM_ENFORCEMENT_ENABLED (identity-policy checks, applied before the request reaches S3) and FLOCI_SERVICES_S3_ENFORCE_AUTH (the S3-specific flag that gates bucket-policy evaluation itself, in S3Service.authorizeS3Read/authorizeS3Write) are also true. Both default to false, and bucket-policy evaluation in particular is a no-op while FLOCI_SERVICES_S3_ENFORCE_AUTH is off — regardless of the IAM flag — so in the default configuration enabling the global namespace does not trade per-account isolation for a policy-gated boundary — it removes the only isolation Floci gives S3 for that bucket and replaces it with nothing. Mutations are written back to the bucket's owning account rather than forked into the caller's, and ListBuckets/ListObjects stay owner-scoped, so the flag never reassigns ownership or leaks a bucket inventory. It is off by default — turn it on when emulating a multi-account estate whose real behaviour depends on the global namespace, and leave it off when the per-account partition is the isolation you are relying on. Account-scoped S3 state, notably the account-level Block Public Access configuration, is never widened by this flag. Note also that retrofitting a global namespace onto state that was previously partitioned per account means that if two accounts already hold a bucket under the same name — a state real AWS can't reach, but Floci's prior per-account isolation could — resolution picks whichever account's copy the backend happens to iterate to first; this is unlikely to be reachable on a fresh LZA-driven account structure, but is worth knowing if you enable the flag on an estate with pre-existing same-name buckets.

Bucket Policy Enforcement

Floci evaluates S3 bucket policies for authenticated (signed) requests when policy enforcement is enabled. Two flags control this evaluation:

  • FLOCI_SERVICES_S3_ENFORCE_AUTH: gates bucket policy authorization directly within S3Service for bucket read, object write, and bucket write operations.
  • FLOCI_SERVICES_IAM_ENFORCEMENT_ENABLED: activates the global IAM request filter to evaluate identity-based policies and resource policies before requests reach the service layer.

Both flags default to false for backward compatibility.

Evaluated Policies and Operations

When FLOCI_SERVICES_S3_ENFORCE_AUTH or FLOCI_SERVICES_IAM_ENFORCEMENT_ENABLED is active:

  • The bucket's attached policy document (configured via PutBucketPolicy) is retrieved.
  • The caller's authenticated principal ARN is resolved from the signing credentials (for example, arn:aws:iam::<account>:user/<name>, arn:aws:iam::<account>:role/<name>, or arn:aws:iam::<account>:root).
  • Policy statements are evaluated for the requested S3 action (e.g. s3:GetObject, s3:PutObject, s3:PutBucketPolicy) and resource ARN (arn:aws:s3:::bucket or arn:aws:s3:::bucket/key).

Supported Principal Types

Bucket policy statements can specify principals using Principal or NotPrincipal with scalar strings or arrays:

  • Wildcard: "*" or {"AWS": "*"} matches any caller.
  • IAM User: {"AWS": "arn:aws:iam::<account>:user/<name>"}.
  • IAM Role: {"AWS": "arn:aws:iam::<account>:role/<name>"} (assumed-role sessions also match the underlying role ARN).
  • AWS Account: {"AWS": "<12-digit-account-id>"} or {"AWS": "arn:aws:iam::<account>:root"} matches any principal belonging to the specified account.
  • Service Principal: {"Service": "<service>.amazonaws.com"}.
  • NotPrincipal: Inverts matching so the statement applies to any principal not matching the specified patterns.

Evaluation Rules

  1. An explicit Deny always wins and rejects the request.
  2. An explicit Allow grants access.
  3. For bucket owners (principals within the account that owns the bucket), the default decision is Allow unless explicitly denied.
  4. For non-owner principals, an explicit Allow in the bucket policy is required; neutral evaluation results in Deny.
  5. Unsigned (anonymous) requests and ACL-based public access fall back to standard S3 ACL and anonymous authorization paths.
  6. Unknown access key IDs return HTTP 403 with InvalidAccessKeyId.

Wire Response Shape

When a request is denied by bucket policy enforcement, Floci returns HTTP 403 with an S3 XML error body including the <Resource> element:

<Error>
    <Code>AccessDenied</Code>
    <Message>Access Denied</Message>
    <Resource>/my-bucket/my-key</Resource>
    <RequestId>...</RequestId>
</Error>

Block Public Access

Floci enforces all four Block Public Access settings. A bucket's configuration is combined with the bucket owner account's and the most restrictive of the two wins, which for four independent booleans is a per-flag OR: a flag set at either level is in force.

Setting What it does Needs enforce-auth
BlockPublicPolicy PutBucketPolicy returns AccessDenied (403) when the submitted policy is public No
BlockPublicAcls PutBucketAcl, PutObjectAcl and a PutObject carrying a public ACL return AccessDenied (403) No
RestrictPublicBuckets A bucket whose policy is public serves only the owner account: anonymous and cross-account callers are denied Yes
IgnorePublicAcls A public ACL on the bucket or on an object stops granting access Yes

The split follows AWS. The first two reject the write that would introduce public access, and AWS applies them whoever the caller is, so they need no flag: put-public-access-block followed by a public put-bucket-policy returns 403 in Floci's default configuration. The second two suppress access that an existing policy or ACL would grant, which only has meaning once anonymous authorization runs at all, so they take effect when FLOCI_SERVICES_S3_ENFORCE_AUTH is on. With that flag off, Floci cannot tell an anonymous caller from a signed one and every request is authorized regardless.

When upgrading, a workflow that already sets BlockPublicPolicy or BlockPublicAcls may now receive 403 on a later public policy or ACL write, even with enforce-auth off. This matches AWS behavior.

As in AWS, enabling a setting never rewrites stored state. An existing public policy or ACL stays exactly as written, and clearing the setting makes the bucket public again.

The meaning of "public"

An ACL is public when it grants any permission to the AllUsers or AuthenticatedUsers predefined groups. This is broader than the grants anonymous authorization consults: a WRITE_ACP grant to AllUsers is public here even though it gives an anonymous caller no read.

A bucket policy is assumed public and then checked for a reason it is not. A statement with a wildcard principal ("Principal": "*", {"AWS": "*"}, or an Allow on NotPrincipal) is public unless a positive condition pins one of aws:PrincipalArn, aws:PrincipalAccount, aws:PrincipalOrgID, aws:PrincipalOrgPaths, aws:SourceArn, aws:SourceVpc, aws:SourceVpce, aws:SourceOwner, aws:SourceAccount, aws:userid, s3:DataAccessPointArn or s3:DataAccessPointAccount to a fixed value, one containing neither a wildcard nor an IAM policy variable. In a bucket policy, s3:DataAccessPointArn may contain a wildcard in the access point name if the account ID is fixed, for example arn:aws:s3:us-west-2:123456789012:accesspoint/*. aws:SourceIp counts too, but only for a range no broader than /8 (IPv4) or /32 (IPv6), matching AWS's treatment of very wide CIDR blocks as public.

For multivalued condition keys such as aws:PrincipalOrgPaths, ForAnyValue: with a positive operator can narrow the grant when each value is fixed. ForAllValues: alone does not narrow it because the condition also matches when the key is missing.

A single public statement makes the whole policy public. As on AWS, RestrictPublicBuckets then withholds even the non-public cross-account delegation another statement grants, until the public statement is removed.

Known gaps

  • Floci does not model access points, so the access-point variants (PutAccessPointPolicy, the VPC-origin rule, the different s3:DataAccessPointArn treatment) do not apply.
  • AWS's organization-level Block Public Access policies are not modelled; only the bucket and account levels combine.
  • CreateBucket does not apply an x-amz-acl at all in Floci, so there is no public bucket ACL at creation for the account-level BlockPublicAcls to reject.
  • GetBucketAcl returns the stored ACL rather than the effective one, so it does not reflect an IgnorePublicAcls that is suppressing a grant.
  • GetBucketPolicyStatus has no handler.
  • RestrictPublicBuckets exempts AWS service principals on AWS. Floci reaches the signed path only for IAM principals, so there is nothing to exempt.

Account-level operations

The s3control PutPublicAccessBlock, GetPublicAccessBlock and DeletePublicAccessBlock operations are keyed by the x-amz-account-id header, stored per account, and persisted like the rest of S3's state so a restart does not silently drop the control. As in AWS, the header must name the caller's own account; a mismatch returns AccessDenied (403). Floci keeps one deliberate exception: the configured default (management) account may act on any account, because Floci's launched Lambdas run on placeholder credentials that resolve to the management account instead of assuming a role in the target account — which is how LZA's Custom::PutPublicAccessBlock resource reaches this API.

Not Implemented

These AWS S3 features have no handler in Floci. Calls will return an error (typically 404 or NoSuchBucket-style):

  • Access logging (PutBucketLogging, GetBucketLogging)
  • Request payment (PutBucketRequestPayment, GetBucketRequestPayment)

Configuration

Variable Default Description
FLOCI_SERVICES_S3_ENABLED true Enable or disable the service
FLOCI_SERVICES_S3_DEFAULT_PRESIGN_EXPIRY_SECONDS 3600 Default pre-signed URL expiry (1 hour)
FLOCI_AUTH_PRESIGN_SECRET local-emulator-secret Secret used to sign pre-signed URLs
FLOCI_SERVICES_S3_ENFORCE_AUTH false Enable bucket policy evaluation for authenticated callers
FLOCI_SERVICES_S3_GLOBAL_BUCKET_NAMESPACE false Enable cross-account bucket and object resolution

Examples

export AWS_ENDPOINT_URL=http://localhost:4566

# Create bucket
aws s3 mb s3://my-bucket --endpoint-url $AWS_ENDPOINT_URL

# Upload a file
aws s3 cp ./report.pdf s3://my-bucket/reports/report.pdf --endpoint-url $AWS_ENDPOINT_URL

# Upload inline content
echo '{"hello":"world"}' | aws s3 cp - s3://my-bucket/data.json --endpoint-url $AWS_ENDPOINT_URL

# Download
aws s3 cp s3://my-bucket/data.json ./data.json --endpoint-url $AWS_ENDPOINT_URL

# Inspect object attributes without downloading the body
aws s3api get-object-attributes \
  --bucket my-bucket \
  --key data.json \
  --object-attributes ETag ObjectSize StorageClass \
  --endpoint-url $AWS_ENDPOINT_URL

# List
aws s3 ls s3://my-bucket --endpoint-url $AWS_ENDPOINT_URL

# Delete
aws s3 rm s3://my-bucket/data.json --endpoint-url $AWS_ENDPOINT_URL

# Enable versioning
aws s3api put-bucket-versioning \
  --bucket my-bucket \
  --versioning-configuration Status=Enabled \
  --endpoint-url $AWS_ENDPOINT_URL

# Generate a pre-signed URL (valid for 1 hour)
aws s3 presign s3://my-bucket/report.pdf \
  --expires-in 3600 \
  --endpoint-url $AWS_ENDPOINT_URL

Addressing Styles

Floci supports both path-style and virtual-hosted style S3 addressing.

Path-Style (always works)

Path-style embeds the bucket name in the URL path:

http://localhost:4566/my-bucket/my-key

Enable it in the SDK with forcePathStyle / pathStyleAccessEnabled:

=== "Java"

```java
S3Client s3 = S3Client.builder()
    .endpointOverride(URI.create("http://localhost:4566"))
    .forcePathStyle(true)
    .build();
```

=== "Node.js"

```javascript
const s3 = new S3Client({
  endpoint: "http://localhost:4566",
  forcePathStyle: true,
});
```

=== "Python"

```python
s3 = boto3.client(
    "s3",
    endpoint_url="http://localhost:4566",
    config=Config(s3={"addressing_style": "path"}),
)
```

Virtual-Hosted Style

Virtual-hosted style puts the bucket name in the hostname:

http://my-bucket.s3.localhost.floci.io:4566/my-key

Floci supports this natively — no forcePathStyle needed. The following wildcard DNS domains resolve to 127.0.0.1 via public DNS, so virtual-hosted requests reach Floci on the host machine automatically:

Domain pattern Resolves to
*.localhost.floci.io 127.0.0.1
*.s3.localhost.floci.io 127.0.0.1
*.localhost.localstack.cloud 127.0.0.1
*.s3.localhost.localstack.cloud 127.0.0.1

Plain http://localhost:4566 also works without forcePathStyle — the SDK sends Host: my-bucket.localhost:4566 and Floci's filter extracts the bucket name from that header.

Configure the SDK endpoint to one of the base domains (no forcePathStyle):

=== "Java"

```java
S3Client s3 = S3Client.builder()
    .endpointOverride(URI.create("http://s3.localhost.floci.io:4566"))
    .region(Region.US_EAST_1)
    .credentialsProvider(StaticCredentialsProvider.create(
        AwsBasicCredentials.create("test", "test")))
    .build();
// SDK sends requests to: my-bucket.s3.localhost.floci.io:4566
```

=== "Node.js"

```javascript
const s3 = new S3Client({
  endpoint: "http://s3.localhost.floci.io:4566",
  // no forcePathStyle — SDK uses virtual-hosted style by default
});
// SDK sends requests to: my-bucket.s3.localhost.floci.io:4566
```

=== "Python"

```python
s3 = boto3.client("s3", endpoint_url="http://s3.localhost.floci.io:4566")
# SDK sends requests to: my-bucket.s3.localhost.floci.io:4566
```

Virtual-Hosted Style Inside Docker

Inside Docker containers, 127.0.0.1 resolves to the container itself — not Floci. Floci's embedded DNS server handles this automatically: it resolves *.localhost.floci.io, *.s3.localhost.floci.io, *.localhost.localstack.cloud, and *.s3.localhost.localstack.cloud to Floci's container IP on the Docker network.

To use virtual-hosted style from a test container, point its DNS at Floci and set the endpoint to Floci's service hostname:

FLOCI_IP=$(docker inspect -f '{{.NetworkSettings.Networks.floci_default.IPAddress}}' floci)

docker run --rm \
  --network floci_default \
  --dns "$FLOCI_IP" \
  -e FLOCI_ENDPOINT=http://floci:4566 \
  -e FLOCI_S3_VHOST_ENDPOINT=http://floci:4566 \
  my-test-image

With FLOCI_HOSTNAME=floci set on the Floci container (default in the provided docker-compose.yml), the embedded DNS resolves my-bucket.floci to Floci's IP, and S3VirtualHostFilter extracts the bucket name from the Host header.

Object Attribute Notes

Floci now persists and returns the following object attribute state on S3 object APIs:

  • user metadata from x-amz-meta-*
  • storage class from x-amz-storage-class
  • checksum metadata for object reads and GetObjectAttributes
  • multipart part manifests for GetObjectAttributes(ObjectParts)
  • multipart checksums follow the AWS checksum type: COMPOSITE (<checksum of the part checksums>-<part count>) for SHA1, SHA256 and, by default, CRC32 and CRC32C; FULL_OBJECT for CRC64NVME or when x-amz-checksum-type: FULL_OBJECT was requested on CreateMultipartUpload. The -N suffix is returned by CompleteMultipartUpload, HeadObject and GetObject, and omitted by GetObjectAttributes, which lists part-level checksums only for COMPOSITE objects (a FULL_OBJECT multipart object reports its PartsCount alone, a single-part object has no ObjectParts), as on AWS. UploadPart echoes the part checksum (x-amz-checksum-<algorithm>) when the upload declared an algorithm, and completing a COMPOSITE upload requires the checksum of every part in the CompleteMultipartUpload body (InvalidRequest otherwise), while FULL_OBJECT uploads accept a body without them
  • canned object ACLs from x-amz-acl on PutObject, CopyObject, and multipart initiation
  • explicit object SSE headers from x-amz-server-side-encryption on PutObject, CopyObject, and multipart initiation, replayed on GetObject and HeadObject
  • SSE-KMS key IDs from x-amz-server-side-encryption-aws-kms-key-id on PutObject and multipart initiation, and from CopyObject/UploadPartCopy (inherited from the source object when the request doesn't specify new SSE headers), returned by PutObject, CopyObject, UploadPartCopy, CompleteMultipartUpload, GetObject, and HeadObject. A bucket's default SSE-KMS key is not simulated: aws:kms with no explicit key ID omits the header rather than returning a synthesized aws/s3 key ARN

Current limitations:

  • checksum algorithms: CRC32, CRC32C, CRC64NVME, SHA1 and SHA256 are supported; the other AWS algorithms (SHA512, MD5, XXHASH3, XXHASH64, XXHASH128) are rejected with InvalidRequest
  • copy-based metadata updates support x-amz-metadata-directive: REPLACE for user metadata and content type, but do not yet cover every AWS copy header
  • explicit ACL grant headers such as x-amz-grant-read and x-amz-grant-full-control are not modeled yet
  • cross-account canned ACL variants collapse to the emulator's single synthetic owner where Floci does not model a distinct second principal
  • aws-exec-read is accepted for compatibility, but Floci does not yet model a distinct EC2 bundle-reader grantee in GetObjectAcl
  • log-delivery-write grants the well-known S3 log-delivery group (http://acs.amazonaws.com/groups/s3/LogDelivery) WRITE and READ_ACP, matching AWS on the bucket path. Floci's shared canned-ACL helper also accepts it on PutObjectAcl, where AWS does not: the canned ACL is documented as bucket-only and the SDK's ObjectCannedACL enum omits it. That object path is a Floci leniency, not parity. It is the mirror image of aws-exec-read above, which botocore's BucketCannedACL enum omits, though AWS's canned-ACL reference documents it as valid for both bucket and object; there the narrower party is the SDK model, not AWS