Skip to content

Narrow kibana_user role to only the dashboards index alias#6227

Draft
cwperks wants to merge 1 commit into
mainfrom
narrow-kibana-user-role
Draft

Narrow kibana_user role to only the dashboards index alias#6227
cwperks wants to merge 1 commit into
mainfrom
narrow-kibana-user-role

Conversation

@cwperks

@cwperks cwperks commented Jun 18, 2026

Copy link
Copy Markdown
Member

Description

Removes .kibana_*, .opensearch_dashboards_*, .kibana-6, .opensearch_dashboards-6, .tasks, and .management-beats from the kibana_user static role's index_permissions. This ensures that tenant_permissions is the sole mechanism controlling tenant access, rather than broad index-level permissions.

Background

The kibana_user role previously granted read, index, delete, manage on .kibana_*. This pattern matches every concrete tenant index (e.g., .kibana_{hash}_{username}), allowing any user with the role to directly access these indices.

The Security plugin's PrivilegesInterceptor (legacy) and DashboardsMultitenancySystemIndexHandler (nextgen) already handle tenant access entirely through:

  1. Rewriting requests from the .kibana alias to the correct concrete tenant index
  2. Validating tenant_permissions for the requested tenant
  3. Stashing a DocumentAllowList in the thread context to grant access to the concrete index for downstream shard-level operations

No additional index permissions are needed for the standard Dashboards UI flow, which always sends the securitytenant header.

The .tasks entry is redundant (.tasks is now a system index with its own protection), and .management-beats is not used by OpenSearch Dashboards.

Testing

All 468 existing DashboardMultiTenancyIntTests pass (both legacy and nextgen evaluator paths), confirming that Dashboards operations work correctly with tenant_permissions alone.

New tests added:

  • mget_concreteIndexWithoutTenantHeader_shouldBeDenied
  • search_concreteIndexWithoutTenantHeader_shouldBeDenied
  • bulk_concreteIndexWithoutTenantHeader_shouldBeDenied

Breaking change note

Users who rely on direct API access (e.g., curl, Dev Tools) to concrete .kibana_* tenant indices without the securitytenant header will need to either:

  • Add the securitytenant header to their requests, or
  • Create a custom role with explicit .kibana_* index permissions

The standard Dashboards UI workflow is unaffected.

Related issues

Check List

  • New functionality includes testing
  • Commits are signed per the DCO using --signoff

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

The kibana_user role previously granted index-level permissions on .kibana_*,
.kibana-6, .opensearch_dashboards_*, .opensearch_dashboards-6, .tasks, and
.management-beats. These are unnecessary for the standard Dashboards UI flow.

The PrivilegesInterceptor (legacy) and DashboardsMultitenancySystemIndexHandler
(nextgen) handle tenant access entirely through the .kibana alias rewrite and
DocumentAllowList thread context mechanism:
1. Requests go through the .kibana alias
2. The interceptor validates tenant_permissions
3. The request is rewritten to the concrete tenant index
4. A DocumentAllowList is stashed in the thread context for shard-level access

No additional index permissions are needed. The .tasks and .management-beats
entries are a legacy artifact (.tasks is now a system index, .management-beats
is unused by OpenSearch Dashboards).

Changes:
- static_roles.yml: Narrow kibana_user to only .kibana and
  .opensearch_dashboards alias permissions
- DashboardMultiTenancyIntTests: Add tests verifying users cannot access
  other users concrete tenant indices without the securitytenant header

Signed-off-by: Craig Perkins <cwperx@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ Recommended focus areas for review

Test Logic Gap

The early return at line 971-973 skips the test when user.reference(READ).covers(dashboards_index_human_resources) is true. This means users who already have read access to the HR index are not tested for the specific vulnerability this PR addresses: direct concrete index access without the tenant header. The test should verify that even users with tenant permissions cannot bypass tenant isolation by omitting the header, but the current logic exempts exactly those users from testing.

if (user.reference(READ).covers(dashboards_index_human_resources)) {
    return;
}
Test Logic Gap

The early return at line 1005-1007 skips the test when user.reference(READ).covers(dashboards_index_human_resources) is true. This exempts users with legitimate tenant access from verifying that direct concrete index access without the tenant header is blocked. The test should confirm that the tenant header is mandatory for all users, regardless of their tenant permissions.

if (user.reference(READ).covers(dashboards_index_human_resources)) {
    return;
}
Test Logic Gap

The early return at line 1021-1023 skips the test when user.reference(WRITE).covers(dashboards_index_human_resources) is true. This means users with write permissions to the HR tenant are not tested for the vulnerability this PR fixes: bypassing tenant isolation by directly writing to concrete indices without the tenant header. The test should verify that all write operations require the tenant header, but currently exempts the very users who need this enforcement.

if (user.reference(WRITE).covers(dashboards_index_human_resources)) {
    return;
}

@github-actions

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Security
Enforce consistent security response

The test accepts both 200 with security_exception and 403 responses as valid denial
outcomes. This dual acceptance may mask inconsistent security behavior. Consider
enforcing a single expected response pattern to ensure consistent security
enforcement across all scenarios.

src/integrationTest/java/org/opensearch/security/privileges/int_tests/DashboardMultiTenancyIntTests.java [989-995]

-if (response.getStatusCode() == 200) {
-    String body = response.getBody();
-    assertTrue("Expected security_exception in mget doc response but got: " + body, body.contains("security_exception"));
-    assertFalse("Expected no found documents but got: " + body, body.contains("\"found\" : true"));
-} else {
-    assertThat(response, isForbidden());
-}
+// Expect consistent 403 forbidden response for unauthorized access
+assertThat(response, isForbidden());
Suggestion importance[1-10]: 4

__

Why: While the suggestion aims to enforce consistency, the current implementation correctly handles the reality that _mget returns 200 with per-document errors. Removing this handling would make the test fail incorrectly, as the behavior is API-specific rather than a security inconsistency.

Low
Enforce consistent bulk security response

Similar to the mget test, accepting both 200 with error items and 403 responses
creates ambiguity in security validation. Standardize on a single expected response
pattern to ensure the security layer consistently blocks unauthorized bulk
operations at the request level.

src/integrationTest/java/org/opensearch/security/privileges/int_tests/DashboardMultiTenancyIntTests.java [1031-1039]

-if (response.getStatusCode() == 200) {
-    String body = response.getBody();
-    assertTrue(
-        "Expected security_exception in bulk item response but got: " + body,
-        body.contains("security_exception") || body.contains("\"status\":403")
-    );
-} else {
-    assertThat(response, isForbidden());
-}
+// Expect consistent 403 forbidden response for unauthorized bulk operations
+assertThat(response, isForbidden());
Suggestion importance[1-10]: 4

__

Why: Similar to suggestion 2, the current code correctly handles the _bulk API's behavior of returning 200 with per-item errors. The suggestion would break the test by not accounting for this legitimate API response pattern.

Low

@cwperks cwperks changed the title Narrow kibana_user role to enforce tenant isolation via tenant_permissions Narrow kibana_user role to only the dashboards index alias Jun 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant