ROR ENT 1.70.1 & 1.70.2 9.4.2 generate CSV in Kibana RO mode broken

Hi,

The team found another issue in Kibana RO mode.

I assumed it was related to the previous two, so I awaited the 1.70.2 build.
But also there it fails.

Kibana RO users are not allowed to generate CSV reports.

The job seems to create succesfull, but then fails in the background and gives you an error in the UI.

Also the actual report isn’t generated.

It shows as a forbidden in ROR audit logs.

It is a call towards /_bulk endpoint that gets forbidden.

I have a working workaround that I will not put in here :slight_smile:

No priority needed for a fix.

1 Like

We will check it too. Thanks for your reports.

@ronald.vanboven could you please show us the forbidden log. I tried to reproduce it but I see in ROR 1.70.2 RO user can successfully create CSV report in Discover page.

BTW, how many Kibana nodes do you have?

Configuration:

  - name: "Allow Kibana main RO access"
    kibana_access: "ro"
    kibana_hide_apps: ["Analytics|Overview", "Observability", "Security", "readonlyrest_kbn", "Management", "Enterprise Search", "ROR Manage Kibana"]
    groups: ["kibana_main_ro"]
    indices: [".kibana", ".reporting.kibana-*", "aaa_dummy_index"]

  - name: "Dynamic data access rule"
    type: "allow"
    groups: ["kibana_main_ro", "*", "aaa_dummy_index", "elasticsearch", "kibana"]
    actions: ["indices:data/read/*", "indices:admin/mappings/fields/get", "indices:admin/mappings/get"]
    indices: ["aaa_dummy_index", "@explode{acl:available_groups}-*", "@explode{acl:available_groups}", "@explode{acl:available_groups}_*", ".ds-@explode{acl:available_groups}-*"]

User:

  users:
  - username: test_ro_user
    auth_key: test_ro_user:XXX
    groups: ["kibana_main_ro", "elasticsearch"]

Relevant audit document:

