OpenSearch "User: anonymous is not authorized to perform: es:ESHttpGet": Why Dashboards Can Never Sign, and the Fixes (SigV4, Fine-Grained Access Control, IP and VPC Policies)

Logeshwaran
—

"User: anonymous is not authorized to perform: es:ESHttpGet" means your request reached the Amazon OpenSearch Service domain without an AWS signature, and the domain's access policy only lets in signed requests from specific IAM users or roles. "Anonymous" does not mean a stranger: it is what OpenSearch calls you when your browser, curl command or client library forgot to sign the request with AWS Signature Version 4. The surprise that sends most people in circles is that OpenSearch Dashboards can never send signed requests, because a browser cannot sign them, so a domain whose access policy lists only IAM roles locks every human out of Dashboards by design, including the administrator who created it. The fix depends on who is calling: for code and scripts, sign the requests (curl --aws-sigv4, awscurl, or the OpenSearch client's SigV4 signer); for people using Dashboards, turn on fine-grained access control with SAML, Amazon Cognito or the internal user database (then an open access policy is safe), or allow your IP addresses; for VPC domains, use an open policy and let security groups decide who reaches the endpoint. This page shows each fix with working policies and code.

Jake's shop search box ran on a small OpenSearch domain that Ethan set up with an access policy allowing only the app's IAM role, which was exactly right for the app. The trouble started when Jake asked to see the search dashboards. Ethan opened the Dashboards link in his browser and got a white page with one line of JSON: {"Message":"User: anonymous is not authorized to perform: es:ESHttpGet"}. "Anonymous?" Jake said. "You built the thing." "To OpenSearch, a browser is always anonymous," Ethan said. "It cannot sign anything." This page is the two-minute fix Ethan was tempted to make, why he did not make it, and the setup he chose instead.

⚡ Quick Answer

• What it means: an unsigned request hit a domain whose policy allows only IAM principals. The error.

• From code or curl: sign with SigV4, service name es. Signed requests.

• From Dashboards in a browser: fine-grained access control with SAML, Cognito or internal users, or an IP-based policy. Dashboards access.

• VPC domain: open access policy plus security groups; you may see timeouts, not 403. VPC domains.

• Never: an open policy on a public domain without fine-grained access control. Why not.

Which fix for you · Policy examples · Fine-grained access control · Logstash and Fluent Bit · Troubleshooting.

New to OpenSearch Service? A domain is a managed OpenSearch cluster with an HTTPS endpoint you send search and indexing requests to, plus OpenSearch Dashboards, a web interface at https://your-domain-endpoint/_dashboards/. Who may call the endpoint is decided by the domain access policy (a resource-based policy, written in the same JSON as any IAM policy), optionally by fine-grained access control inside OpenSearch, and, for domains in a VPC, by security groups. Our smallest domain guide covers setup and cost.

🧭 NEW HERE? READ THESE FIRST

New to OpenSearch, IAM policies or Cognito? These five pages cover the basics this fix builds on:

💡 Note whether your domain has a public endpoint or VPC access before you change anything; the right fix is different for each.

What "User: anonymous is not authorized" actually means

Every request to an OpenSearch Service domain is checked against the domain access policy. A request that carries an AWS Signature Version 4 signature is identified as the IAM user or role that signed it. A request with no signature has no identity, so IAM names it anonymous. If the policy only allows specific IAM principals, an unsigned request matches nothing and is denied with HTTP 403:

{"Message":"User: anonymous is not authorized to perform: es:ESHttpGet"}

The action at the end is the HTTP method you used: es:ESHttpGet for searches and reads, es:ESHttpPost or es:ESHttpPut for indexing, es:ESHttpDelete, es:ESHttpHead. Three situations produce it, and they need different fixes:

  • A browser opening Dashboards on a domain whose policy lists only IAM roles. The Dashboards endpoint does not support signed requests at all.
  • A script or tool sending plain HTTP: curl, Postman, a Python requests call, a Lambda function using a generic HTTP client, Logstash or Fluent Bit without AWS authentication turned on.
  • A client that signs, but with the wrong service name or Region, which usually produces a signature error rather than "anonymous", covered in troubleshooting.

A close cousin, User: arn:aws:sts::111122223333:assumed-role/booking-app/... is not authorized to perform: es:ESHttpPost, means the request was signed but the signer is not allowed by the policy. That one is fixed by adding the role to the policy, not by signing.

Read the error to find the cause in one minute

