Skip to content

Policy Conditions

Ben McClelland edited this page Sep 18, 2026 · 2 revisions

Condition blocks in VersityGW use AWS's policy-condition language unchanged — the block's JSON shape, the AND/AND/OR combining rules, the operator semantics, and the IfExists / ForAllValues: / ForAnyValue: modifiers all behave as AWS's IAM condition-element reference describes them. This page covers only what differs: which condition keys actually exist, where the parser is stricter or looser than AWS's, and what an absent key does.

One engine, three key sets

Bucket policies, identity policies attached to an IAM user or role, and role trust policies all go through one shared condition implementation — the operator registry, IfExists, the set qualifiers, multi-value aggregation, and the ${...} rules are identical across the three. Only the available key set differs, and a key that is never populated for the policy type you are writing is simply absent at evaluation time. An absent key is by far the most common reason a policy copied from AWS documentation appears to do nothing, or the opposite of what it says — see When a key is absent.

Bucket policy Identity policy Role trust policy
Operator names validated at write time yes yes yes
Condition keys validated at write time yes — a fixed catalogue of 17 keys no — any key name is accepted no — any key name is accepted
Condition values validated at write time only aws:SourceIp, which must be an IP or CIDR no no
Default Version when the element is omitted 2008-10-17 empty (neither version) empty (neither version)

The conditions array inside a browser POST-Object form policy is a completely separate mechanism with its own grammar. Nothing on this page applies to it.


Operators

The operator set is complete: all 26 AWS comparison operators plus Null, each optionally carrying the IfExists suffix and one of the ForAllValues: / ForAnyValue: qualifiers.

  • String — StringEquals, StringNotEquals, StringEqualsIgnoreCase, StringNotEqualsIgnoreCase, StringLike, StringNotLike
  • Numeric — NumericEquals, NumericNotEquals, NumericLessThan, NumericLessThanEquals, NumericGreaterThan, NumericGreaterThanEquals
  • Date — DateEquals, DateNotEquals, DateLessThan, DateLessThanEquals, DateGreaterThan, DateGreaterThanEquals
  • Bool — Bool
  • Binary — BinaryEquals
  • Arn — ArnEquals, ArnLike, ArnNotEquals, ArnNotLike
  • IP — IpAddress, NotIpAddress
  • Presence — Null

Per-family behavior worth knowing before you write one:

Family Behavior
Numeric Both sides are parsed as float64. An unparseable value on either side makes the comparison false — never an error, never aborting evaluation.
Date Accepts RFC 3339 (with or without fractional seconds) and Unix epoch seconds (optionally fractional), and the two forms are interchangeable across a comparison. A bare calendar date (2026-06-15), ISO 8601 without the T separator or the zone offset, and RFC 1123 all fail to parse — such a policy silently never matches.
Bool Go's ParseBool, so "t", "1", "TRUE", "True" all parse, as does a bare JSON true / false. An unparseable value on either side is false. There is no BoolNotEquals — write {"Bool": {"aws:SecureTransport": "false"}}.
Binary BinaryEquals is a literal string comparison of the base64 text, not decode-then-compare.
Arn The same flat whole-string glob StringLike uses. There is no ARN-component-aware parsing — the ARN is never split into partition, service, region, account and resource — so ArnEquals ≡ ArnLike and ArnNotEquals ≡ ArnNotLike.
IP An unparseable range, or an unparseable request address, never matches. In a bucket policy aws:SourceIp is additionally validated at write time — see Keys available in bucket policies.
Null Any value other than true / false never holds — the condition fails closed. NullIfExists is rejected at write time. ForAllValues:Null and ForAnyValue:Null are accepted, with the qualifier a silent no-op; that is more permissive than AWS, so write bare Null and do not rely on it.

A few grammar points the parser enforces:

  • Condition key names are looked up case-insensitively — aws:SourceIp, AWS:SOURCEIP, and aws:sourceip are one key. Condition values stay case-sensitive, except under the IgnoreCase operators.
  • A value may be a JSON string, a bare number, or a bare boolean. Numbers keep their literal text, so 5.50 stays "5.50" and is not normalized to 5.5.
  • An absent, null, or empty Condition block always matches. So does an operator with an empty key map, such as {"StringEquals": {}}.
  • An unqualified negated operator requires every request value to fail against every policy value.

