Skip to content

Commit 4eae801

Browse files
david-yuclaude
andcommitted
rpk: enable OAUTHBEARER for remote debug bundle
Remove the early-exit guard that rejected OAUTHBEARER profiles in 'rpk debug remote-bundle start'. The broker-side admin API now accepts a bearer_creds payload (#30225), and rpadmin v0.2.6 exposes WithOAuthBearerAuthentication so rpk can forward the token. toRpadminOptions now dispatches on mechanism: OAUTHBEARER profiles call WithOAuthBearerAuthentication(token), all other SASL profiles fall through to the existing WithSCRAMAuthentication path. Add DebugBundleOAuthBearerAuthn, an end-to-end ducktape test that exercises the full OAUTHBEARER forwarding path. The test spins up a Keycloak OIDC provider alongside a single Redpanda broker configured with SASL OAUTHBEARER. It issues a client credentials token from Keycloak, then POSTs a debug bundle start request with authentication: {mechanism: OAUTHBEARER, token: <JWT>}. The broker forwards the token to the rpk subprocess via -Xsasl.mechanism and -Xpass=token:..., and the subprocess authenticates to Kafka using the JWT. The test verifies the bundle completes successfully and the expected topic appears in kafka.json. Supporting changes: - redpanda_types.py: add OAuthBearerCredentials dataclass, which serializes to {mechanism, token} - admin.py: widen DebugBundleStartConfigParams.authentication to accept OAuthBearerCredentials alongside the existing SaslCredentials Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent aad92ce commit 4eae801

6 files changed

Lines changed: 120 additions & 19 deletions

File tree

src/go/rpk/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ require (
4646
github.com/prometheus/client_model v0.6.2
4747
github.com/prometheus/common v0.67.5
4848
github.com/redpanda-data/common-go/proto v0.0.0-20260223115805-73fb9bd9c2c0
49-
github.com/redpanda-data/common-go/rpadmin v0.2.5
49+
github.com/redpanda-data/common-go/rpadmin v0.2.6
5050
github.com/redpanda-data/common-go/rpsr v0.1.4
5151
github.com/redpanda-data/protoc-gen-go-mcp v0.0.0-20250930092048-a98b94b5957a
5252
github.com/rs/xid v1.6.0

src/go/rpk/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,8 @@ github.com/redpanda-data/common-go/net v0.1.0 h1:JnJioRJuL961r1QXiJQ1tW9+yEaJfu8
279279
github.com/redpanda-data/common-go/net v0.1.0/go.mod h1:iOdNkjxM7a1T8F3cYHTaKIPFCHzzp/ia6TN+Z+7Tt5w=
280280
github.com/redpanda-data/common-go/proto v0.0.0-20260223115805-73fb9bd9c2c0 h1:HgdM3npLEzqKyrzni05JwFb9B/Yd7T7hdFYgaUY7d38=
281281
github.com/redpanda-data/common-go/proto v0.0.0-20260223115805-73fb9bd9c2c0/go.mod h1:4TOyhdEvR/hlk0RjHXzmO0hYmWBpGGI1qiFPNv211O4=
282-
github.com/redpanda-data/common-go/rpadmin v0.2.5 h1:mKYr5ffO6TmBwNSAHgnMI703bbf8961GczTDUFxvkqA=
283-
github.com/redpanda-data/common-go/rpadmin v0.2.5/go.mod h1:uOAY10WXPtcDPU0aUdpkqHR+b1BqUvRhlvMf0vha73A=
282+
github.com/redpanda-data/common-go/rpadmin v0.2.6 h1:fVfWvyw1xJy7lWBQvyEdLxx40Pkq74DuBwOdCKS3lZ0=
283+
github.com/redpanda-data/common-go/rpadmin v0.2.6/go.mod h1:uOAY10WXPtcDPU0aUdpkqHR+b1BqUvRhlvMf0vha73A=
284284
github.com/redpanda-data/common-go/rpsr v0.1.4 h1:d9lu5q5wyhZWBYR1GnZkq+eZGKU0qoaSwwybRS9Uk2k=
285285
github.com/redpanda-data/common-go/rpsr v0.1.4/go.mod h1:qVa7b0yaCRdZDn5dcZ9CazqVr4jYbgtOJUywI2X3G3I=
286286
github.com/redpanda-data/protoc-gen-go-mcp v0.0.0-20250930092048-a98b94b5957a h1:jNHT6Fcy/rBAFnX8rjbwrJ+lSF2Ufa1jqmnCV6m6RKY=

src/go/rpk/pkg/cli/debug/remotebundle/start.go

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -78,16 +78,6 @@ Use the flag '--no-confirm' to avoid the confirmation prompt.
7878
out.MaybeDie(err, "rpk unable to load config: %v", err)
7979
config.CheckExitCloudAdmin(p)
8080

81-
// Remote debug bundle forwards SASL credentials to the broker so
82-
// rpk running on the broker can authenticate to Kafka. The broker
83-
// API currently only accepts a SCRAM-shaped payload; until that
84-
// gains OAUTHBEARER support (tracked in redpanda#30222), reject
85-
// up front rather than silently sending the request with no auth
86-
// (which then fails confusingly on secured clusters).
87-
if p.KafkaAPI.SASL != nil && strings.EqualFold(p.KafkaAPI.SASL.Mechanism, adminapi.OAuthBearer) {
88-
out.Die("OAUTHBEARER is not yet supported for remote debug bundle collection; use SCRAM-SHA-256 or SCRAM-SHA-512 credentials to run this command")
89-
}
90-
9181
if !noConfirm {
9282
printBrokers(p.AdminAPI.Addresses)
9383
confirmed, err := out.Confirm("Confirm debug bundle collection from these brokers?")
@@ -260,9 +250,12 @@ func (o *remoteBundleOptions) toRpadminOptions(p *config.RpkProfile) []rpadmin.D
260250
}
261251
opts = append(opts, rpadmin.WithLabelSelector(dbls))
262252
}
263-
if p.HasSASLCredentials() {
264-
s := p.KafkaAPI.SASL
265-
opts = append(opts, rpadmin.WithSCRAMAuthentication(s.User, s.Password, s.Mechanism))
253+
if s := p.KafkaAPI.SASL; s != nil {
254+
if strings.EqualFold(s.Mechanism, adminapi.OAuthBearer) {
255+
opts = append(opts, rpadmin.WithOAuthBearerAuthentication(adminapi.OAuthBearerToken(s.Password)))
256+
} else if p.HasSASLCredentials() {
257+
opts = append(opts, rpadmin.WithSCRAMAuthentication(s.User, s.Password, s.Mechanism))
258+
}
266259
}
267260
if tls := p.KafkaAPI.TLS; tls != nil {
268261
opts = append(opts, rpadmin.WithTLS(true, tls.InsecureSkipVerify))

tests/rptest/services/admin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from requests.exceptions import HTTPError, RequestException
2727
from urllib3.util.retry import Retry
2828

29-
from rptest.services.redpanda_types import SaslCredentials
29+
from rptest.services.redpanda_types import OAuthBearerCredentials, SaslCredentials
3030
from rptest.util import not_none, wait_until_result
3131
from rptest.utils.mode_checks import is_debug_mode
3232

@@ -456,7 +456,7 @@ class DebugBundleLabelSelection:
456456

457457

458458
class DebugBundleStartConfigParams(NamedTuple):
459-
authentication: Optional[SaslCredentials] = None
459+
authentication: Optional[SaslCredentials | OAuthBearerCredentials] = None
460460
controller_logs_size_limit_bytes: Optional[int] = None
461461
cpu_profiler_wait_seconds: Optional[int] = None
462462
logs_since: Optional[str] = None

tests/rptest/services/redpanda_types.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ def mechanism(self):
4646
return self.algorithm
4747

4848

49+
@dataclass
50+
class OAuthBearerCredentials:
51+
"""Bearer token credentials for OAUTHBEARER authentication in debug bundle."""
52+
53+
token: str
54+
mechanism: str = "OAUTHBEARER"
55+
56+
4957
class SecurityProtocol(Enum):
5058
"""The four possible security protocol options for Kafka authentication."""
5159

tests/rptest/tests/debug_bundle_test.py

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
import time
1616
import zipfile
1717
from typing import Optional
18+
from urllib.parse import urlparse
1819
from uuid import UUID, uuid4
1920

2021
import requests
2122
from ducktape.cluster.cluster import ClusterNode
2223
from ducktape.mark import matrix
2324
from ducktape.services.service import Service
2425
from ducktape.utils.util import wait_until
26+
from keycloak import KeycloakOpenID
2527

2628
from rptest.clients.rpk import RpkTool
2729
from rptest.services.admin import (
@@ -31,6 +33,7 @@
3133
DebugBundleStartConfigParams,
3234
)
3335
from rptest.services.cluster import cluster
36+
from rptest.services.keycloak import DEFAULT_REALM, KeycloakService
3437
from rptest.services.redpanda import (
3538
LoggingConfig,
3639
MetricSamples,
@@ -39,11 +42,13 @@
3942
SecurityConfig,
4043
TLSProvider,
4144
)
42-
from rptest.services.redpanda_types import SaslCredentials
45+
from rptest.services.redpanda_types import OAuthBearerCredentials, SaslCredentials
4346
from rptest.services.tls import Certificate, CertificateAuthority, TLSCertManager
4447
from rptest.tests.redpanda_test import RedpandaTest
4548
from rptest.util import wait_until_result
4649

50+
_OIDC_TOKEN_AUDIENCE = "account"
51+
4752

4853
class DebugBundleErrorCode:
4954
SUCCESS = 0
@@ -734,3 +739,98 @@ def test_validate_tls(self):
734739
)
735740
content = self._retrieve_file(node=node)
736741
self._validate_topic_name_in_response(content, self.topic_name, True)
742+
743+
744+
class DebugBundleOAuthBearerAuthn(DebugBundleTestBase):
745+
"""
746+
End-to-end test verifying OAUTHBEARER authentication for the debug bundle.
747+
748+
The broker receives {mechanism: OAUTHBEARER, token: JWT} in the start
749+
request and forwards it as -Xsasl.mechanism=OAUTHBEARER and
750+
-Xpass=token:<JWT> to the rpk subprocess. The subprocess uses those
751+
credentials to authenticate to Kafka, producing a successful bundle.
752+
"""
753+
754+
_CLIENT_ID = "debug-bundle-test"
755+
_TOPIC = "oauthbearer_test_topic"
756+
757+
def __init__(self, context):
758+
# Allocate the Keycloak node now so we can derive its hostname for
759+
# oidc_discovery_url before creating the Redpanda service. The node
760+
# is not started until setUp() so that self.logger is available for
761+
# error handling by then.
762+
self.keycloak = KeycloakService(context)
763+
kc_node = self.keycloak.nodes[0]
764+
kc_discovery_url = self.keycloak.get_discovery_url(kc_node)
765+
766+
security = SecurityConfig()
767+
security.enable_sasl = True
768+
security.sasl_mechanisms = ["SCRAM", "OAUTHBEARER"]
769+
security.kafka_enable_authorization = False
770+
security.endpoint_authn_method = "sasl"
771+
772+
super().__init__(
773+
context,
774+
num_brokers=1,
775+
security=security,
776+
extra_rp_conf={
777+
"oidc_discovery_url": kc_discovery_url,
778+
"oidc_token_audience": _OIDC_TOKEN_AUDIENCE,
779+
},
780+
)
781+
782+
self.super_admin = Admin(self.redpanda)
783+
self.rpk = RpkTool(
784+
self.redpanda,
785+
username=self.redpanda.SUPERUSER_CREDENTIALS.username,
786+
password=self.redpanda.SUPERUSER_CREDENTIALS.password,
787+
sasl_mechanism=self.redpanda.SUPERUSER_CREDENTIALS.algorithm,
788+
)
789+
790+
def setUp(self):
791+
kc_node = self.keycloak.nodes[0]
792+
try:
793+
self.keycloak.start_node(kc_node)
794+
except Exception as e:
795+
self.logger.error(f"Keycloak failed to start: {e}")
796+
self.keycloak.clean_node(kc_node)
797+
raise
798+
799+
super().setUp()
800+
801+
self.keycloak.admin.create_client(self._CLIENT_ID)
802+
self.oauth_cfg = self.keycloak.generate_oauth_config(kc_node, self._CLIENT_ID)
803+
self.rpk.create_topic(self._TOPIC)
804+
805+
def _get_access_token(self) -> str:
806+
cfg = self.oauth_cfg
807+
parsed = urlparse(cfg.token_endpoint)
808+
openid = KeycloakOpenID(
809+
server_url=f"{parsed.scheme}://{parsed.netloc}",
810+
client_id=cfg.client_id,
811+
client_secret_key=cfg.client_secret,
812+
realm_name=DEFAULT_REALM,
813+
verify=False,
814+
)
815+
return openid.token(grant_type="client_credentials")["access_token"]
816+
817+
@cluster(num_nodes=2)
818+
def test_oauthbearer_auth(self):
819+
"""
820+
Verify that the broker correctly forwards the bearer token to the rpk
821+
subprocess, which uses OAUTHBEARER to authenticate to Kafka and produce
822+
a successful debug bundle containing the expected topic metadata.
823+
"""
824+
node = self.redpanda.started_nodes()[0]
825+
token = self._get_access_token()
826+
827+
job_id = uuid4()
828+
config = DebugBundleStartConfigParams(
829+
authentication=OAuthBearerCredentials(token=token)
830+
)
831+
self._run_debug_bundle(
832+
job_id=job_id, node=node, config=config, admin=self.super_admin
833+
)
834+
835+
content = self._retrieve_file(node=node, admin=self.super_admin)
836+
self._validate_topic_name_in_response(content, self._TOPIC, True)

0 commit comments

Comments
 (0)