The exact text of the refusal tells you which layer said no, so read it before you change anything:

  1. User: anonymous is not authorized: the request had no signature. The fix is signing (code, curl, shippers) or a sign-in method for people (Dashboards).
  2. User: arn:aws:...:assumed-role/NAME/... is not authorized: the request was signed by that role, and the domain access policy does not allow it. Add the role to the policy.
  3. security_exception ... no permissions for [...]: the access policy let you in, and fine-grained access control refused. Map the user or role to an OpenSearch role with that permission.
  4. Unauthorized with fine-grained access control: the username or password was wrong.
  5. A timeout: the request never reached the domain. Check the VPC, security groups and DNS.
  6. A signature error (The request signature we calculated does not match...): the request was signed, but with the wrong Region, service name or credentials.

One more clue: data requests to the domain (searches, indexing) are not recorded in AWS CloudTrail, which logs only the configuration API. If you need to see who searched what, turn on fine-grained access control's audit logs, which go to CloudWatch Logs.

Which fix is right for you

Who is callingPublic endpointVPC endpoint
Your application or Lambda functionSign requests with its IAM role; allow the role in the policySame, from inside the VPC; security group allows the app
You, from a terminalcurl --aws-sigv4 or awscurl with your credentialsSame, from a host in the VPC (or over VPN)
People using DashboardsFine-grained access control with SAML, Cognito or internal users; or an IP-based policyOpen policy, security groups, plus fine-grained access control for per-user permissions
Log shippers (Logstash, Fluent Bit, OpenSearch Ingestion)Turn on their AWS SigV4 option with an IAM roleSame, with network access to the VPC

If you are not sure whether your domain is public or in a VPC, the console's domain overview shows it under Network, and the endpoint name gives it away too: public endpoints start with search-, VPC endpoints with vpc-. Many domains need two of these at once, for example an app that signs and people who use Dashboards. That combination is exactly what fine-grained access control was designed for, and it is what Ethan ended up with.

Sign requests from curl, scripts and code

From a terminal, modern curl can sign requests itself with your AWS credentials. The service name for OpenSearch Service domains is es:

export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... AWS_SESSION_TOKEN=...
curl --aws-sigv4 "aws:amz:us-east-1:es" \
  --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
  -H "x-amz-security-token: $AWS_SESSION_TOKEN" \
  "https://search-booking-abc123.us-east-1.es.amazonaws.com/_cat/indices?v"

The x-amz-security-token header is needed only with temporary credentials (an assumed role or SSO session); leave it out for long-term keys. Alternatively, awscurl (a small open-source tool installed with pip install awscurl) reads your AWS profile and signs for you: awscurl --service es --region us-east-1 "https://search-booking-abc123.us-east-1.es.amazonaws.com/_cat/indices?v".

In Python, the official opensearch-py client includes a SigV4 signer that uses whatever credentials boto3 finds (environment, profile, or the Lambda function's role):

import boto3
from opensearchpy import OpenSearch, RequestsHttpConnection, AWSV4SignerAuth

region = "us-east-1"
credentials = boto3.Session().get_credentials()
auth = AWSV4SignerAuth(credentials, region, "es")

client = OpenSearch(
    hosts=[{"host": "search-booking-abc123.us-east-1.es.amazonaws.com", "port": 443}],
    http_auth=auth, use_ssl=True, verify_certs=True,
    connection_class=RequestsHttpConnection,
)
print(client.search(index="repairs", body={"query": {"match": {"model": "pixel"}}}))

In Node.js, the @opensearch-project/opensearch client has AwsSigv4Signer for the same job. Whatever language you use, three details must line up with the domain or the signature fails: the Region of the domain, the service name (es for managed domains, aoss for OpenSearch Serverless collections), and the credentials of a principal the access policy allows.

Domain access policies that work, with examples

Change the policy in the console (domain, Security configuration, Edit, Access policy) or with aws opensearch update-domain-config --domain-name booking --access-policies file://policy.json. These are the patterns you will use; replace the account, Region and domain:

1. Allow one application role (signed requests only). The right policy for an app on a public domain without fine-grained access control:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "arn:aws:iam::111122223333:role/booking-app" },
    "Action": "es:ESHttp*",
    "Resource": "arn:aws:es:us-east-1:111122223333:domain/booking/*"
  }]
}