Rejected at the grammar level:

Written Why it is rejected
NullIfExists Null has no IfExists variant (neither does AWS)
stringequals, STRINGEQUALS, StringEQUALS operator names are case-sensitive
ForSomeValues:StringEquals, ForAllValue:StringEquals only ForAllValues: and ForAnyValue: are qualifiers
BinaryNotEquals, NumericLike, DateLike, BoolNotEquals no such operators (neither in AWS)
{"StringEquals": "foo"}, {"StringEquals": ["a"]} an operator's value must be an object of key/value pairs
{"StringEquals": {"k": null}}, {"StringEquals": {"k": [["a"]]}}, {"StringEquals": {"k": {}}} a condition value must be a string, number, or boolean scalar, or an array of those

What is not supported

The operator set is complete. What is missing is condition keys, plus four gateway-wide gaps:

  • No policy-variable substitution outside the String and Arn families. A ${...} placeholder inside a Numeric*, Date*, Bool, BinaryEquals, IpAddress, NotIpAddress, or Null value is left as literal text and simply fails to match.
  • No condition-key validation for identity or trust policies. Any key name is accepted at write time, so a typo such as aws:SourceIPAddress is stored and evaluates as an absent key forever after. (aws:SourceIP does resolve, because key lookup is case-insensitive.)
  • No value validation for identity or trust policies. A malformed CIDR, an unparseable date, or a non-numeric numeric value is accepted at write time and quietly never matches.
  • No editing support in the WebGUI. Policy documents, Condition blocks included, are edited as raw JSON.

What happens when an unsupported operator appears

An unrecognized operator name is rejected at write time, not ignored at evaluation time. Which error you get depends on the policy type:

API Error code HTTP Message
PutBucketPolicy MalformedPolicy 400 Invalid Condition type : <Name> — the name is echoed exactly as written
PutUserPolicy, PutRolePolicy, and the Policy session-policy parameter MalformedPolicyDocument 400 Syntax errors in policy.
CreateRole, UpdateAssumeRolePolicy MalformedPolicyDocument 400 Syntax error in policy. (singular, unlike the identity-policy message)

Identity and trust policies reject two further things inside a Condition block that a bucket policy accepts: a duplicate JSON key at any nesting depth — two of the same operator, or the same condition key twice inside one operator — is rejected outright rather than silently keeping the last occurrence, and unknown fields on a statement object are rejected.

Important

If a document containing an unrecognized operator does reach storage anyway — written before this validation existed, restored from a backup, or written out of band — the Condition cannot be evaluated and every evaluator fails closed, on the reasoning that an unevaluable condition might have been hiding a Deny. A bucket policy denies the request. An identity policy denies the entire evaluation, including every other document attached to the principal. A trust policy produces AccessDenied from AssumeRoleWithWebIdentity. Run with --log-level debug (Debugging) to see policy condition block failed to parse in the log.

Condition keys that are not supported

VersityGW populates none of the keys below. A bucket policy rejects them at write time with MalformedPolicy / Policy has an invalid condition key; an identity or trust policy accepts them and then evaluates them as absent forever — exactly the fail-open shape described in When a key is absent.

  • Tags on S3 objects — s3:ExistingObjectTag/<key>, s3:RequestObjectTag/<key>, s3:RequestObjectTagKeys, and aws:ResourceTag/<key> naming an S3 bucket or object tag.
  • Object Lock — every s3:object-lock-* key.
  • Encryption, storage class, copy — s3:x-amz-server-side-encryption, s3:x-amz-server-side-encryption-aws-kms-key-id, s3:x-amz-storage-class, s3:x-amz-copy-source, s3:x-amz-metadata-directive.
  • Request shape — s3:signatureversion, s3:authType, s3:TlsVersion, s3:DataAccessPointArn, s3:AccessPointNetworkOrigin.
  • MFA, VPC, cross-account, organization, region, federation — aws:MultiFactorAuthPresent, aws:SourceVpc, aws:SourceVpce, aws:VpcSourceIp, aws:SourceArn, aws:SourceAccount, aws:SourceOwner, aws:PrincipalOrgID, aws:PrincipalOrgPaths, aws:PrincipalIsAWSService, aws:RequestedRegion, aws:ViaAWSService, aws:CalledVia, aws:CalledViaFirst, aws:CalledViaLast, aws:FederatedProvider, aws:TokenIssueTime.

