Skip to content

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:MultiFactorAuthPresent and aws:MultiFactorAuthAge are 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: an Allow gated on Bool: {"aws:MultiFactorAuthPresent": "true"} never grants, because a missing key fails the condition block; and the common Deny + BoolIfExists: {"aws:MultiFactorAuthPresent": "false"} lockout idiom always denies, because IfExists passes 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. GetSessionToken and AssumeRole accept SerialNumber and TokenCode on 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:

FLOCI_SERVICES_IAM_SEED_DEPLOYER_PRINCIPAL=true

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:

FLOCI_SERVICES_IAM_ENFORCEMENT_ENABLED=true

Docker Compose:

environment:
  FLOCI_SERVICES_IAM_ENFORCEMENT_ENABLED: "true"

Evaluation rules

Policy evaluation follows the standard AWS precedence:

  1. 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
  2. An explicit Deny in any identity, session, or boundary policy denies the request
  3. An explicit Allow in an identity policy creates the base grant
  4. If a session policy is present, it must also explicitly allow the request
  5. If a permission boundary is present, it must also explicitly allow the request
  6. 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 (Principal and NotPrincipal clauses), each principal type naming only its own kind of caller, as on AWS: "*" matches anyone; {"AWS": "*"} matches IAM identities and AWS services; any other AWS entry (a user, role, account id or account root) matches IAM identities only, and a role session matches a Principal naming 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, and Federated entries 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:

floci:
  services:
    iam:
      enforcement-enabled: true
    organizations:
      scp-enforcement-enabled: true

(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 (*, ?), and NotAction/NotResource blocks. Action names match without regard to case, resource ARNs match case-sensitively, as on AWS.
  • Conditions: support for Condition blocks with multiple operators.
  • Effects: Allow and Deny.

Supported Condition Operators:

  • StringEquals, StringNotEquals, StringEqualsIgnoreCase, StringNotEqualsIgnoreCase
  • StringLike, 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. ArnEquals and ArnLike behave identically, as do their negated forms. Service-principal trust policies use the same component-by-component ARN matching for aws:SourceArn.
  • NumericEquals, NumericNotEquals, NumericLessThan, NumericGreaterThan (and Equals variants)
  • DateEquals, DateNotEquals, DateLessThan, DateGreaterThan (and Equals variants)
  • Bool, IpAddress, NotIpAddress, Null
  • Supports ...IfExists variants for all operators.
  • Set operators ForAllValues: and ForAnyValue: over multi-valued condition keys, in AWS's own spelling (the prefix match is case-sensitive). They compose with IfExists (ForAnyValue:StringEqualsIfExists). ForAllValues: over an empty set matches vacuously and ForAnyValue: over an empty set does not match, so pair ForAllValues: with "Null":{"<key>":"false"} as you would on AWS: Null treats 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 makes ForAllValues:StringNotEquals on dynamodb:Attributes a 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, for ec2:RunInstances (TagSpecification.N), ec2:CreateTags (Tag.N) and s3:PutBucketTagging (the <Tagging> body).
  • aws:ResourceTag/<key>: the target resource's current tags, for ec2:CreateTags, ec2:DeleteTags, ec2:TerminateInstances and ec2:DescribeInstances (the first ResourceId.N or InstanceId.N), and for s3:GetBucketTagging, s3:DeleteBucketTagging and s3: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, for s3:GetObject, s3:GetObjectTagging, s3:GetObjectAcl, s3:PutObjectAcl, s3:DeleteObjectTagging and s3:PutObjectTagging. A versionId in the request selects the version whose tags are read. s3:DeleteObject and s3:PutObject do 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:PutObject reads these from the x-amz-tagging header and s3:PutObjectTagging from 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 with 400 InvalidTag. s3:RequestObjectTagKeys is not populated, so a condition on it never matches.
  • A PutObject carrying If-Match is authorized as s3:GetObject as well, and that second check is made without the object's tags in the context, as measured on AWS. If-None-Match needs 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, and arn:aws:iam::<account>:root for 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 the assumed-role session 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, for GetItem, PutItem, UpdateItem, DeleteItem, Query, BatchGetItem and BatchWriteItem (dynamodb:Select for Query and Scan). LeadingKeys holds the partition-key values the request names: from Key, Item, the KeyConditionExpression equality, the legacy KeyConditions EQ entry, or each RequestItems entry. Attributes holds the attribute names the request names (item and key fields, AttributesToGet, projection / update / filter / condition / key-condition expressions, and ExpressionAttributeNames); a request with no projection returns every attribute while reporting only the names it mentions, exactly as on AWS, which is why AWS pairs Attributes with dynamodb:Select. Each key is omitted when it cannot be determined (unknown table, a Key that omits the partition attribute, an unparseable KeyConditionExpression, 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.com yields AWSServiceRoleForLexBots there, where Floci derives AWSServiceRoleForLex. 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. DeleteServiceLinkedRole completes before it returns, so the task id it hands back is already finished and GetServiceLinkedRoleDeletionStatus always reports SUCCEEDED. The IN_PROGRESS, NOT_STARTED and FAILED states never occur, and no failure Reason is ever returned — a poll loop works, but its failure branch is never taken.
  • CreateRole accepts the /aws-service-role/ path, which AWS reserves. AWS rejects that prefix on CreateRole; 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, and DeleteServiceLinkedRole answers NoSuchEntity for 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