2. Allow an office IP range (unsigned requests from those addresses). An IP-based policy is a resource-based policy with an anonymous principal and a SourceIp condition; it is the one policy shape that allows unsigned clients, such as a browser on Dashboards:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "*" },
    "Action": "es:ESHttp*",
    "Condition": { "IpAddress": { "aws:SourceIp": ["203.0.113.0/24"] } },
    "Resource": "arn:aws:es:us-east-1:111122223333:domain/booking/*"
  }]
}

3. Both together. Put both statements in one policy to let the app in by role from anywhere and people in by IP from the office. Note that an IP condition and a role in the same statement mean both must be true (signed by that role and from that range), which is the tighter option recommended for public domains without fine-grained access control.

4. Open policy, with fine-grained access control or a VPC. If fine-grained access control is on, or the domain is in a VPC, you can let every request through the access policy and do the real checking elsewhere:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "*" },
    "Action": "es:ESHttp*",
    "Resource": "arn:aws:es:us-east-1:111122223333:domain/booking/*"
  }]
}

Two details in every example matter more than they look. The trailing /* in the resource means "everything inside the domain" (its indexes and APIs); without it the statement covers only the domain itself and no search is allowed. And these policies govern only the data endpoint; changing the domain's configuration (the es:UpdateDomainConfig family) needs an identity-based IAM policy on your user or role. A policy change is applied by the service and usually takes effect within a few minutes; wait for the domain's status to show it as active before testing.

Which es:ESHttp action each request needs

If you write narrow policies, the HTTP method decides the action, and the method is not always the one you expect:

ActionHTTP methodTypical requests
es:ESHttpGetGETGet a document, _cat APIs, cluster health, a search with query-string parameters
es:ESHttpPostPOSTSearches with a JSON body (_search), _bulk, _msearch, indexing without an ID
es:ESHttpPutPUTCreate an index, put a mapping, index a document with an ID
es:ESHttpDeleteDELETEDelete a document or an index
es:ESHttpHeadHEAD"Does this index exist?" checks that many clients send on startup
es:ESHttpPatchPATCHSecurity plugin API changes and a few others

Before you tighten actions, check which methods your client really sends; the domain's slow logs and audit logs show them, and so does a quick test with each method from curl. The trap: most client libraries send searches as POST with a JSON body, so a "read-only" policy allowing only es:ESHttpGet blocks the searches it was written for. A read-only app usually needs es:ESHttpGet, es:ESHttpHead and es:ESHttpPost restricted to search paths, for example arn:aws:es:us-east-1:111122223333:domain/booking/repairs/_search. When you need truly read-only access for people, fine-grained access control's read-only roles are easier to get right than path-by-path IAM statements.

Policy mistakes that look right and still say no

When a policy "clearly allows" your role and the error continues, the cause is usually one of these:

  • The wrong account or Region in the resource ARN. arn:aws:es:us-east-1:111122223333:domain/booking/* must match the domain's real Region and account exactly; copy the domain ARN from the console instead of typing it.
  • A role from IAM Identity Center without its path. Single sign-on roles live under a path, such as arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_Admin_0123456789abcdef. A principal written without the path does not match.
  • An assumed-role session ARN as the principal. Policies name the role (role/booking-app), not the session (assumed-role/booking-app/session-name), which changes on every login.
  • A deny elsewhere. An explicit deny in the caller's own IAM policy, a permissions boundary or an organization policy wins over the domain's allow. Our explicit deny guide shows how to find it.
  • Testing too soon. A policy change takes a few minutes to apply; check the domain status before retrying.

The tempting fix that you should not make

The quickest way to make the error disappear is policy 4 on a public domain without fine-grained access control. It works instantly, and it means anyone on the internet who finds your endpoint can read, change and delete every index, with no password. Open OpenSearch and Elasticsearch clusters are one of the most common sources of data leaks, precisely because this fix is so easy. If you see an open policy on a public domain in your account, treat it as urgent: replace it with policy 1, 2 or 3, or turn on fine-grained access control first.

"So the quick fix is a lock with the door left open," Jake said when Ethan explained why he was not using it. "The quick fix is no door," Ethan said.

Give people access to OpenSearch Dashboards

Dashboards does not support IAM users and roles natively, so people need one of these:

  1. Fine-grained access control with SAML: people sign in through your identity provider (Okta, Microsoft Entra ID, IAM Identity Center and others), and their groups map to OpenSearch roles. Best for companies with single sign-on.
  2. Fine-grained access control with Amazon Cognito: a Cognito user pool handles sign-in, and Cognito identity pool roles map to OpenSearch roles. A good fit if you already use Cognito.
  3. Fine-grained access control with the internal user database: users and passwords stored in OpenSearch itself, entered in a sign-in page (HTTP basic authentication). Simplest for a small team.
  4. An IP-based policy for public domains: anyone at the allowed addresses can use Dashboards without signing in. Workable for a fixed office range, impractical for people working from many places, and everyone gets the same access.
  5. A proxy in front of a public domain, so all Dashboards traffic comes from the proxy's IP address, which the IP-based policy allows. More moving parts; usually only worth it if the options above are not available.

For Jake's shop, Ethan chose option 3: fine-grained access control with a master user in the internal user database, an open access policy (safe now that every request is authenticated inside OpenSearch), a read-only internal user for Jake, and the app still signing with its IAM role. Jake opens Dashboards, types his username and password, and sees the search reports without being able to delete an index.

Fine-grained access control: how it changes the picture

Fine-grained access control (FGAC) adds users, roles and permissions inside OpenSearch, down to individual indexes, documents and fields. With it turned on, the request path has two checks: the domain access policy first, then FGAC. The details that matter for this error:

  • The master user is set when you enable FGAC, either as an IAM ARN or as a username and password in the internal user database. It is mapped to the all_access and security_manager roles, so it can manage everyone else from the Security page in Dashboards.
  • IAM roles map to OpenSearch roles as backend roles. An app signing with the booking-app role gets exactly the OpenSearch permissions of the role you map it to, for example a custom role allowed only to write to repairs* indexes.
  • The FGAC denial looks different. A user who passes the access policy but lacks OpenSearch permissions gets a security_exception with a reason such as no permissions for [indices:data/read/search] and User [name=limited-user, roles=[], ...] and status 403. That means "map this user to a role", not "sign your request".
  • Signed and unsigned requests are handled differently. When the access policy names an IAM principal, signed requests must pass both the policy and FGAC, while requests using HTTP basic authentication with internal users skip the access policy and are controlled entirely by FGAC roles. You cannot sign a request and send a username and password in the same request.

Turning on FGAC for an existing domain is a configuration change that requires encryption at rest, node-to-node encryption and HTTPS to be enabled, and it cannot be turned off afterwards; plan it as a one-way step and do it at a quiet time.

SAML single sign-on for Dashboards

If your company already signs in through an identity provider, SAML is usually the best way to put Dashboards behind it. It needs fine-grained access control, and the setup is a short exchange of details between the domain and the provider:

  • In the domain's Security configuration, enable SAML authentication, copy the service provider entity ID and the sign-in URLs the console shows, and paste them into a new SAML application in your identity provider.
  • From the identity provider, download the metadata XML and upload it to the domain, along with the provider's entity ID.
  • Choose the SAML attribute that carries the user's groups (the roles key), and name a group as the SAML master backend role, so its members can manage security inside OpenSearch.
  • Map other groups to OpenSearch roles (for example a "support" group to readall and opensearch_dashboards_user).

SAML covers people only; your applications still sign their requests with IAM roles, which you allow in the access policy (or map in fine-grained access control). Once SAML works, the Dashboards link sends people through your usual company sign-in, and "anonymous" never appears for them again.

Set up Amazon Cognito sign-in for Dashboards

If you choose Cognito for Dashboards, these are the pieces and the order to create them in. The console's "Enable Amazon Cognito authentication" option can create several of them for you, but knowing what each does makes the errors make sense:

  1. A Cognito user pool with the users who may sign in (and, optionally, a SAML or social identity provider connected to it). Self sign-up is usually off for this use.
  2. A Cognito identity pool in the same Region, linked to the user pool. Its authenticated role is the IAM role that signed-in users take on.
  3. An IAM role for OpenSearch Service with the managed policy AmazonOpenSearchServiceCognitoAccess attached (the console names it CognitoAccessForAmazonOpenSearch), which lets the service configure the user and identity pools.
  4. Enable Cognito authentication on the domain (Security configuration, Edit), choosing the user pool, identity pool and that role.
  5. Allow the authenticated role in the domain access policy, for example "Principal": {"AWS": "arn:aws:iam::111122223333:role/Cognito_BookingAuth_Role"} with es:ESHttp*. Dashboards then makes signed requests on the user's behalf using that role.
  6. Test with a user from the pool in a private browser window.

Step 5 is the one people miss: Cognito signs the user in correctly, and then the requests are refused because the access policy does not allow the identity pool's role. If you also use fine-grained access control, map that role (or per-group roles from the identity pool) to OpenSearch roles, so different Cognito groups get different permissions. The user pool and identity pool can be shared by several domains.

Map users and roles with fine-grained access control

With fine-grained access control on, signing in or signing a request only proves who you are; what you may do comes from role mappings. You can manage them in Dashboards (Security, Roles, choose a role, Mapped users) or with the security REST API as the master user. To let the app's IAM role write to the repair indexes, create a role and map the IAM role ARN to it as a backend role:

PUT _plugins/_security/api/roles/repairs_writer
{
  "cluster_permissions": ["cluster_composite_ops"],
  "index_permissions": [{
    "index_patterns": ["repairs*"],
    "allowed_actions": ["crud", "create_index"]
  }]
}

PUT _plugins/_security/api/rolesmapping/repairs_writer
{
  "backend_roles": ["arn:aws:iam::111122223333:role/booking-app"]
}

For a person in the internal user database, create the user and map them by name; this gives Jake a read-only login:

PUT _plugins/_security/api/internalusers/jake
{ "password": "a-long-unique-password", "backend_roles": [] }

PUT _plugins/_security/api/rolesmapping/readall
{ "users": ["jake"] }

Run these from Dashboards' Dev Tools while signed in as the master user. Note that a PUT on a role mapping replaces the whole mapping, so include existing users and backend roles you want to keep. For Dashboards itself, also map people to the opensearch_dashboards_user role, which carries the permissions for index patterns, visualizations and dashboards. Be careful with all_access: mapping a user to it alone gives full data access but not the Security page; the master user gets both all_access and security_manager.

A Lambda function that searches OpenSearch, end to end

A common way to hit "anonymous" is a Lambda function that calls the domain with a plain HTTP library. The working setup has three parts:

  • The function's code signs requests, with opensearch-py and AWSV4SignerAuth as shown earlier (package opensearch-py and requests with the function or in a layer).
  • The domain access policy allows the function's execution role ARN (or the policy is open and fine-grained access control maps that role to an OpenSearch role).
  • The network: for a VPC domain, the function runs in the same VPC (or a connected one), in subnets that can reach the domain's subnets, with a security group the domain's security group allows on port 443.

With fine-grained access control on, the function's role also needs a role mapping, or the signed request passes the access policy and then fails with a security_exception. A useful first test from the function is client.info(): it fails fast with a clear error if signing, policy or network is wrong, before you debug your queries.

Postman, the AWS CLI and other tools

For trying requests by hand, Postman can sign them: choose the AWS Signature authorization type, enter your access key, secret key and session token, the domain's Region, and the service name es. The AWS CLI does not have a command for arbitrary OpenSearch data requests (the aws opensearch commands manage domains, not documents), so for data use curl --aws-sigv4, awscurl or a client library. Browser extensions that add basic authentication work only with fine-grained access control's internal users, never with an IAM-only policy.

Across accounts, the same rules hold: the domain access policy names the other account's role as the principal, and the caller signs with credentials for that role. If the domain uses fine-grained access control, map the other account's role ARN as a backend role as well.

Turning on fine-grained access control without locking everyone out

Switching an existing domain to fine-grained access control is the right long-term fix, and it has one feature designed for exactly the "anonymous" situation. When you enable it on a domain that uses an open or IP-based policy, you can tick Enable migration period for open/IP-based access policy. For up to 30 days, existing clients keep working as before while you create roles and map users. With the CLI, it is the AnonymousAuthEnabled option:

aws opensearch update-domain-config --domain-name booking --region us-east-1 \
  --advanced-security-options '{"Enabled": true, "InternalUserDatabaseEnabled": true,
    "MasterUserOptions": {"MasterUserName": "admin", "MasterUserPassword": "a-long-unique-password"},
    "AnonymousAuthEnabled": true}'

Three things to plan around. The change runs a blue/green deployment, during which the cluster health shows red while operations continue. The migration period ends automatically after 30 days and cannot be re-enabled, so any unsigned client you have not moved to a real user by then starts failing on that day. And for domains that used identity-based policies, OpenSearch Service temporarily maps existing IAM users to a role called default_role, so signed GET and PUT requests keep working; delete that mapping once your own roles are in place. Put the 30-day end date in your calendar the moment you click Save.

Terraform for a domain people and apps can both use

In Terraform, the access policy and fine-grained access control are set on the aws_opensearch_domain resource. This is the shape of Jake's domain: fine-grained access control with an internal master user, the encryption it requires, HTTPS enforced, and an open access policy that is safe because every request is authenticated inside OpenSearch:

resource "aws_opensearch_domain" "booking" {
  domain_name    = "booking"
  engine_version = "OpenSearch_2.19"

  cluster_config {
    instance_type  = "t3.small.search"
    instance_count = 1
  }
  ebs_options {
    ebs_enabled = true
    volume_size = 10
  }

  encrypt_at_rest { enabled = true }
  node_to_node_encryption { enabled = true }
  domain_endpoint_options {
    enforce_https       = true
    tls_security_policy = "Policy-Min-TLS-1-2-2019-07"
  }

  advanced_security_options {
    enabled                        = true
    internal_user_database_enabled = true
    master_user_options {
      master_user_name     = "admin"
      master_user_password = var.opensearch_master_password
    }
  }

  access_policies = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { AWS = "*" }
      Action    = "es:ESHttp*"
      Resource  = "arn:aws:es:us-east-1:111122223333:domain/booking/*"
    }]
  })
}

Keep the master password in a secret store and pass it in as a variable, never in the code. The engine version shown is an example; use a version your Region offers. Without fine-grained access control, replace the open statement with a named role and IP ranges, as in the policy examples above. If several teams manage the policy, the separate aws_opensearch_domain_policy resource lets it live apart from the domain, but use one or the other, not both.

Audit logs: see who was refused and why

Because data requests are not in CloudTrail, the way to see refused and allowed requests is fine-grained access control's audit logs. Turn on audit log publishing for the domain (Logs tab, Audit logs, choose a CloudWatch Logs group), then enable audit logging in Dashboards under Security, Audit logs. Failed logins and missing privileges are logged with the user, the action and the index, which turns "someone gets 403" into "the booking-app role was refused indices:data/write/bulk on repairs-2026". The log group needs a resource policy that lets OpenSearch Service write to it; the console offers to create one. Audit logs add log volume and ingestion cost, so start with failed requests only and widen if you need more.

VPC domains: timeouts instead of "anonymous"

A domain with VPC access has no public endpoint. Requests from outside the VPC never arrive, so instead of the 403 you usually get a timeout. Inside the VPC, the rules shift:

  • You cannot use an IP-based policy on a VPC domain; use security groups to control which addresses can reach the endpoint.
  • An open access policy on a VPC domain accepts unsigned requests from anything the security group allows. That is a normal setup when combined with tight security groups, and with FGAC if different people need different access.
  • To use Dashboards on a VPC domain, your browser must be able to reach the VPC: through a VPN, AWS Client VPN, a bastion with port forwarding, or a proxy inside the VPC.

A quick way to tell which problem you have: from a machine inside the VPC, curl -s -o /dev/null -w "%{http_code}" https://vpc-booking-abc123.us-east-1.es.amazonaws.com/. A 403 is a policy problem (this page); a hang is a network problem (security group, route, or DNS).

Logstash, Fluent Bit and other log shippers

Log shippers are a classic source of "anonymous" errors, because their OpenSearch outputs default to plain HTTP. Turn on AWS signing and give them credentials through an IAM role:

  • Fluent Bit: in the opensearch output, set AWS_Auth On and AWS_Region us-east-1 (and AWS_Role_ARN if it should assume a role). On ECS or EKS, the task or pod role supplies the credentials.
  • Logstash: use the OpenSearch output plugin with its AWS IAM authentication settings, and the open-source (OSS) Logstash distribution, because the default distributions include a license check that fails against OpenSearch.
  • Amazon OpenSearch Ingestion pipelines sign their requests with the pipeline role you give them; allow that role in the domain access policy (and map it in FGAC).
  • Any shipper with only username and password support needs FGAC with an internal user it can authenticate as.

OpenSearch Serverless is different

OpenSearch Serverless collections do not use domain access policies at all. Access is controlled by data access policies (who can do what on which collection and index), together with network policies and encryption policies, and every request must be signed with the service name aoss. A data access policy that lets the app's role read and write documents in one collection looks like this:

[{
  "Rules": [{
    "ResourceType": "index",
    "Resource": ["index/booking-search/*"],
    "Permission": ["aoss:ReadDocument", "aoss:WriteDocument", "aoss:CreateIndex", "aoss:DescribeIndex"]
  }],
  "Principal": ["arn:aws:iam::111122223333:role/booking-app"]
}]

The caller's IAM identity also needs aoss:APIAccessAll in its own IAM policy to call the collection's data API at all. If you see access errors on a collection, check those two policies and the service name in your signer, not anything on this page about domain policies.

A safe setup, step by step

  1. Decide public or VPC for the domain. VPC is safer by default; public needs fine-grained access control or a tight policy.
  2. Turn on fine-grained access control if people will use Dashboards or you need per-index permissions. Choose the master user (IAM ARN, or internal user).
  3. Write the access policy: open if FGAC or VPC protects the domain; otherwise named roles, IP ranges, or both.
  4. Sign every programmatic request with an IAM role, and map that role to a narrow OpenSearch role in FGAC.
  5. Give people their own users (SAML, Cognito or internal) mapped to read-only or limited roles.
  6. Test from outside: an unsigned curl to the endpoint from a random network should be refused (403, or a sign-in prompt with FGAC).

OpenSearch access errors, by symptom

Dashboards shows "User: anonymous is not authorized"

The access policy allows only IAM principals, and browsers cannot sign. Set up FGAC with SAML, Cognito or internal users and an open policy, or allow your IP range.

curl gets "anonymous is not authorized"

The request was not signed. Use curl --aws-sigv4 "aws:amz:REGION:es" with your credentials, or awscurl.

My Lambda function gets "anonymous is not authorized"

It uses a plain HTTP client. Use opensearch-py with AWSV4SignerAuth (or a signer for your language), and allow the function's role in the policy.

"is not authorized" with my role's ARN instead of anonymous

The request was signed, but the policy does not allow that role or action. Add the role, or the missing es:ESHttp* action, to the access policy.

"The request signature we calculated does not match"

The signer used the wrong Region, the wrong service name (es vs aoss), or changed the request after signing. Match the domain's Region and service.

"security_exception: no permissions for [indices:...]"

FGAC is on and the user or role is not mapped to an OpenSearch role with that permission. Map it in Dashboards under Security, Roles.

The request times out

A VPC domain reached from outside the VPC, or a security group that does not allow you. Fix the network path first.

I can search but cannot change the domain configuration

Domain configuration uses the AWS API, which needs an identity-based IAM policy on your user or role; the domain access policy does not cover it.

Unsigned clients worked for a month after enabling FGAC, then stopped

The 30-day migration period ended automatically, and it cannot be turned back on. Give each client a real identity: a signed IAM role mapped to an OpenSearch role, or an internal user.

An OpenSearch Ingestion pipeline is refused

The pipeline role is not allowed in the access policy, or not mapped in fine-grained access control. Allow the role and map it to a role that can write to the target indexes.

Everything worked until someone changed the access policy

Compare the current policy with the last working one; a missing /*, a new deny or a typo in the account ID are the usual culprits. Keep the policy in infrastructure code so you can see the change.

403 "Request throttled due to too many requests"

Not a permission problem: the domain is overloaded. Scale the instance type or reduce the request rate.

OpenSearch "anonymous is not authorized": frequently asked questions

What does "User: anonymous is not authorized to perform: es:ESHttpGet" mean?

The request reached the OpenSearch domain without an AWS signature, and the access policy allows only specific IAM principals. Sign the request or change how the domain authenticates people.

Why can't I open OpenSearch Dashboards?

Dashboards does not support signed requests, so an access policy that lists only IAM roles blocks every browser. Use fine-grained access control with SAML, Cognito or internal users, or an IP-based policy.

How do I sign requests to OpenSearch Service with curl?

Use curl --aws-sigv4 "aws:amz:us-east-1:es" with your access key and secret as --user, plus an x-amz-security-token header for temporary credentials.

What service name do I use to sign OpenSearch requests?

es for OpenSearch Service domains and aoss for OpenSearch Serverless collections, with the Region where the domain or collection lives.

How do I connect to OpenSearch from Python with IAM?

Use opensearch-py with AWSV4SignerAuth(credentials, region, "es") and RequestsHttpConnection; the credentials come from boto3.

Is an open access policy on OpenSearch safe?

Only with fine-grained access control turned on, or for a VPC domain behind tight security groups. On a public domain without FGAC it lets anyone read and delete your data.

How do I allow my IP address to access OpenSearch?

Add a statement with Principal AWS "*", Action es:ESHttp* and an IpAddress condition on aws:SourceIp, for public domains only. VPC domains use security groups instead.

What is fine-grained access control in OpenSearch?

Users, roles and permissions inside OpenSearch, down to index, document and field level, with sign-in through SAML, Cognito or an internal user database. It cannot be turned off once enabled.

How do I give an IAM role access to OpenSearch with fine-grained access control?

Allow the role in the access policy (or use an open policy), then map its ARN as a backend role to an OpenSearch role in Dashboards under Security.

Why does my VPC OpenSearch domain time out instead of returning 403?

Requests from outside the VPC never reach it. Connect from inside the VPC, through a VPN or a bastion, and check the security group.

Can I use an IP-based access policy on a VPC domain?

No. VPC domains use security groups to control which addresses can reach the endpoint.

What does es:ESHttp* allow?

All HTTP methods against the domain's data endpoint: GET, POST, PUT, DELETE, HEAD and PATCH. In resource-based policies, es:* means the same.

Why does my access policy need /* at the end of the resource?

The /* covers the domain's indexes and APIs. Without it, the statement applies only to the domain itself and no data request is allowed.

How do I fix Fluent Bit "anonymous is not authorized" errors?

Set AWS_Auth On and AWS_Region in the opensearch output, give it an IAM role, and allow that role in the domain access policy.

Does OpenSearch Serverless use access policies?

It uses data access policies, network policies and encryption policies instead of domain access policies, and requests are signed with the aoss service name.

How long does an OpenSearch access policy change take?

Usually a few minutes. Wait until the domain shows the change as active before testing.

For readers preparing for an AWS certification

We have been covering AWS certifications alongside our fixes, so here is a short extra for readers studying for one. If you only came for the fix, you already have it; feel free to skip this part.

Exams that test this topic: Security – Specialty (SCS-C03) • Solutions Architect – Associate (SAA-C03) • Developer – Associate (DVA-C03 from December 1, 2026).

Here are the exam-style questions this topic tends to raise, with the reasoning you need to answer them:

1. Users opening OpenSearch Dashboards get "User: anonymous is not authorized to perform: es:ESHttpGet". The access policy allows only an IAM role. Why?

Browsers cannot send SigV4-signed requests to Dashboards, so the requests are anonymous; use fine-grained access control with SAML, Cognito or internal users, or an IP-based policy.

2. A Lambda function calling an OpenSearch domain gets the same anonymous error. What fixes it?

Sign the requests with SigV4 using the function's execution role (for example opensearch-py AWSV4SignerAuth with service es) and allow that role in the access policy.

3. When is an open domain access policy acceptable?

When fine-grained access control authenticates every request, or the domain is in a VPC protected by security groups; never on a public domain without fine-grained access control.

4. A VPC-based OpenSearch domain must allow access only from an application subnet. What controls this?

Security groups on the domain's VPC endpoint; IP-based access policies are not supported for VPC domains.

5. Cognito authentication for Dashboards signs users in, but requests are still refused. What is missing?

The domain access policy must allow the Cognito identity pool's authenticated IAM role.

6. A signed request passes the access policy but returns security_exception no permissions. What is required?

Map the IAM role or user to an OpenSearch role with the needed permissions in fine-grained access control.

Exam codes and dates change often. Our AWS certification changes page keeps track of what is current.

πŸ“š ALSO READ

Where to go next, from explicit denies to VPC endpoints:

📌 Bookmark this; the four policy examples are the part you will copy again.

Jake now opens Dashboards from the shop's laptop, signs in with his own read-only user, and checks which phone models customers search for most. The app still signs every request with its role, and an unsigned test from Ethan's phone gets the sign-in page instead of data. Jake asked if "anonymous" would come back. "Only if someone opens the door again," Ethan said. "And the door now asks for a name."

📌 If you keep one line from this page

"Anonymous" means unsigned: sign code and curl requests with SigV4 (service es), give people Dashboards access through fine-grained access control with SAML, Cognito or internal users (or an IP-based policy), use security groups for VPC domains, and never put an open policy on a public domain without fine-grained access control.

Service name es (aoss for Serverless) · resource ends in /* · es:ESHttp* = all data methods · FGAC is one-way.

Revision note. Written October 22, 2026, with Amazon OpenSearch Service access control behavior of that date. "Good fences make good neighbors," the proverb says; an access policy is the fence, and fine-grained access control is the gate with a name on it.

Related