IAM
Protocol: Query (XML) — POST http://localhost:4566/ with Action= parameter
AWS Sign-In login credentials
Floci also implements the AWS Sign-In data-plane flow used by the AWS CLI login credentials
provider. The local GET /v1/authorize endpoint performs the emulator's local sign-in and returns
a one-time PKCE authorization code. POST /v1/token exchanges that code (or a refresh token) for
15-minute temporary SigV4 credentials registered with the local IAM account.
aws login --endpoint-url http://localhost:4566 --region us-east-1
aws sts get-caller-identity --endpoint-url http://localhost:4566
The authorize endpoint first presents a local consent page, then redirects back to the callback
listener owned by the AWS CLI. Floci does not contact AWS or require a real AWS account. This
follows the AWS Sign-In AuthorizeOAuth2Access and CreateOAuth2Token wire shapes, including
one-time codes, PKCE verification, refresh-token expiry, and the aws_sigv4 temporary credential
type.
Supported Actions
Users
| Action | Description |
|---|---|
| CreateUser | Creates an IAM user in the local account. |
| GetUser | Returns a stored IAM user. |
| DeleteUser | Deletes an IAM user from the local IAM store. |
| ListUsers | Lists IAM users in the local account. |
| UpdateUser | Updates mutable IAM user fields. |
| TagUser | Adds tags to an IAM user. |
| UntagUser | Removes tags from an IAM user. |
| ListUserTags | Lists tags stored for an IAM user. |
Groups
| Action | Description |
|---|---|
| CreateGroup | Creates an IAM group. |
| GetGroup | Returns an IAM group and its users. |
| UpdateGroup | Renames a group and/or changes its path; ARN and stored users move with it. |
| DeleteGroup | Deletes an IAM group from the local IAM store. |
| ListGroups | Lists IAM groups in the local account. |
| AddUserToGroup | Adds a user to an IAM group. |
| RemoveUserFromGroup | Removes a user from an IAM group. |
| ListGroupsForUser | Lists groups that contain a user. |
Roles
| Action | Description |
|---|---|
| CreateRole | Creates an IAM role with an assume-role policy. |
| GetRole | Returns a stored IAM role. |
| DeleteRole | Deletes an IAM role from the local IAM store. |
| ListRoles | Lists IAM roles in the local account. |
| UpdateRole | Updates mutable IAM role fields. |
| CreateServiceLinkedRole | Creates a role under /aws-service-role/ for a service principal. |
| DeleteServiceLinkedRole | Deletes a service-linked role and returns a deletion task id. |
| GetServiceLinkedRoleDeletionStatus | Returns the status of a service-linked role deletion. |
| UpdateAssumeRolePolicy | Replaces a role's assume-role policy document. |
| TagRole | Adds tags to an IAM role. |
| UntagRole | Removes tags from an IAM role. |
| ListRoleTags | Lists tags stored for an IAM role. |
Policies
| Action | Description |
|---|---|
| CreatePolicy | Creates a customer-managed IAM policy. |
| GetPolicy | Returns metadata for a managed IAM policy. |
| DeletePolicy | Deletes a managed IAM policy. |
| ListPolicies | Lists managed IAM policies, including seeded AWS managed policies. |
| ListEntitiesForPolicy | Lists roles, users, and groups with a direct managed-policy attachment. |
| CreatePolicyVersion | Creates a new version of a managed policy. |
| GetPolicyVersion | Returns a managed policy version document. |
| DeletePolicyVersion | Deletes a non-default managed policy version. |
| ListPolicyVersions | Lists versions for a managed policy. |
| SetDefaultPolicyVersion | Sets the default version for a managed policy. |
| TagPolicy | Adds tags to a managed policy. |
| UntagPolicy | Removes tags from a managed policy. |
| ListPolicyTags | Lists tags stored for a managed policy. |
ListEntitiesForPolicy currently returns direct permissions-policy attachments. EntityFilter,
PathPrefix, PolicyUsageFilter, and pagination are not yet applied; responses return
IsTruncated=false.
Permission Boundaries
| Action | Description |
|---|---|
| PutUserPermissionsBoundary | Sets a managed policy as a user's permissions boundary. |
| DeleteUserPermissionsBoundary | Removes a user's permissions boundary. |
| PutRolePermissionsBoundary | Sets a managed policy as a role's permissions boundary. |
| DeleteRolePermissionsBoundary | Removes a role's permissions boundary. |
Policy Attachments
| Action | Description |
|---|---|
| AttachUserPolicy | Attaches a managed policy to a user. |
| DetachUserPolicy | Detaches a managed policy from a user. |
| ListAttachedUserPolicies | Lists managed policies attached to a user. |
| AttachGroupPolicy | Attaches a managed policy to a group. |
| DetachGroupPolicy | Detaches a managed policy from a group. |
| ListAttachedGroupPolicies | Lists managed policies attached to a group. |
| AttachRolePolicy | Attaches a managed policy to a role. |
| DetachRolePolicy | Detaches a managed policy from a role. |
| ListAttachedRolePolicies | Lists managed policies attached to a role. |
Inline Policies
| Action | Description |
|---|---|
| PutUserPolicy | Stores or replaces an inline policy on a user. |
| GetUserPolicy | Returns an inline policy stored on a user. |
| DeleteUserPolicy | Deletes an inline policy from a user. |
| ListUserPolicies | Lists inline policy names stored on a user. |
| PutGroupPolicy | Stores or replaces an inline policy on a group. |
| GetGroupPolicy | Returns an inline policy stored on a group. |
| DeleteGroupPolicy | Deletes an inline policy from a group. |
| ListGroupPolicies | Lists inline policy names stored on a group. |
| PutRolePolicy | Stores or replaces an inline policy on a role. |
| GetRolePolicy | Returns an inline policy stored on a role. |
| DeleteRolePolicy | Deletes an inline policy from a role. |
| ListRolePolicies | Lists inline policy names stored on a role. |
Instance Profiles
| Action | Description |
|---|---|
| CreateInstanceProfile | Creates an IAM instance profile, applying any Tags given at creation. |
| GetInstanceProfile | Returns an instance profile and its roles. |
| DeleteInstanceProfile | Deletes an instance profile from the local IAM store. |
| ListInstanceProfiles | Lists IAM instance profiles. |
| AddRoleToInstanceProfile | Adds a role to an instance profile. |
| RemoveRoleFromInstanceProfile | Removes a role from an instance profile. |
| ListInstanceProfilesForRole | Lists instance profiles associated with a role. |
| TagInstanceProfile | Adds tags to an instance profile. |
| UntagInstanceProfile | Removes tags from an instance profile. |
| ListInstanceProfileTags | Lists tags stored for an instance profile. |
CreateInstanceProfile, GetInstanceProfile and ListInstanceProfilesForRole include an instance
profile's own tags inline. ListInstanceProfiles omits them: like ListRoles, its own operation
documentation says "this operation does not return tags, even though they are an attribute of the
returned object", even though the InstanceProfile shape itself carries no such exclusion note.
Tags on a role embedded in InstanceProfileList are always omitted, matching GetInstanceProfile's
own documented role subset.
Access Keys
| Action | Description |
|---|---|
| CreateAccessKey | Creates access-key credentials for a user. |
| GetAccessKeyLastUsed | Returns the stored last-used metadata for an access key. |
| ListAccessKeys | Lists access keys for a user. |
| UpdateAccessKey | Updates an access key's status. |
| DeleteAccessKey | Deletes an access key from a user. |
Account Aliases
| Action | Description |
|---|---|
| ListAccountAliases | Lists the alias set for the account, or an empty list when none is set. |
| CreateAccountAlias | Sets the account alias. An account can hold only one. |
| DeleteAccountAlias | Removes the account alias. |
An account holds one alias, and AWS enforces that by replacement rather than rejection:
CreateAccountAlias with a new value silently swaps the current one. EntityAlreadyExists means
the requested name is taken — on AWS that includes names held by other accounts, since aliases are
globally unique, but the store here is per-account so only "you already hold this one" arises.
DeleteAccountAlias must name the current alias; a mismatch returns NoSuchEntity. Both verbs
apply the same pattern constraint, so a malformed value returns ValidationError on either.
Aliases are 3–63 characters of lowercase letters, digits and hyphens, may not start or end with a
hyphen, and may not contain two hyphens in a row — AWS's documented
^[a-z0-9]([a-z0-9]|-(?!-)){1,61}[a-z0-9]$. The ValidationError message is reproduced from AWS
verbatim and does not itself mention the consecutive-hyphen rule.
Set FLOCI_SERVICES_IAM_ACCOUNT_ALIAS to seed an alias at startup, for callers that expect to
read one without creating it first. It seeds the default account only, so a caller signing
with a credential that resolves to a different account still reads an empty list. Seeding is
skipped when an alias is already stored, so under storage.mode: persistent a changed value has
no effect on later starts — the skip is logged at debug with both values. /_floci/state/reset
clears the alias without re-seeding it, as it does the optional deployer principal; the seed
returns on restart.
Account Password Policy
| Action | Description |
|---|---|
| GetAccountPasswordPolicy | Returns the account's password policy. |
| UpdateAccountPasswordPolicy | Replaces the account's password policy wholesale. |
| DeleteAccountPasswordPolicy | Removes the account's password policy. |
An account holds one password policy. UpdateAccountPasswordPolicy replaces it wholesale rather
than merging — a field the caller omits resets to its AWS-documented default (false for the
boolean requirements, AllowUsersToChangePassword and HardExpiry; 6 for
MinimumPasswordLength; unset for the optional MaxPasswordAge and PasswordReusePrevention)
rather than carrying over the previous value. Unlike the two optional integer fields,
HardExpiry is never absent from the response — AWS documents it as a boolean that always
defaults to false, so GetAccountPasswordPolicy always echoes it back. ExpirePasswords is
derived, not stored: it reports true exactly when MaxPasswordAge is set.
GetAccountPasswordPolicy and DeleteAccountPasswordPolicy both return NoSuchEntity when no
policy has ever been set — a documented, expected result the Terraform provider's
aws_iam_account_password_policy resource branches on. MinimumPasswordLength must be 6–128,
MaxPasswordAge 1–1095, and PasswordReusePrevention 1–24; a value outside those ranges is
rejected with ValidationError. The integer parameters (MinimumPasswordLength, MaxPasswordAge,
PasswordReusePrevention) and the boolean parameters both reject anything that isn't parseable —
a malformed integer or a value other than true/false (case-insensitive) returns
ValidationError rather than silently falling back to a default.
OIDC Identity Providers
| Action | Description |
|---|---|
| CreateOpenIDConnectProvider | Creates an OIDC identity provider from an https URL. |
| GetOpenIDConnectProvider | Returns a provider's URL, client IDs, thumbprints and tags. |
| ListOpenIDConnectProviders | Lists the ARNs of stored OIDC providers. |
| DeleteOpenIDConnectProvider | Deletes an OIDC identity provider. |
| AddClientIDToOpenIDConnectProvider | Adds a client ID (audience) to a provider. |
| RemoveClientIDFromOpenIDConnectProvider | Removes a client ID from a provider. |
| UpdateOpenIDConnectProviderThumbprint | Replaces a provider's thumbprint list. |
| TagOpenIDConnectProvider | Adds tags to a provider. |
| UntagOpenIDConnectProvider | Removes tags from a provider. |
| ListOpenIDConnectProviderTags | Lists tags stored for a provider. |
A provider is identified by its URL, so the ARN is derived from it rather than from a generated
id: https://oidc.eks.eu-central-1.amazonaws.com/id/EXAMPLE becomes
arn:aws:iam::<account>:oidc-provider/oidc.eks.eu-central-1.amazonaws.com/id/EXAMPLE. Creating
the same URL twice returns EntityAlreadyExists. As on AWS, GetOpenIDConnectProvider reports
the URL without its scheme.
The URL must begin with https:// and is at most 255 characters. It is not normalized, matching
AWS: a trailing slash or a difference in case produces a separate provider rather than a
duplicate.
A provider holds at most 100 client IDs (LimitExceeded beyond that) and 5 thumbprints
(InvalidInput beyond that). Adding a client ID that is already present, and removing one that
was never added, both succeed and change nothing, as they do on AWS.
Thumbprints are stored and echoed back but never validated against the remote endpoint, since nothing here performs the TLS handshake they describe.
SAML Identity Providers
| Action | Description |
|---|---|
| CreateSAMLProvider | Creates a SAML identity provider from a metadata document, with optional tags echoed back in the response. |
| GetSAMLProvider | Returns a provider's creation date, tags, and a metadata document rebuilt from its stored entity ID and signing certificate. |
| ListSAMLProviders | Lists the stored SAML providers of the calling account. |
| UpdateSAMLProvider | Replaces a provider's metadata document. |
| DeleteSAMLProvider | Deletes a SAML identity provider. |
| TagSAMLProvider | Adds tags to a SAML identity provider. |
| UntagSAMLProvider | Removes tags from a SAML identity provider. |
| ListSAMLProviderTags | Lists tags stored for a SAML identity provider. |
A provider is identified by name, giving an ARN of the form arn:aws:iam::<account>:saml-provider/<name>.
The name must match [A-Za-z0-9+=,.@_-]{1,128}, and creating the same name twice returns
EntityAlreadyExists. An empty or unparseable metadata document returns InvalidInput. Tags on the
create request are validated before anything is stored, so a request carrying more than 50 of them
fails without leaving a provider behind, as AWS documents.
Floci stores only the entity ID and signing certificate parsed from the metadata, so GetSAMLProvider
returns a minimal rebuilt document rather than the one that was uploaded. UpdateSAMLProvider re-parses
a new SAMLMetadataDocument the same way and replaces the stored entity ID and certificate; omitting it
leaves the provider unchanged, since it is optional on the request. AWS's current UpdateSAMLProvider
and GetSAMLProvider also manage an AssertionEncryptionMode and a private-key list for decrypting
encrypted assertions; Floci's assertion verifier only checks signatures against a single certificate and
does not model encrypted assertions at all, so neither of those is modeled here either.
DeleteSAMLProvider does not check or update any role whose trust policy still references the provider's
ARN, matching AWS's own documented behavior: the delete succeeds regardless, and it is a later
AssumeRoleWithSAML against the now-dangling ARN that fails, not this call. CreateSAMLProvider and
GetSAMLProvider return their tags sorted by key, which is what AWS documents for those two responses;
ListSAMLProviderTags is sorted the same way here for consistency, though AWS does not document an
order for it. Providers created here are used by AssumeRoleWithSAML for trust-policy and assertion
validation.
Login Profiles
| Action | Description |
|---|---|
| CreateLoginProfile | Creates a console password login profile for a user. |
| GetLoginProfile | Returns a user's login profile. |
| UpdateLoginProfile | Updates a user's login profile password and/or reset-required flag. |
| DeleteLoginProfile | Deletes a user's login profile. |
UserName is optional on CreateLoginProfile, GetLoginProfile and DeleteLoginProfile: it
defaults to the user resolved from the signing access key, the same fallback GetUser uses. It is
required on UpdateLoginProfile, matching the AWS API.
A user holds at most one login profile: CreateLoginProfile on a user that already has one
returns EntityAlreadyExists; Get/Update/DeleteLoginProfile on a user with none return
NoSuchEntity. Password is required on CreateLoginProfile and optional on
UpdateLoginProfile; an omitted field on UpdateLoginProfile (Password or
PasswordResetRequired) leaves that field unchanged, unlike UpdateAccountPasswordPolicy's
wholesale replace. A password must be 1–128 characters from AWS's documented password character
class, and when the account has an account password policy set, it is
also checked against that policy's length and character-class requirements, with
PasswordPolicyViolation returned on either action if it doesn't comply. The password itself is never
echoed back by any of these actions, matching AWS.
DeleteUser returns DeleteConflict, as on AWS, while the user still has a login profile, access
keys, inline policies, attached managed policies, group memberships, or an
enabled MFA device: remove those first. Floci has no actions that
create signing certificates, SSH public keys, or Git credentials, so there is nothing of those
kinds to block on. Renaming a user with UpdateUser carries its login
profile, access keys, and group membership to the new name. Unlike AWS, Floci does not rewrite
policy documents that name the user's ARN, so a resource or trust policy that referred to the old
name still refers to it after a rename.
Policy Simulation
| Action | Description |
|---|---|
| SimulatePrincipalPolicy | Evaluates requested actions and resources against the resolved principal's policies. |
| SimulateCustomPolicy | Evaluates requested actions and resources against a standalone set of policy documents, with an optional permissions boundary. |
| GetContextKeysForCustomPolicy | Lists the context keys referenced across a set of policy documents. |
| GetContextKeysForPrincipalPolicy | Lists the context keys referenced across a resolved principal's policies, plus any additional documents supplied. |
GetContextKeysForCustomPolicy and GetContextKeysForPrincipalPolicy return every Condition
operator's key, and every ${...} policy variable found in a Resource pattern or a Condition
value, in the order statements are found. A variable's default value (${key, 'default'}) is
stripped, and the three single-character escapes (${*}, ${?}, ${$}) are excluded, since
they substitute a literal character rather than naming a context key. The list is neither sorted
nor de-duplicated, matching AWS's own documented
example response, which repeats a key referenced by more than one statement.
PolicySourceArn on SimulatePrincipalPolicy and GetContextKeysForPrincipalPolicy resolves an IAM
user or role only, not a group. SimulateCustomPolicy accepts only one
PermissionsBoundaryPolicyInputList document, matching AWS's own documented limit; extra documents
beyond the first are ignored. Neither simulation action evaluates a resource-based policy
(ResourcePolicy) or OrderedOrganizationPolicyInputList, and neither returns
MatchedStatements, ResourceSpecificResults, or a PermissionsBoundaryDecisionDetail: only the
top-level EvalDecision is populated. ContextEntries.member.N.ContextKeyType is accepted but not
read; the comparison is driven entirely by the policy's own condition operator (Bool,
NumericEquals, DateEquals, and so on), not by the declared type.
Last-Accessed Reporting
| Action | Description |
|---|---|
| GenerateServiceLastAccessedDetails | Starts an Access Advisor job for a user, group, role or managed policy and returns its job ID. |
| GetServiceLastAccessedDetails | Returns the job's status and its service-access report. |
| GetServiceLastAccessedDetailsWithEntities | Returns the job's status and the entities that used a given service. |
| ListPoliciesGrantingServiceAccess | Lists the policies that let an IAM identity access each requested service. |
Floci does not record service access. Nothing populates a principal's usage history, and
GetAccessKeyLastUsed already answers with AWS's documented "never used" shape rather than
inventing one. That shape is what these actions reproduce: AWS lists every service an entity could
reach through its permissions policies, and for a service with no access attempt it leaves
LastAuthenticated and TotalAuthenticatedEntities null rather than dropping the service. So
GetServiceLastAccessedDetails returns a real, policy-derived ServicesLastAccessed list in which
every entry is that "no attempt" shape. The missing piece is the usage timestamps, not the list.
A grant that names no namespace is expanded against the vendored catalog of IAM service namespaces
in src/main/resources/aws/iam-service-namespaces.json: an Action of * reaches every published
service, a globbed prefix such as s3* reaches the ones it matches, and an Allow with NotAction
reaches everything the list does not carve out entirely. A namespace a policy names outright is
reported whether or not the catalog knows it, so a catalog that has fallen behind an AWS launch
still reports an explicitly named service; only wildcard expansion depends on it being current.
That catalog is generated by tools/aws/regen_service_namespaces.py from AWS's Service Reference
Information index, whose per-service service field is the IAM namespace itself. botocore is
deliberately not the source: a namespace is not any botocore field, since CloudWatch's
endpointPrefix is monitoring and its serviceId is CloudWatch against an IAM namespace of
cloudwatch. Because that index is a live endpoint rather than a pinned dependency, the gate is not
byte-equality: make iam-namespaces-check validates the file's shape offline, and
make iam-namespaces-verify checks online that every namespace in it still exists upstream, so
nothing invented can survive. Services AWS has added since the last make iam-namespaces-sync are
reported by that check without failing it, since being behind makes the list incomplete rather than
wrong.
ServiceName is a required member that AWS fills with a display name (Amazon S3); Floci has no
such mapping and repeats the namespace instead.
GetServiceLastAccessedDetailsWithEntities is policy-derived in the same way. AWS reports the
entities that could have used the reported permissions to reach a service, so a group report
lists the group's users, a policy report lists the users and roles the policy is attached to plus
the users of any group it is attached to, and a user or role report lists that entity. A user
reachable by more than one of those paths is listed once. Entities are omitted when the reported permissions do not
grant the requested service at all. LastAuthenticated is absent on every entry, since that is
the part Floci does not record.
GenerateServiceLastAccessedDetails resolves the ARN first and returns NoSuchEntity for one that
names nothing, rather than handing out a job ID that could never be meaningful. That resolution
checks the whole ARN: an ARN whose account is not the caller's, or whose path is not the resolved
entity's own, names no entity and is rejected rather than falling back to a same-named local one. A
job belongs to the account that created it, an unknown JobId is NoSuchEntity, and Granularity
is validated against SERVICE_LEVEL/ACTION_LEVEL and echoed back as JobType.
The report is produced when the job is created and stored with it, matching AWS, where
GenerateServiceLastAccessedDetails produces a report and the Get* operations retrieve that one.
So a completed job keeps answering what it answered first: editing the policies afterwards does not
change it, and deleting the entity it covers does not stop a valid JobId from resolving. There is
nothing left to compute asynchronously, so a job is complete when created and its creation and
completion timestamps are the same instant. Jobs are kept for the life of the store: AWS documents
no expiry for a JobId, so ageing them out would mean inventing a retention window and failing a
caller holding an ID that AWS would still answer.
Both readers honour MaxItems and Marker and report IsTruncated truthfully, returning a
Marker only when a page remains. This differs from the rest of this page, where pagination inputs
are accepted and ignored; these actions apply them rather than inherit that gap.
ListPoliciesGrantingServiceAccess is not an Access Advisor job and needs no usage history: AWS
defines it purely over permissions-policy logic, so it is answered from real policy content. It
follows AWS's documented scoping, where a user contributes its own managed and inline policies
plus those of every group it belongs to, while a group or role contributes only its own. Managed
policies are reported with their ARN and their current default version is the one read; inline
policies have no ARN and are identified by the entity holding them. Only Allow grants, so a
Deny-only policy is not listed, and an Allow with NotAction grants every service the list does
not carve out. Permissions boundaries are excluded, as the operation's documentation requires, and
resource-based policies, ACLs, Organizations policies and trust policies are not consulted.
Resources and conditions are not evaluated either: the question is which policies could grant the
service at all, not whether one specific call would be authorized.
Multi-Factor Authentication
| Action | Description |
|---|---|
| CreateVirtualMFADevice | Creates an unassigned virtual MFA device and returns its SerialNumber and Base32StringSeed. |
| ListVirtualMFADevices | Lists the account's virtual MFA devices, filtered by AssignmentStatus (Assigned, Unassigned or Any, defaulting to Any). |
| DeleteVirtualMFADevice | Deletes a device. Returns DeleteConflict while it is still assigned to a user. |
| EnableMFADevice | Assigns a device to a user, after verifying two consecutive authentication codes. |
| DeactivateMFADevice | Detaches a device from its user, leaving the device itself in place. |
| ResyncMFADevice | Re-synchronizes an assigned device, again against two consecutive codes. |
| ListMFADevices | Lists the devices assigned to a user. |
| TagMFADevice / UntagMFADevice / ListMFADeviceTags | Manage a device's tags. |
The seed is real. CreateVirtualMFADevice generates a 160-bit secret from SecureRandom and
returns it as an RFC 4648 base32 string (base64-wrapped on the wire, as AWS models the member), so
an authenticator app seeded from it produces codes Floci accepts. EnableMFADevice and
ResyncMFADevice verify those codes as RFC 6238 TOTP (HMAC-SHA1 over 30-second steps, truncated
to six digits) and return InvalidAuthenticationCode when they don't match, so a caller that does
not hold the seed cannot enable a device. The two codes must be consecutive, as AWS asks ("a
subsequent authentication code"), so the same code sent twice is rejected. EnableMFADevice
allows one 30-second step of drift either side; ResyncMFADevice allows ten, since a device
needing resync is by definition one whose clock has wandered.
SerialNumber is the device ARN, arn:aws:iam::<account>:mfa/<name>, so Path and
VirtualMFADeviceName together identify a device. A device survives DeactivateMFADevice with its
seed intact, so re-enabling it needs no re-provisioning. DeleteUser returns DeleteConflict while
the user still holds a device, and a user with one reports mfa_active as TRUE in the credential
report. Renaming a user with UpdateUser carries the assignment to the new name, alongside the
login profile and access keys it already moved.
A user may hold up to 8 devices, the per-user limit the IAM User Guide documents, after which
EnableMFADevice returns LimitExceeded. That is the only MFA quota Floci enforces:
CreateVirtualMFADevice models LimitExceeded too, but AWS publishes no account-wide figure for
virtual MFA devices, so there is nothing to enforce it against.
Request shapes are checked before the device is resolved, so a SerialNumber outside its modeled
9-to-256 range is a ValidationError rather than a NoSuchEntity for a device that could not have
existed. Note that VirtualMFADeviceName is the one IAM name type with no documented maximum
length: a name longer than the 128 characters other IAM names stop at is accepted here, as on AWS.
ListMFADeviceTags honors Marker and MaxItems, sorting by tag key first as AWS documents, so a
client can walk the result a page at a time. IAM's other tag readers in Floci still return every
tag with IsTruncated=false.
VirtualMFADeviceName has no maximum length, but the serial number it mints does: 256 characters.
A name long enough to overflow that is rejected at creation rather than producing a device whose
serial every other MFA operation would refuse.
QRCodePNG is not returned. AWS marks it optional, and rendering a PNG would mean taking on a QR
encoder dependency for a field whose content is derivable: it encodes
otpauth://totp/<device>@<account>?secret=<Base32String>, which a caller can build from the
Base32StringSeed that is returned. aws iam create-virtual-mfa-device works against Floci with
--bootstrap-method Base32StringSeed, and fails only when asked for the QR code specifically.
GetMFADevice is not implemented: AWS states "for this API, we only accept FIDO security key
ARNs", and Floci models virtual devices only. Hardware TOTP tokens and FIDO security keys are not
modeled either, so ListMFADevices returns only virtual devices where AWS would return every type.
What a device does not yet affect
Enabling a device records state and nothing more. It does not change what a request is allowed to do:
- Policy evaluation ignores MFA.
aws:MultiFactorAuthPresentandaws:MultiFactorAuthAgeare never placed in the request context, so under enforcement a statement conditioned on either key does not behave as it would on AWS. Both directions fail closed rather than open: anAllowgated onBool: {"aws:MultiFactorAuthPresent": "true"}never grants, because a missing key fails the condition block; and the commonDeny+BoolIfExists: {"aws:MultiFactorAuthPresent": "false"}lockout idiom always denies, becauseIfExistspasses on a missing key. So an MFA-gated policy is stricter than AWS here, not laxer, but a device being enabled will not unlock it. - No MFA-authenticated credentials.
GetSessionTokenandAssumeRoleacceptSerialNumberandTokenCodeon AWS and return credentials that carry the MFA context keys. Floci's STS implementations ignore both parameters, so there is no way to obtain a session that would satisfy an MFA condition even once the keys are populated.
Both are out of scope here: this covers the device lifecycle only, and wiring MFA into authorization means touching the request context and STS session shape, which is separate work.
Account
| Action | Description |
|---|---|
| GetAccountSummary | Returns entity counts (users, groups, roles, customer-managed policies, instance profiles, MFA devices) and IAM quota values. Providers counts OIDC providers only; SAML providers are not included. Resources Floci does not track (the account password) are reported as zero rather than omitted. |
| GetAccountAuthorizationDetails | Returns every user, group and role in the account, and the policies relevant to them: every local (customer-managed) policy, and every AWS-managed policy actually attached to or used as a permissions boundary by something in the account. |
| GenerateCredentialReport | Generates (or, within 4 hours of the last one, reuses) the account's credential report. |
| GetCredentialReport | Returns the most recently generated credential report as Base64-encoded CSV. |
Filter, MaxItems and Marker are not honored: the response always includes everything, with
IsTruncated always false. AttachmentCount and PermissionsBoundaryUsageCount are computed by
scanning the account's own users, groups and roles rather than read off a stored counter, so they
are correctly scoped to the calling account even for an AWS-managed policy (see the note on
IamService.getAccountAuthorizationDetails for why that distinction matters). Policy documents are
returned as plain JSON, not URL-encoded as AWS documents them; this matches every other IAM action
that returns a policy document (GetPolicyVersion, GetRolePolicy, and so on), none of which
URL-encode either.
The credential report holds the 23 columns AWS documents, always led by a <root_account> row.
Floci does not model root account credentials at all (GetAccountSummary's
AccountPasswordPresent/AccountAccessKeysPresent are always zero for the same reason), so that
row is placeholder values throughout, including its mfa_active, which reports on root rather
than on any IAM user's device. X.509 signing certificates are not modeled, so every cert_* column
is always FALSE/N/A; access key last-used tracking (date, region, service) is not modeled, so
those three columns are always N/A too. mfa_active on a user row is real, and is TRUE once
the user has a device enabled. password_last_used is likewise not tracked, so it is always no_information.
password_last_changed reflects an UpdateLoginProfile password change, not just
CreateLoginProfile. additional_credentials_info is Floci's own wording, since AWS does not
document the exact text; in practice it is unreachable, since CreateAccessKey already enforces
the real 2-key-per-user quota. Generating a report is effectively instant, so GenerateCredentialReport never actually
returns INPROGRESS, and a GetCredentialReport call right after it always finds the report
ready. GenerateCredentialReport's State/Description for the no-report-exists case match AWS's
own documented example response (STARTED / "No report exists. Starting a new report generation
task"); the wording for the report-expired case is Floci's own, since AWS does not document it.
Organizations Root Access
| Action | Description |
|---|---|
| ListOrganizationsFeatures | Lists the centralized root access features that are currently enabled. |
| EnableOrganizationsRootCredentialsManagement | Enables the RootCredentialsManagement feature. |
| DisableOrganizationsRootCredentialsManagement | Disables the RootCredentialsManagement feature. |
| EnableOrganizationsRootSessions | Enables the RootSessions feature. |
| DisableOrganizationsRootSessions | Disables the RootSessions feature. |
Only the set of enabled features is stored, and enabling a feature twice is idempotent. Floci does not
model root credentials or root sessions themselves, so the flags change what ListOrganizationsFeatures
returns and nothing else.
Server Certificates
| Action | Description |
|---|---|
| UploadServerCertificate | Stores a PEM certificate, its private key and an optional chain under a name unique to the account. |
| GetServerCertificate | Returns a stored certificate and its chain, never the private key. |
| UpdateServerCertificate | Renames a certificate and/or changes its path; the ARN moves with it. |
| DeleteServerCertificate | Deletes a stored certificate. |
| ListServerCertificates | Lists certificate metadata, filtered by PathPrefix. |
| TagServerCertificate | Adds tags to a server certificate. |
| UntagServerCertificate | Removes tags from a server certificate. |
| ListServerCertificateTags | Lists tags stored for a server certificate. |
The uploaded material is really parsed, because two of this operation's modeled errors cannot be
answered otherwise. CertificateBody (and CertificateChain, when given) must be readable PEM or
the upload is MalformedCertificate, and the private key must actually match the certificate's
public key or it is KeyPairMismatch. The match is a sign-then-verify check, so it holds for RSA
and EC alike. Expiration is read from the certificate's own notAfter rather than stored
separately, so it cannot drift from the certificate it describes.
The private key is stored and never returned. AWS marks privateKeyType sensitive and models it
only on the upload, so neither GetServerCertificate nor ListServerCertificates echoes it back.
ListServerCertificates returns metadata only, as AWS documents: it "does not return the
certificate body, certificate chain, or private key".
ServerCertificateId uses AWS's ASCA prefix for certificates. GetAccountSummary's
ServerCertificates count is backed by this store rather than reporting zero.
One modeled error is not raised: AWS returns DeleteConflict from DeleteServerCertificate when a
load balancer still references the certificate. Floci cannot determine that yet, because ELB
Classic and CloudFront store certificate identifiers without resolving them against IAM. Tracked in
#4875, which covers both directions: rejecting a
reference to a certificate that does not exist, and refusing to delete one that is in use.
Signing Certificates
| Action | Description |
|---|---|
| UploadSigningCertificate | Stores an X.509 signing certificate against an IAM user and returns its generated CertificateId. |
| ListSigningCertificates | Lists a user's signing certificates, with Marker and MaxItems paging. |
| UpdateSigningCertificate | Sets a certificate's status to Active, Inactive or Expired. |
| DeleteSigningCertificate | Deletes one of a user's signing certificates. |
A signing certificate is not a server certificate: it belongs to a user rather than the account, it
carries no private key, no name and no path, and the generated CertificateId is the only handle
to it. The body is parsed on upload, because MalformedCertificate cannot be answered without
reading the material, and the status starts as Active.
UserName is optional on all four operations. Left out, it resolves to the user owning the access
key that signed the request, which is what the model documents.
Two certificates per user, which the User Guide states directly: "Users can have up to two X.509
signing certificates, to make certificate rotation easier". A third upload is LimitExceeded. The
count is taken inside the same lock as the write, so concurrent uploads cannot both see room for
the last slot.
DuplicateCertificate is account-wide rather than per user: AWS describes it as "the same
certificate is associated with an IAM user in the account", so a second user cannot upload material
the first already holds. The comparison is made on the encoded certificate rather than the PEM
text, so the same certificate re-wrapped or re-indented still counts as the same one.
UpdateSigningCertificate accepts Expired as well as Active and Inactive. The parameter's
prose explains only the first two, but the API Reference gives all three as valid values.
A signing certificate blocks DeleteUser until it is removed, which is one of the items AWS lists
as a prerequisite for deleting a user programmatically. It also follows the user across an
UpdateUser rename: left behind, a certificate would be stranded on a name that no longer exists,
invisible to its owner because listing goes through the user.
The credential report's cert_1_active and cert_2_active columns are backed by this store
instead of always reporting FALSE. The matching cert_*_last_rotated columns report the upload
date, and N/A when the certificate is not Active, which is how the User Guide defines them.
GetAccountSummary's AccountSigningCertificatesPresent is unaffected: it reports the account root
user's certificates, and Floci does not model root credentials.
Under enforcement these actions are evaluated against * rather than the
owning user's ARN, along with every other IAM action except the server-certificate operations. That
is the general gap tracked in #4979, not something
specific to signing certificates.
AWS Managed Policies
Floci seeds a catalog of commonly-used AWS managed policies at startup. These are attachable immediately without any setup:
General access
AdministratorAccess · PowerUserAccess · ReadOnlyAccess · IAMFullAccess · AmazonS3FullAccess · AmazonS3ReadOnlyAccess · AmazonDynamoDBFullAccess · AmazonEC2FullAccess · AmazonSQSFullAccess · AmazonSNSFullAccess · AmazonVPCFullAccess · CloudWatchFullAccess · AWSLambdaFullAccess
Lambda execution roles (arn:aws:iam::aws:policy/service-role/...)
AWSLambdaBasicExecutionRole · AWSLambdaBasicDurableExecutionRolePolicy · AWSLambdaDynamoDBExecutionRole · AWSLambdaKinesisExecutionRole · AWSLambdaMSKExecutionRole · AWSLambdaSQSQueueExecutionRole · AWSLambdaVPCAccessExecutionRole
ECS / EKS execution roles
AmazonECSTaskExecutionRolePolicy · AmazonEKSFargatePodExecutionRolePolicy
EKS cluster & node groups
AmazonEKSClusterPolicy · AmazonEKSServicePolicy · AmazonEKSVPCResourceController · AmazonEKSWorkerNodePolicy · AmazonEKS_CNI_Policy
Other execution roles
AmazonS3ObjectLambdaExecutionRolePolicy · CloudWatchLambdaInsightsExecutionRolePolicy · CloudWatchLambdaApplicationSignalsExecutionRolePolicy · AWSConfigRulesExecutionRole · AWSMSKReplicatorExecutionRole · AWS-SSM-DiagnosisAutomation-ExecutionRolePolicy · AWS-SSM-RemediationAutomation-ExecutionRolePolicy · AmazonSageMakerGeospatialExecutionRole · AmazonSageMakerCanvasEMRServerlessExecutionRolePolicy · SageMakerStudioBedrockFunctionExecutionRolePolicy · SageMakerStudioDomainExecutionRolePolicy · SageMakerStudioQueryExecutionRolePolicy · AmazonDataZoneDomainExecutionRolePolicy · AmazonBedrockAgentCoreMemoryBedrockModelInferenceExecutionRolePolicy · AWSPartnerCentralSellingResourceSnapshotJobExecutionRolePolicy
Every catalog entry carries the real policy document of its current default version, generated from the public iam-dataset, so GetPolicyVersion returns the same statements a real account would and enforcement mode evaluates them faithfully.
Version numbers
AWS revises its managed policies in place, so their default version is rarely v1: AmazonS3ReadOnlyAccess is on v3, ReadOnlyAccess far beyond that, while AdministratorAccess has never been revised. Floci reports the version id AWS publishes for each policy, the date the policy was first created as CreateDate, and the date of its current default version as UpdateDate:
aws --endpoint-url http://localhost:4566 iam get-policy \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
# -> DefaultVersionId: "v3", CreateDate: 2015-02-06T18:40:00Z, UpdateDate: 2023-08-10T21:31:39Z
aws --endpoint-url http://localhost:4566 iam list-policy-versions \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
# -> a single entry, v3, IsDefaultVersion: true
Only the default version's document is bundled. Requesting a superseded version (v1 or v2 of AmazonS3ReadOnlyAccess) returns NoSuchEntity, the same answer AWS gives once it has pruned a managed policy's history, and ListPolicyVersions lists only the default. AWS managed policies remain read-only: CreatePolicyVersion, SetDefaultPolicyVersion and DeletePolicyVersion are rejected with AccessDenied.
Optional Local Deployer Principal
Floci can seed a local IAM user for development workflows that expect a concrete caller identity before provisioning starts. This is disabled by default.
Enable it with:
When enabled, Floci creates the floci-deployer user if it does not already exist, attaches arn:aws:iam::aws:policy/AdministratorAccess, and creates static floci / floci access-key credentials if that access key does not already exist. Existing users and access keys are preserved.
Requests signed with the seeded access key return the deployer user ARN from sts:GetCallerIdentity.
IAM Enforcement Mode
By default Floci accepts any credentials without enforcing IAM policies — all requests are allowed through regardless of what policies are attached to the calling identity. This preserves backward compatibility and keeps the default setup frictionless.
Setting enforcement-enabled: true activates the policy evaluator as a JAX-RS request filter. Every inbound request is then evaluated against the identity-based policies of the calling IAM user or assumed role before it reaches the service handler. This includes IAM's own management actions (iam:CreateUser, iam:CreateGroup, iam:AttachUserPolicy, iam:DeleteUser, ...): a user whose policies only grant, say, s3:* receives AccessDenied when calling them.
The startup banner reports the effective state (IAM: policy enforcement enabled / disabled). If requests you expect to be denied keep succeeding, check that line first: the flag is only read under the name below, and any other spelling (for example FLOCI_IAM_STRICT_VALIDATION, which does not exist) is silently ignored, leaving the permissive default in place.
Enable enforcement
Environment variable:
Docker Compose:
Evaluation rules
Policy evaluation follows the standard AWS precedence:
- If SCP enforcement is active, the action must be allowed at every organization level (root, OUs on the path, account) and explicitly denied at none — otherwise the request is denied before identity policies are consulted
- An explicit Deny in any identity, session, or boundary policy denies the request
- An explicit Allow in an identity policy creates the base grant
- If a session policy is present, it must also explicitly allow the request
- If a permission boundary is present, it must also explicitly allow the request
- No matching effective allow results in an implicit deny
IAM authorization denials return HTTP 400 AccessDeniedException for AWS JSON 1.0/1.1
requests. REST-JSON requests return HTTP 403 AccessDeniedException; AWS Query and S3
requests retain HTTP 403 XML AccessDenied responses. The routed protocol determines
the status, not just the request's content type.
A REST request is authorized as the operation of the route it reached. An X-Amz-Target header or
a Query Action field on it does not change the action it is checked as, and its path is matched
still percent-encoded, as the router matches it, so an encoded / inside a parameter cannot make
the route's rule miss.
Resource-based policies
When FLOCI_SERVICES_IAM_ENFORCEMENT_ENABLED is active, Floci also queries registered ResourcePolicyProvider SPI implementations (such as S3 bucket policies) during request authorization:
- Resource policy statements are matched against the caller (
PrincipalandNotPrincipalclauses), each principal type naming only its own kind of caller, as on AWS:"*"matches anyone;{"AWS": "*"}matches IAM identities and AWS services; any otherAWSentry (a user, role, account id or account root) matches IAM identities only, and a role session matches aPrincipalnaming its role through the role's own ARN, path included;{"Service": "<name>"}matches that service exactly.{"CanonicalUser": "<id>"}, which S3 bucket policies accept, names an account by its S3 canonical user ID and matches that account's IAM identities, as an account principal does; Floci's canonical ID for an account is the account id, as its S3 ACLs report it.{"Service": "*"}, which AWS does not accept, matches nothing, andFederatedentries never match an IAM caller. - An explicit Deny in a resource policy overrides any allows.
- In cross-account scenarios or resource-controlled access, an explicit Allow in a resource policy grants access to the principal.
- For detailed S3 bucket policy behavior and configuration, see S3 Bucket Policy Enforcement.
Service control policies (SCPs)
When the caller's account belongs to an Organizations organization, SCPs attached to the root, the OUs on the account's path, and the account itself can participate in evaluation. Two flags must both be on:
(env: FLOCI_SERVICES_IAM_ENFORCEMENT_ENABLED and
FLOCI_SERVICES_ORGANIZATIONS_SCP_ENFORCEMENT_ENABLED)
SCP semantics match AWS: SCPs never grant permissions — they cap what identity policies
may allow; the organization's management account is exempt; and an account outside
any organization is unaffected. The test credential is never SCP-denied.
The account-root principal is subject to SCPs. floci's account root is a bare
12-digit account-id access key (the LocalStack multi-account convention). It carries no
registered IAM identity, so it normally takes the unknown-key bypass. But when the access
key equals its own account ID and that account has an effective SCP ceiling
(effectiveScpLevels != null — i.e. it is a non-management member under a root with the
SCP policy type enabled and at least one attached SCP), floci synthesizes an
allow-everything root identity and evaluates the request against the SCP chain. In that
case SCPs apply and nothing else does — no identity policies, permission boundary, or
session policy attaches to the bare account key. If the account has no effective SCP
ceiling (the management account, an account outside any organization, or the SCP type
disabled), the bare key still bypasses enforcement entirely. An AKIA… key that exists
nowhere is rejected rather than bypassed.
Bypass rules
These identities always bypass enforcement (backward-compatible defaults):
| Identity | Behaviour |
|---|---|
Access key test (the default dev credential) |
Always allowed — no policy lookup |
| Access key that exists nowhere | Rejected with 403: InvalidAccessKeyId for S3, InvalidClientTokenId for Query services, UnrecognizedClientException for JSON services |
| Credential the filter cannot map to policies, such as a session carrying no role ARN | Allowed: it is a real credential, so rejecting it would refuse an authenticated caller |
No Authorization header |
Allowed — unauthenticated path (e.g. health checks) |
| Unresolvable IAM action for the request | Allowed — unknown mappings are permissive |
IAM's own resources are mostly not named. When enforcement evaluates a request, the target
resource comes from ResourceArnBuilder, which builds an ARN for S3, Lambda, SQS, SNS, DynamoDB,
Kinesis, Secrets Manager, SSM, KMS, and, within IAM, only the server-certificate operations. Every
other IAM action is evaluated against *, so a statement naming a specific user, role, policy,
instance profile, MFA device or identity provider does not constrain it: a Deny on
arn:aws:iam::123456789012:user/bob does not stop DeleteUser from running, and an Allow
scoped to one role does not limit DeleteRole to it. Action-level matching works normally, so
denying iam:DeleteUser outright does take effect; it is only the resource half that is missing.
This is the behaviour IAM has always had here rather than a recent change, and it errs toward permissive, which is the direction worth knowing about. Closing it means mapping the resource of every dispatched IAM action, which is tracked in #4979 rather than bundled into the server-certificate work that mapped the first few.
A certificate rename names two resources. UpdateServerCertificate is evaluated against both
the certificate's current ARN and the ARN that NewServerCertificateName or NewPath would
produce, because AWS requires the principal to hold permission on the old name and the new one: a
principal allowed to update ProductionCert but not ProdCert cannot rename the first into the
second. A request naming several resources is authorized once per resource, so a Deny on either
name refuses the rename, and the certificate keeps its original name and path. An update that
changes neither the name nor the path names a single resource. The destination ARN is built beside
the stored one, keeping the certificate's own partition and account, since a rename moves a
certificate within an account rather than between partitions.
Exception: a bare 12-digit account-id key that equals its own account and sits under
an effective SCP ceiling is not treated as an unknown key — it is evaluated against
the SCP chain as the account root (see Service control policies
above). Identity-policy enforcement of a member account still requires an assumable,
account-routable identity such as the OrganizationAccountAccessRole session; the bare
account key carries no identity policies of its own.
Supported policy features
- Identity-based policies: inline user/group/role policies and managed attached policies.
- Session policies: inline policies passed during
sts:AssumeRole. - Permission boundaries: managed policies used to cap maximum permissions.
- Action/Resource patterns: literal matches, wildcards (
*,?), andNotAction/NotResourceblocks. Action names match without regard to case, resource ARNs match case-sensitively, as on AWS. - Conditions: support for
Conditionblocks with multiple operators. - Effects:
AllowandDeny.
Supported Condition Operators:
StringEquals,StringNotEquals,StringEqualsIgnoreCase,StringNotEqualsIgnoreCaseStringLike,StringNotLike: case-sensitive glob matching with*and?.ArnEquals,ArnLike,ArnNotEquals,ArnNotLike: case-sensitive glob matching of each of the six ARN components independently. Wildcards cannot cross the first five colon separators; colons within the resource component are retained.ArnEqualsandArnLikebehave identically, as do their negated forms. Service-principal trust policies use the same component-by-component ARN matching foraws:SourceArn.NumericEquals,NumericNotEquals,NumericLessThan,NumericGreaterThan(and Equals variants)DateEquals,DateNotEquals,DateLessThan,DateGreaterThan(and Equals variants)Bool,IpAddress,NotIpAddress,Null- Supports
...IfExistsvariants for all operators. - Set operators
ForAllValues:andForAnyValue:over multi-valued condition keys, in AWS's own spelling (the prefix match is case-sensitive). They compose withIfExists(ForAnyValue:StringEqualsIfExists).ForAllValues:over an empty set matches vacuously andForAnyValue:over an empty set does not match, so pairForAllValues:with"Null":{"<key>":"false"}as you would on AWS:Nulltreats a present-but-empty set as absent, so the guard fires either way. - When a condition lists several values, a positive operator matches if the request value
equals any of them; a negated operator (
StringNotEquals,ArnNotLike,NotIpAddress, …) matches only if the request value differs from all of them. This is what makesForAllValues:StringNotEqualsondynamodb:Attributesa usable deny-list.
Condition keys floci populates
A Condition operator can only match a key floci actually places in the request context.
floci populates:
s3:prefix,s3:delimiter,s3:max-keys: from the S3 request parameters.aws:RequestTag/<key>: the tags named in the request itself, before they are applied, forec2:RunInstances(TagSpecification.N),ec2:CreateTags(Tag.N) ands3:PutBucketTagging(the<Tagging>body).aws:ResourceTag/<key>: the target resource's current tags, forec2:CreateTags,ec2:DeleteTags,ec2:TerminateInstancesandec2:DescribeInstances(the firstResourceId.NorInstanceId.N), and fors3:GetBucketTagging,s3:DeleteBucketTaggingands3:DeleteBucket(the bucket). A request naming several EC2 resources is evaluated once per resource and denied when any of them fails the condition, as on AWS.s3:ExistingObjectTag/<key>: the tags already on the target object version, fors3:GetObject,s3:GetObjectTagging,s3:GetObjectAcl,s3:PutObjectAcl,s3:DeleteObjectTaggingands3:PutObjectTagging. AversionIdin the request selects the version whose tags are read.s3:DeleteObjectands3:PutObjectdo not receive this key, as measured on AWS. An allow conditioned on it denies the delete of a correctly tagged object, and a create cannot be gated on tags an object does not have yet.s3:RequestObjectTag/<key>: a tag the request asks to attach.s3:PutObjectreads these from thex-amz-taggingheader ands3:PutObjectTaggingfrom the<Tagging>body. Any pair that does not decode is dropped, so a policy conditioned on the key denies such a request. Where enforcement lets it through, the handler still answers a malformed header with400 InvalidTag.s3:RequestObjectTagKeysis not populated, so a condition on it never matches.- A
PutObjectcarryingIf-Matchis authorized ass3:GetObjectas well, and that second check is made without the object's tags in the context, as measured on AWS.If-None-Matchneeds no such permission. aws:PrincipalArn: the caller's ARN, resolved from the signing access key. It is the IAM-user ARN for a user access key, andarn:aws:iam::<account>:rootfor the bare account-id key (floci's account-root principal), matching the ARN shape AWS itself reports for the account root. For an STS role session it is the ARN of the role that was assumed, path included, not theassumed-rolesession ARN, as AWS reports it ("For IAM roles, the request context returns the ARN of the role"); a condition naming the session ARN does not match. It is absent only for unknown keys, where nothing about the caller can be resolved.dynamodb:LeadingKeys,dynamodb:Attributes,dynamodb:Select: from the DynamoDB request body, forGetItem,PutItem,UpdateItem,DeleteItem,Query,BatchGetItemandBatchWriteItem(dynamodb:SelectforQueryandScan).LeadingKeysholds the partition-key values the request names: fromKey,Item, theKeyConditionExpressionequality, the legacyKeyConditionsEQentry, or eachRequestItemsentry.Attributesholds the attribute names the request names (item and key fields,AttributesToGet, projection / update / filter / condition / key-condition expressions, andExpressionAttributeNames); a request with no projection returns every attribute while reporting only the names it mentions, exactly as on AWS, which is why AWS pairsAttributeswithdynamodb:Select. Each key is omitted when it cannot be determined (unknown table, aKeythat omits the partition attribute, an unparseableKeyConditionExpression, a multi-table batch), so a policy scoping access through it denies the request rather than allowing an unproven one.
Consequence: with enforcement on and access scoped purely through dynamodb:LeadingKeys,
a malformed request (such as a GetItem whose Key omits the partition attribute) is answered with
AccessDeniedException instead of the ValidationException DynamoDB would return. Failing
closed is the correct direction for a security boundary.
Any other condition key is absent from the request context. A plain (non-IfExists)
operator on an absent key makes the whole statement not apply: it neither matches nor
blocks. A DenyRootUser-style guardrail keyed on aws:PrincipalArn therefore fires against
the account root the same way it does on real AWS, consistent with the account root already
being bounded by SCPs (below): both forms of root enforcement now agree. A negated operator
(StringNotEquals, ArnNotLike, NotIpAddress and the rest) is the exception, as on AWS: an
absent key cannot equal what the policy names, so the condition holds, and a Deny written
that way applies when the key is missing.
Not yet supported: NotPrincipal, resource-based policies (S3 bucket policy, Lambda resource
policy), and dynamodb:LeadingKeys for Scan, TransactWriteItems / TransactGetItems and the
PartiQL operations.
Assumed roles
When a caller uses sts:AssumeRole the returned session credentials are registered internally. Subsequent requests signed with those session credentials are evaluated against:
1. The role's attached and inline policies.
2. The session policy (if provided during AssumeRole), acting as an intersection filter.
Example — minimal enforcement setup
export AWS_ENDPOINT_URL=http://localhost:4566
# Create a user and get credentials
aws iam create-user --user-name alice
KEY=$(aws iam create-access-key --user-name alice --query 'AccessKey.[AccessKeyId,SecretAccessKey]' --output text)
AKID=$(echo $KEY | awk '{print $1}')
SECRET=$(echo $KEY | awk '{print $2}')
# Create and attach a policy that allows S3 list
POLICY_ARN=$(aws iam create-policy \
--policy-name allow-s3-list \
--policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:ListAllMyBuckets","Resource":"*"}]}' \
--query 'Policy.Arn' --output text)
aws iam attach-user-policy --user-name alice --policy-arn $POLICY_ARN
# alice can now list buckets
AWS_ACCESS_KEY_ID=$AKID AWS_SECRET_ACCESS_KEY=$SECRET \
aws s3 ls
Service Control Policies (SCPs)
When FLOCI_SERVICES_IAM_ENFORCEMENT_ENABLED=true and
FLOCI_SERVICES_ORGANIZATIONS_SCP_ENFORCEMENT_ENABLED=true, service control policies attached to
the caller account's organization participate in policy evaluation.
The SCP chain is resolved root → OUs on the path → account, and every level must allow the action for the request to proceed. SCPs never grant permissions on their own — they are a ceiling applied before identity policies are consulted, exactly as on AWS. Organizations is resolved lazily, so IAM does not gain a hard dependency on it: with the Organizations service absent or the flag off, the chain is skipped entirely and evaluation falls back to identity policies alone.
A member account's own bare 12-digit access key is floci's account-root principal. It is not a
registered IAM identity, but like the AWS account root it is still bounded by SCPs, so an SCP Deny
(for example a DenyLeaveOrganization guardrail) blocks it. What that key does not carry is any
identity policy — it behaves as allow-everything bounded only by the SCP ceiling. To exercise
identity-policy enforcement, use account-routable credentials for the member instead, most
naturally the ASIA… session from assuming its OrganizationAccountAccessRole.
Note that aws:PrincipalArn is populated for the account-root principal too, as
arn:aws:iam::<account>:root, so a principal-scoped guardrail keyed on aws:PrincipalArn
(for example a DenyRootUser statement matching arn:aws:iam::*:root) fires against it —
consistent with the account root already being bounded by SCPs above.
A level containing an SCP document that fails to parse denies every action at that level. The
ceiling cannot tell what an unreadable guardrail would have said, and every target also carries
FullAWSAccess, so dropping the bad document would leave the level allowing everything.
The ceiling is attached by the request filter, which is the only producer of SCP levels. Two
evaluation paths therefore run without one and are not SCP-bounded: SimulatePrincipalPolicy,
and the field-level authorization check on the AppSync GraphQL IAM-auth path (the coarse AppSync
request itself still passes through the filter). Both are deliberate non-goals — bounding them
would require IAM to resolve the caller's organization directly, which is the dependency the
lazily-resolved ScpProvider exists to avoid.
Unsigned requests
With enforcement on, a request carrying no Authorization header is refused with
403 MissingAuthenticationToken when its wire shape names a management operation: a JSON, CBOR or
Query request, identified by X-Amz-Target, an rpcv2 path or an Action parameter.
Two carve-outs, both deliberate.
Operations AWS serves without credentials are still allowed. Signing up or signing in to a
Cognito user pool, and AssumeRoleWithWebIdentity, happen before the caller has any AWS
credentials, so AWS marks them as needing none and Floci does the same. The list is taken from the
authtype: none trait in AWS's own service models and covers the Cognito user-pool and identity
flows and the two web-identity and SAML AssumeRole calls. A Cognito Admin* operation is not in that set and does require a signature.
The refusal arrives in the encoding the request used: XML for Query, CBOR for a CBOR request, JSON otherwise.
REST requests are not checked. The same filter sees the API Gateway execute path, Lambda function URLs, CloudFront serving, the Cognito OIDC endpoints and Floci's own health endpoint, all of which are unsigned by design, and a REST request does not say which service will serve it. An unsigned REST call therefore still reaches the service, including an unsigned S3 call.
Bypass rules
Enforcement is deliberately permissive in a few cases, so that enabling it does not break workloads the emulator cannot reason about:
| Case | Behaviour |
|---|---|
| Unresolvable action | Allowed. An action the registry cannot resolve is not evaluated. |
No Authorization header, RPC protocol |
Rejected with 403 MissingAuthenticationToken, unless the operation is one AWS itself serves without credentials. |
No Authorization header, REST protocol |
Allowed. This filter also sees the API Gateway execute path, Lambda function URLs, CloudFront serving and the Cognito OIDC endpoints, which are unsigned by design. |
sts:GetCallerIdentity |
Always allowed — AWS returns caller identity even when a policy denies it. |
| Access key that exists nowhere | Rejected with 403, in each protocol's own vocabulary: InvalidAccessKeyId for S3, InvalidClientTokenId for Query services, UnrecognizedClientException for JSON services. Allowing it would let any string authorize the request. |
| Known credential with no mappable caller context | Allowed. A stored session carrying no role ARN is a real credential, so it is not treated as unauthenticated. |
| Bare account-id key with no SCP ceiling | Allowed. With no organization or SCP enforcement off, the account root keeps the historical bypass. |
| Bare account-id key with an SCP ceiling | Enforced as the account root, bounded by the SCP chain. |
Service-linked roles
CreateServiceLinkedRole puts a role under /aws-service-role/<principal>/ and marks it as
service-linked. As on AWS, a role carrying that mark is protected: AttachRolePolicy,
DetachRolePolicy, PutRolePolicy, DeleteRolePolicy, PutRolePermissionsBoundary,
DeleteRolePermissionsBoundary, UpdateRole, UpdateAssumeRolePolicy, AddRoleToInstanceProfile,
RemoveRoleFromInstanceProfile and DeleteRole all answer UnmodifiableEntity and name the
linked service to go through instead. TagRole and UntagRole are allowed, as on AWS. Within the
IAM API DeleteServiceLinkedRole is the only way to remove such a role — the emulator's own
/_floci/state/reset still clears it along with everything else.
Three deviations to be aware of:
- The role name is derived locally and will not match AWS for most services. AWS lets each
linked service choose the name, and it is not computable from the service principal —
lex.amazonaws.comyieldsAWSServiceRoleForLexBotsthere, where Floci derivesAWSServiceRoleForLex. Read the name back from the create response rather than hardcoding it, and do not rely on a name observed locally matching the one AWS mints. - Deletion is synchronous.
DeleteServiceLinkedRolecompletes before it returns, so the task id it hands back is already finished andGetServiceLinkedRoleDeletionStatusalways reportsSUCCEEDED. TheIN_PROGRESS,NOT_STARTEDandFAILEDstates never occur, and no failureReasonis ever returned — a poll loop works, but its failure branch is never taken. CreateRoleaccepts the/aws-service-role/path, which AWS reserves. AWS rejects that prefix onCreateRole; Floci allows it and treats the result as an ordinary role, since the service-linked mark comes from the action that minted the role rather than from its path. Such a role stays fully modifiable, andDeleteServiceLinkedRoleanswersNoSuchEntityfor it.
Configuration
| Variable | Default | Description |
|---|---|---|
FLOCI_SERVICES_IAM_ENABLED |
true |
Enable or disable the service |
FLOCI_SERVICES_IAM_ENFORCEMENT_ENABLED |
false |
Enforce IAM policies on all inbound requests |
FLOCI_SERVICES_IAM_SEED_DEPLOYER_PRINCIPAL |
false |
Seed the optional floci-deployer user and floci / floci access key |
FLOCI_SERVICES_IAM_ACCOUNT_ALIAS |
(unset) | Seed an account alias at startup; unset means the account has no alias |
Examples
export AWS_ENDPOINT_URL=http://localhost:4566
# Create a role
aws iam create-role \
--role-name lambda-execution-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}' \
--endpoint-url $AWS_ENDPOINT_URL
# Attach a managed policy
aws iam attach-role-policy \
--role-name lambda-execution-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole \
--endpoint-url $AWS_ENDPOINT_URL
# Create a user
aws iam create-user --user-name alice --endpoint-url $AWS_ENDPOINT_URL
# Create an access key
aws iam create-access-key --user-name alice --endpoint-url $AWS_ENDPOINT_URL
# List roles
aws iam list-roles --endpoint-url $AWS_ENDPOINT_URL