One exception to the write-time rule: aws:MultiFactorAuthAge is accepted by PutBucketPolicy, though never populated, while aws:MultiFactorAuthPresent is rejected.

There is no MFA concept in the gateway, no VPC concept, no region concept, and no cross-account concept — this deployment is a single account, 000000000000. S3 bucket and object tags are not exposed as condition keys anywhere.


Condition keys

Keys available in bucket policies

A bucket policy's Condition is evaluated entirely on the S3 gateway, against a context built from the HTTP request. PutBucketPolicy accepts exactly 17 key names — anything else is rejected with MalformedPolicy / Policy has an invalid condition key — and of those 17, 13 are ever populated.

Key Operator family Source Present when
aws:CurrentTime Date server time, RFC 3339 with second precision every request
aws:EpochTime Numeric or Date server time, integer epoch seconds every request
aws:SecureTransport Bool whether the S3 connection is TLS every request
aws:SourceIp IpAddress the connection's peer address every request (see the proxy caveat below)
aws:UserAgent String the User-Agent header when the header is non-empty
aws:Referer String the Referer header when the header is non-empty
s3:prefix String the prefix query parameter when ?prefix= is non-empty
s3:delimiter String the delimiter query parameter when ?delimiter= is non-empty
s3:max-keys Numeric or String the max-keys query parameter, unparsed when ?max-keys= is non-empty
s3:x-amz-acl String the X-Amz-Acl header when the header is non-empty
s3:VersionId String the versionId query parameter when ?versionId= is non-empty
s3:if-match String the If-Match header, with one pair of surrounding ETag quotes removed conditional PutObject, CompleteMultipartUpload, or unversioned DeleteObject with a non-empty header
s3:if-none-match String the If-None-Match header, with one pair of surrounding ETag quotes removed conditional PutObject or CompleteMultipartUpload with a non-empty header
aws:PrincipalArn, aws:username, aws:userid, aws:MultiFactorAuthAge — — never — see the warning below

The same request context is used for anonymous and public requests, so a public statement's Condition sees the same request-derived facts an authenticated one would.

The seven s3: keys carry a write-time action restriction: a statement naming one of them must name only actions the key applies to.

Key Allowed actions
s3:prefix, s3:delimiter, s3:max-keys s3:ListBucket, s3:ListBucketVersions
s3:x-amz-acl s3:PutObject, s3:PutBucketAcl, s3:PutObjectAcl
s3:VersionId s3:GetObjectVersion, s3:DeleteObjectVersion, s3:GetObjectVersionAttributes, s3:GetObjectVersionTagging, s3:PutObjectVersionTagging, s3:DeleteObjectVersionTagging
s3:if-match s3:PutObject, s3:DeleteObject
s3:if-none-match s3:PutObject

Every concrete action named in the statement must be in the list — an explicit ["s3:GetObject", "s3:PutObject"] with s3:x-amz-acl is rejected even though s3:PutObject alone would be accepted — and a violation returns MalformedPolicy / Conditions do not apply to combination of actions and resources in statement. Note that s3:ListBucketMultipartUploads is deliberately not in the list-action set. An action pattern containing * or ? is exempt from the check entirely, so "Action": "s3:*" accepts any key from the catalogue.

aws:SourceIp is additionally validated as an IP address or CIDR range at write time, no matter which operator wraps it: {"Null": {"aws:SourceIp": "true"}} is rejected with Invalid IP address in Conditions for that reason. The check is keyed to the key, not to the operator.