{
  "_index": "readonlyrest_audit-2026.06",
  "_id": "5cdee58e-2c07-4b21-bf1c-407e8e61e9c6-688276354#3682535",
  "_version": 1,
  "_ignored": [
    "acl_history.keyword",
    "content.keyword"
  ],
  "_source": {
    "headers": [
      "accept",
      "x-ror-kibana-index",
      "x-elastic-client-meta",
      "x-ror-kibana-request-path",
      "x-ror-current-group",
      "Authorization",
      "Host",
      "x-forwarded-for",
      "tracestate",
      "x-ror-correlation-id",
      "x-elastic-product-origin",
      "user-agent",
      "x-ror-tenancy",
      "x-opaque-id",
      "traceparent",
      "Content-Length",
      "x-ror-kibana-request-method",
      "keep-alive",
      "content-type",
      "cookie",
      "Accept-Charset",
      "connection"
    ],
    "acl_history": "[Allow Kibana main RO access: NOT_MATCHED (AUTHZ_FAIL) -> RULES:[groups_any_of->true, kibana_hide_apps->true, kibana_access->false]], [Dynamic data access rule: NOT_MATCHED (AUTHZ_FAIL) -> RULES:[groups_any_of->true, actions->false]]",
    "origin": "x.x.x.x/32",
    "match": false,
    "final_state": "FORBIDDEN",
    "destination": "x.x.x.x/32",
    "task_id": 3682535,
    "type": "BulkRequest",
    "req_method": "POST",
    "content": "{\"update\":{\"_id\":\"legacy-url-alias:default:index-pattern:b878e66b-25ac-456a-b85e-696ec3bc0493\",\"_index\":\".kibana\",\"_source\":true}}\n{\"script\":{\"source\":\"\\n            if (ctx._source[params.type].disabled != true) {\\n              if (ctx._source[params.type].resolveCounter == null) {\\n                ctx._source[params.type].resolveCounter = 1;\\n              }\\n              else {\\n                ctx._source[params.type].resolveCounter += 1;\\n              }\\n              ctx._source[params.type].lastResolved = params.time;\\n              ctx._source.updated_at = params.time;\\n            }\\n          \",\"lang\":\"painless\",\"params\":{\"type\":\"legacy-url-alias\",\"time\":\"2026-06-23T07:31:28.558Z\"}}}\n",
    "path": "/_bulk",
    "indices": [],
    "@timestamp": "2026-06-23T07:31:28Z",
    "content_len_kb": 0,
    "correlation_id": "5cdee58e-2c07-4b21-bf1c-407e8e61e9c6",
    "processingMillis": 12,
    "xff": "localhost:24001",
    "action": "indices:data/write/bulk",
    "block": "default",
    "id": "5cdee58e-2c07-4b21-bf1c-407e8e61e9c6-688276354#3682535",
    "content_len": 706,
    "user": "test_ro_user"
  },
  "fields": {
    "headers.keyword": [
      "accept",
      "x-ror-kibana-index",
      "x-elastic-client-meta",
      "x-ror-kibana-request-path",
      "x-ror-current-group",
      "Authorization",
      "Host",
      "x-forwarded-for",
      "tracestate",
      "x-ror-correlation-id",
      "x-elastic-product-origin",
      "user-agent",
      "x-ror-tenancy",
      "x-opaque-id",
      "traceparent",
      "Content-Length",
      "x-ror-kibana-request-method",
      "keep-alive",
      "content-type",
      "cookie",
      "Accept-Charset",
      "connection"
    ],
    "origin": [
      "x.x.x.x/32"
    ],
    "destination": [
      "x.x.x.x/32"
    ],
    "final_state": [
      "FORBIDDEN"
    ],
    "task_id": [
      3682535
    ],
    "req_method.keyword": [
      "POST"
    ],
    "type": [
      "BulkRequest"
    ],
    "final_state.keyword": [
      "FORBIDDEN"
    ],
    "user.keyword": [
      "test_ro_user"
    ],
    "content": [
      "{\"update\":{\"_id\":\"legacy-url-alias:default:index-pattern:b878e66b-25ac-456a-b85e-696ec3bc0493\",\"_index\":\".kibana\",\"_source\":true}}\n{\"script\":{\"source\":\"\\n            if (ctx._source[params.type].disabled != true) {\\n              if (ctx._source[params.type].resolveCounter == null) {\\n                ctx._source[params.type].resolveCounter = 1;\\n              }\\n              else {\\n                ctx._source[params.type].resolveCounter += 1;\\n              }\\n              ctx._source[params.type].lastResolved = params.time;\\n              ctx._source.updated_at = params.time;\\n            }\\n          \",\"lang\":\"painless\",\"params\":{\"type\":\"legacy-url-alias\",\"time\":\"2026-06-23T07:31:28.558Z\"}}}\n"
    ],
    "path": [
      "/_bulk"
    ],
    "id.keyword": [
      "5cdee58e-2c07-4b21-bf1c-407e8e61e9c6-688276354#3682535"
    ],
    "xff.keyword": [
      "localhost:24001"
    ],
    "type.keyword": [
      "BulkRequest"
    ],
    "correlation_id.keyword": [
      "5cdee58e-2c07-4b21-bf1c-407e8e61e9c6"
    ],
    "action": [
      "indices:data/write/bulk"
    ],
    "block": [
      "default"
    ],
    "id": [
      "5cdee58e-2c07-4b21-bf1c-407e8e61e9c6-688276354#3682535"
    ],
    "content_len": [
      706
    ],
    "action.keyword": [
      "indices:data/write/bulk"
    ],
    "headers": [
      "accept",
      "x-ror-kibana-index",
      "x-elastic-client-meta",
      "x-ror-kibana-request-path",
      "x-ror-current-group",
      "Authorization",
      "Host",
      "x-forwarded-for",
      "tracestate",
      "x-ror-correlation-id",
      "x-elastic-product-origin",
      "user-agent",
      "x-ror-tenancy",
      "x-opaque-id",
      "traceparent",
      "Content-Length",
      "x-ror-kibana-request-method",
      "keep-alive",
      "content-type",
      "cookie",
      "Accept-Charset",
      "connection"
    ],
    "destination.keyword": [
      "x.x.x.x/32"
    ],
    "acl_history": [
      "[Allow Kibana main RO access: NOT_MATCHED (AUTHZ_FAIL) -> RULES:[groups_any_of->true, kibana_hide_apps->true, kibana_access->false]], [Dynamic data access rule: NOT_MATCHED (AUTHZ_FAIL) -> RULES:[groups_any_of->true, actions->false]]"
    ],
    "match": [
      false
    ],
    "req_method": [
      "POST"
    ],
    "@timestamp": [
      "2026-06-23T07:31:28.000Z"
    ],
    "content_len_kb": [
      0
    ],
    "origin.keyword": [
      "x.x.x.x/32"
    ],
    "block.keyword": [
      "default"
    ],
    "correlation_id": [
      "5cdee58e-2c07-4b21-bf1c-407e8e61e9c6"
    ],
    "processingMillis": [
      12
    ],
    "xff": [
      "localhost:24001"
    ],
    "user": [
      "test_ro_user"
    ],
    "path.keyword": [
      "/_bulk"
    ]
  },
  "ignored_field_values": {
    "acl_history.keyword": [
      "[Allow Kibana main RO access: NOT_MATCHED (AUTHZ_FAIL) -> RULES:[groups_any_of->true, kibana_hide_apps->true, kibana_access->false]], [Dynamic data access rule: NOT_MATCHED (AUTHZ_FAIL) -> RULES:[groups_any_of->true, actions->false]]"
    ],
    "content.keyword": [
      "{\"update\":{\"_id\":\"legacy-url-alias:default:index-pattern:b878e66b-25ac-456a-b85e-696ec3bc0493\",\"_index\":\".kibana\",\"_source\":true}}\n{\"script\":{\"source\":\"\\n            if (ctx._source[params.type].disabled != true) {\\n              if (ctx._source[params.type].resolveCounter == null) {\\n                ctx._source[params.type].resolveCounter = 1;\\n              }\\n              else {\\n                ctx._source[params.type].resolveCounter += 1;\\n              }\\n              ctx._source[params.type].lastResolved = params.time;\\n              ctx._source.updated_at = params.time;\\n            }\\n          \",\"lang\":\"painless\",\"params\":{\"type\":\"legacy-url-alias\",\"time\":\"2026-06-23T07:31:28.558Z\"}}}\n"
    ]
  }
}

I had the prune the acl_history because of internal policy. But the relevant part is in there.

I see this forbidden 3 times when trying to generate the report.

If I login with admin user and check the report, I see:

We have configured ROR to be clear about it blocking things with:

  response_if_req_forbidden: "Computer says no!"

I tried CSV export via “Discover in dashboard” and “Discover session via discover”, both don’t work.

If I switch user to RW role, the CSV export works.

On RO role:

From the users perspective it looks like:

Setup details:

In test setup we have 1 kibana node.

image

And 3 Elasticsearch nodes:

XXXC001 readonlyrest 1.70.2
XXXV001 readonlyrest 1.70.2
XXXV002 readonlyrest 1.70.2

XXXC001 dirt
XXXV001 m
XXXV002 dirt

Kibana points to XXXV002.

Hope this helps.

1 Like