Two consequences of the context being built from the request alone:

  • It is not action-gated. s3:prefix, s3:delimiter, s3:max-keys, s3:x-amz-acl, and s3:VersionId are populated from the query string and headers for every operation, not only the ones the write-time catalogue permits. A statement with "Action": "s3:*" may legally carry s3:prefix, and that condition is then also evaluated against, say, a GetObject whose URL happens to carry ?prefix=.
  • The conditional-write keys are action-gated. Unlike the five keys above, s3:if-match and s3:if-none-match are populated only when the header controls an object write. They remain absent for GET/HEAD cache conditions, CopyObject source preconditions, UploadPart, UploadPartCopy, DeleteObjects, versioned deletes, and object subresource operations. CompleteMultipartUpload is included because it is authorized as s3:PutObject.
  • aws:SourceIp is the real peer address. X-Forwarded-For and other proxy headers are deliberately not trusted — honouring one would make aws:SourceIp client-controlled. Behind a reverse proxy or load balancer aws:SourceIp is therefore the proxy's address, and an IP-based policy will not distinguish clients; enforce source-IP restrictions at the proxy in that topology.

Keys available in identity policies

An identity policy is an inline policy on an IAM user or role, evaluated by the standalone IAM service. Two request paths reach it, carrying different context: an S3 request, where the gateway sends the request-derived keys it observed over the private endpoint and the IAM service overlays the identity-derived keys it alone can know; and an IAM API request, where the IAM service builds the whole context itself.

Key Operator family Source S3 request IAM API request
aws:CurrentTime Date request time, RFC 3339 yes yes
aws:EpochTime Numeric or Date request time, epoch seconds yes yes
aws:SecureTransport Bool TLS on the connection yes yes
aws:SourceIp IpAddress peer address yes yes
aws:UserAgent String User-Agent header yes no
aws:Referer String Referer header yes no
s3:prefix, s3:delimiter, s3:max-keys, s3:x-amz-acl, s3:VersionId, s3:if-match, s3:if-none-match String / Numeric the S3 request, as in the bucket-policy table above yes no
aws:PrincipalArn String or Arn the user's Arn, or the assumed role's ARN (arn:aws:iam::000000000000:role/<RoleName>) for a session yes yes
aws:PrincipalAccount String always 000000000000 yes yes
aws:PrincipalType String User or AssumedRole yes yes
aws:username String the IAM user's name. Absent for an assumed-role session users only users only
aws:userid String the user's id, or <RoleId>:<RoleSessionName> for a session yes yes
aws:PrincipalTag/<key> String the calling user's own IAM tags, or the assumed role's tags for a session yes yes
iam:ResourceTag/<key> and aws:ResourceTag/<key> String the tags on the target IAM entity of the call, under both key forms no yes
aws:RequestTag/<key> String the Tags parameter of the call no CreateUser, CreateRole, TagUser, TagRole, CreateOpenIDConnectProvider, TagOpenIDConnectProvider
aws:TagKeys String, multi-valued the tag keys the call supplies no the actions above, plus UntagUser, UntagRole, UntagOpenIDConnectProvider

Notes that matter in practice:

  • aws:PrincipalArn for a session is the role's ARN, not the session's. A Condition on it therefore applies to every session of that role and can never single one out; aws:userid, which carries <RoleId>:<RoleSessionName>, is the key that can. Denial messages are different — they still name the session by its arn:aws:sts::000000000000:assumed-role/<role>/<session> ARN.
  • aws:username is absent for a session. Anything conditioned on it silently changes meaning once the caller assumes a role instead of using long-term keys. Use aws:PrincipalArn or aws:userid when you need to cover both.
  • aws:PrincipalTag/<key> for a session comes from the role's tags. There is no session-tag parameter on AssumeRoleWithWebIdentity, so the role's tags are the session's tags for the session's whole lifetime.
  • aws:TagKeys is the only multi-valued key on this path, and the only one where the set qualifiers do anything useful. A request may carry at most 50 tags, so at most 50 aws:RequestTag/<key> entries can appear.
  • Tag-key casing does not reach a Condition. The context is keyed with the tag key exactly as supplied, but condition-key names are looked up case-insensitively over the whole name, /<key> suffix included. So aws:RequestTag/env and aws:RequestTag/ENV are the same condition key even on an OIDC provider, where env and ENV are two independent tags: a Condition cannot single out one casing, and when one request supplies both, the value the condition sees is whichever key the lookup finds first. aws:TagKeys values are unaffected — they are compared by the operator, so StringEquals on them stays case-sensitive.
  • aws:ResourceTag/<key> never describes an S3 bucket or object. On the S3 path it is stripped from the gateway's contribution and never re-added.
  • The IAM service filters every identity- and resource-namespaced key out of the gateway's contribution before overlaying its own, rather than merging with override-on-collision. A gateway cannot smuggle in aws:PrincipalTag/x for an untagged role, which is precisely the value a StringNotEquals-guarded Allow keys off.
  • The root account bypasses policy evaluation entirely — no condition, and no identity policy, applies to it. See Identity Policies.

Keys available in role trust policies

A role's AssumeRolePolicyDocument is evaluated during AssumeRoleWithWebIdentity, against the claims of the presented OIDC token. There is no caller identity yet, so no aws:PrincipalArn, aws:username, or aws:userid exists here. <provider> below is the token's iss claim with a leading https:// stripped — for example token.actions.githubusercontent.com.

Key Source
<provider>:sub the sub claim
<provider>:aud the effective audience: the azp claim when present, otherwise the single aud value
<provider>:oaud the original aud value(s) — set only when azp is present
<provider>:<claim> every other top-level scalar or scalar-array claim
aws:SourceIp peer address, when non-empty
aws:SecureTransport whether the connection is TLS — always present
aws:CurrentTime, aws:EpochTime request time
sts:RoleSessionName the RoleSessionName request parameter, when non-empty. This key exists only in trust policies

iss, aud, azp, and sub are excluded from the generic <provider>:<claim> projection — the first is the provider itself and the rest have dedicated keys — as are exp, iat, and nbf, which are timestamps rather than condition material.

Claim values are coerced to strings, which is what makes Bool, Numeric*, and Date* conditions against custom claims work: a JSON string is taken as-is, a number is formatted without a trailing .0, a boolean becomes "true" / "false", a nested object or null is dropped, a JSON array yields a multi-valued key, and an array with no usable scalars produces no key at all.

A trust statement whose Condition fails yields InvalidIdentityToken, not AccessDenied; an explicit Deny, or a condition that cannot be evaluated, yields AccessDenied. See STS & Web Identity for the full flow, the error matrix, and the extra rule that a trust policy naming a shared-audience OIDC provider must scope that provider's tenancy claim with StringEquals, StringLike, or StringEqualsIgnoreCase.

Identity-derived keys are not available in bucket policies

Warning

aws:PrincipalArn, aws:username, aws:userid, and aws:PrincipalTag/<key> never match in a bucket policy. The S3 gateway evaluates bucket policies on its own and has no way to know who an access key belongs to — resolving that is the standalone IAM service's job, and it only does so while evaluating an identity policy.

  • The first three are nonetheless accepted by PutBucketPolicy; aws:PrincipalTag/<key> is rejected as an invalid condition key.
  • A positive operator on any of them therefore never matches, and a negated one always matches for everybody: on an Allow that grants every principal the statement names, on a Deny it denies every one of them.
  • To scope a bucket policy to particular callers, use the statement's Principal element. To scope by an identity attribute such as a tag, use an identity policy attached to the user or role.

When a key is absent

A condition key is "absent" whenever the request context does not carry it — a key VersityGW never populates, a key populated only under certain circumstances (an empty User-Agent, a ListObjectsV2 with no prefix), or a plain typo in the key name.

Operator shape Key absent means
Null with "true" condition holds
Null with "false" condition fails
Null with any other value condition fails
positive operator, no qualifier, no IfExists — StringEquals, IpAddress, Bool, … condition fails, so the statement does not apply
positive operator with IfExists condition holds
any negated operator — StringNotEquals, ArnNotLike, NotIpAddress, … — with or without IfExists condition holds (vacuously true)
ForAllValues: with anything condition holds (vacuously true)
ForAnyValue: with a positive operator condition fails, unless IfExists is also present
ForAnyValue: with a negated operator condition holds

A key that is present but carries an empty value list behaves like an absent key for the set qualifiers — ForAllValues: (and an unqualified negated operator) is vacuously true, ForAnyValue: (and an unqualified positive operator) is vacuously false — but Null still sees it as present.

Important

This is the most important safety property on this page. A Deny guarded by a negated operator on a key that is never populated fires for everyone; an Allow guarded the same way grants everyone. Before writing NotIpAddress, StringNotEquals, or ArnNotEquals, confirm from the tables above that the key is actually populated for the policy type you are writing. The safe pattern is to pair the guard with a presence test on the same key, since the two blocks are ANDed:

"Condition": {
  "Null": { "aws:SourceIp": "false" },
  "NotIpAddress": { "aws:SourceIp": "203.0.113.0/24" }
}

Policy variables in condition values

A ${...} placeholder inside a condition value is replaced with the request context's value for the named key. Substitution happens only when the enclosing document's Version is exactly "2012-10-17" and the operator's base name is in the String or Arn family; everywhere else the ${...} stays literal text.

  • A placeholder name may contain letters, digits, _, :, ., and -. It resolves with the same case-insensitive lookup used for condition keys, so ${aws:username} and ${AWS:UserName} are the same variable.
  • A placeholder that cannot be resolved — the named key is absent, or holds more than one value — is left literal, never expanded to an empty string. That is the safe fallback: the condition simply fails to match rather than silently collapsing a Deny into a no-op.
  • A bucket policy that omits Version defaults to 2008-10-17, which disables substitution — write "Version": "2012-10-17" explicitly. 2012-10-17 and 2008-10-17 are the only accepted values; anything else is rejected with The policy must contain a valid version string. An identity or trust policy may omit Version entirely, which likewise disables substitution.
  • The same substitution applies to an identity policy's Resource and NotResource patterns, under the same version rule. It does not apply to a bucket policy's Resource patterns, which are always matched literally.

Example

A bucket policy allowing one principal to list media, but only under the photos/ prefix.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListPhotosPrefixOnly",
      "Effect": "Allow",
      "Principal": "arn:aws:iam::000000000000:user/alice",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::media",
      "Condition": {
        "StringEquals": { "s3:prefix": "photos/" }
      }
    }
  ]
}

The Principal form depends on the IAM backend — a principal ARN under the standalone IAM service, the account's access key ID under the built-in backends; see Bucket Policies for the accepted forms. PutBucketPolicy rejects a principal that does not resolve, with MalformedPolicy / Invalid principal in policy.

Require create-only uploads

The following bucket policy follows the AWS conditional-write enforcement pattern. The explicit Deny rejects any upload that does not carry If-None-Match; the unconditional Allow supplies the underlying permission:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowObjectUploads",
      "Effect": "Allow",
      "Principal": "arn:aws:iam::000000000000:user/alice",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::media/*"
    },
    {
      "Sid": "RequireIfNoneMatch",
      "Effect": "Deny",
      "Principal": "arn:aws:iam::000000000000:user/alice",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::media/*",
      "Condition": {
        "Null": { "s3:if-none-match": "true" }
      }
    }
  ]
}

Clients must send:

If-None-Match: *

The * is preserved as the condition value. With StringEquals, it is a literal asterisk, not a wildcard. A request without the header is denied by the policy. A request with the header is authorized, after which the storage backend evaluates the precondition and returns 412 PreconditionFailed if the object already exists.

Allow an overwrite only for a specific ETag

This statement grants an overwrite only when the request names the expected ETag:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReplaceKnownObjectVersion",
      "Effect": "Allow",
      "Principal": "arn:aws:iam::000000000000:user/alice",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::media/catalog.json",
      "Condition": {
        "StringEquals": {
          "s3:if-match": "686897696a7c876b7e"
        }
      }
    }
  ]
}

Although an HTTP ETag is normally quoted, write the policy value without those quotes. Both If-Match: "686897696a7c876b7e" and If-Match: 686897696a7c876b7e produce the bare condition value 686897696a7c876b7e. A missing or different header fails authorization. A matching header passes policy evaluation, but the backend still compares it with the current object's ETag and returns 412 PreconditionFailed if the condition is stale.

Use the same key with s3:DeleteObject to authorize conditional, unversioned deletes. It does not apply to s3:DeleteObjectVersion. To allow any conditional write without pinning a particular ETag, use:

"Condition": {
  "Null": { "s3:if-match": "false" }
}

These keys control authorization; they do not make the underlying update atomic by themselves. The selected storage backend must also support conditional object publication. See the conditional publish lock modes under POSIX Backend and ScoutFS Backend.


See Also

Clone this wiki locally