This is an automated email from the ASF dual-hosted git repository.
dongjoon-hyun pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.x by this push:
new 749972239a05 [SPARK-59923][CORE] Warn when OIDC credential propagation
is enabled without RPC encryption
749972239a05 is described below
commit 749972239a05d1c021e703305805d990198aa1a4
Author: Kousuke Saruta <[email protected]>
AuthorDate: Fri Oct 2 08:20:51 2026 -0700
[SPARK-59923][CORE] Warn when OIDC credential propagation is enabled
without RPC encryption
### What changes were proposed in this pull request?
When OIDC credential propagation is enabled
(`spark.security.oidc.enabled=true`) but Spark's RPC channel encryption is not
configured, Spark now logs a warning at startup.
The check is added to `UserCredentialManager.create()`, which runs once on
the driver when the scheduler backend starts. It reuses the same "RPC
encryption is enabled" criteria as `HadoopDelegationTokenManager`'s
direct-credential-provider path:
- `spark.ssl.rpc.enabled=true`, or
- `spark.authenticate=true` together with one of
`spark.network.crypto.enabled` / `spark.authenticate.enableSaslEncryption`.
Unlike the `directCredentialProviders` path (which always fails fast via
`require()`), OIDC credential propagation warns by default. Operators can opt
into fail-fast behavior by setting
`spark.security.oidc.requireRpcEncryption=true`, which makes Spark refuse to
start unless RPC encryption is configured (the default is `false`, preserving
the SPIP's warn-by-default behavior). The predicate and the remediation message
are shared with `HadoopDelegationTokenManager` via `SecurityManager` [...]
`docs/security.md` is updated to document both the warning and the new
`spark.security.oidc.requireRpcEncryption` config.
### Why are the changes needed?
OIDC-derived service credentials are transmitted to executors over Spark's
RPC channel (via
`UpdateUserCredentials` broadcasts and `TaskDescription` delivery). If RPC
encryption is not configured, those credentials travel in cleartext.
The SPIP (design document) specifies this behavior:
> Enabling credential propagation without RPC encryption logs a warning by
default. A stricter
> mode (`spark.security.credentials.requireEncryption=true`) that fails
application startup unless
> RPC encryption is enabled may be introduced based on community feedback.
This PR implements both the warning and that stricter mode. The stricter
mode is exposed as `spark.security.oidc.requireRpcEncryption` (default `false`)
rather than the SPIP's tentative `spark.security.credentials.requireEncryption`
name, to stay within the existing `spark.security.oidc.*` namespace for this
feature (`spark.security.credentials.*` is the Kerberos /
direct-credential-provider namespace). The design and the documentation already
assumed the warning, but the implementati [...]
### Does this PR introduce _any_ user-facing change?
Yes. A warning is now logged at driver startup when OIDC credential
propagation is enabled without RPC encryption configured. By default startup is
not blocked. Additionally, a new config
`spark.security.oidc.requireRpcEncryption` (default `false`) is introduced:
when set to `true`, Spark fails startup (throws `IllegalArgumentException`) if
OIDC credential propagation is enabled without RPC encryption, instead of only
warning. This also fails startup in local mode, which is consistent [...]
### How was this patch tested?
New and updated unit tests:
- `SecurityManagerSuite`: `isRpcEncryptionEnabled` truth-table coverage for
the shared predicate (nothing configured / SSL / auth+crypto / auth+SASL / SSL
together with auth+crypto / SSL with crypto only / auth-only-without-encryption
/ crypto-only-without-auth / SASL-only-without-auth).
- `UserCredentialManagerSuite`: `checkRpcEncryption` emits / does not emit
the warning (default mode); fails fast with `IllegalArgumentException` when
`spark.security.oidc.requireRpcEncryption` is true and RPC encryption is
missing, and does not fail when encryption is configured; and `create()` emits
the warning (or fails fast under the enforce flag) when OIDC is enabled without
encryption but does nothing when OIDC is disabled (guards the wiring). A
further test pins the check order [...]
- `NonKerberosCredentialsSuite`: the direct-credential-provider `require()`
still rejects a config with no RPC encryption and accepts SASL / SSL RPC
encryption, now exercising the shared `SecurityManager.isRpcEncryptionEnabled`
predicate.
`SecurityManagerSuite`, `UserCredentialManagerSuite`, and
`NonKerberosCredentialsSuite` pass.
`core/scalastyle` passes.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Kiro CLI / Claude
Closes #59185 from sarutak/oidc-propagation/rpc-encryption-warning.
Authored-by: Kousuke Saruta <[email protected]>
Signed-off-by: Dongjoon Hyun <[email protected]>
(cherry picked from commit 5adc30a74c2da2c7194e5d965da029d418b38020)
Signed-off-by: Dongjoon Hyun <[email protected]>
---
.../scala/org/apache/spark/SecurityManager.scala | 51 +++++++++-
.../security/HadoopDelegationTokenManager.scala | 13 +--
.../deploy/security/UserCredentialManager.scala | 49 +++++++++-
.../org/apache/spark/internal/config/package.scala | 11 +++
.../org/apache/spark/SecurityManagerSuite.scala | 68 ++++++++++++++
.../security/UserCredentialManagerSuite.scala | 104 ++++++++++++++++++++-
docs/security.md | 20 +++-
7 files changed, 302 insertions(+), 14 deletions(-)
diff --git a/core/src/main/scala/org/apache/spark/SecurityManager.scala
b/core/src/main/scala/org/apache/spark/SecurityManager.scala
index 1461677219bc..ddfeae900fca 100644
--- a/core/src/main/scala/org/apache/spark/SecurityManager.scala
+++ b/core/src/main/scala/org/apache/spark/SecurityManager.scala
@@ -86,8 +86,7 @@ private[spark] class SecurityManager(
private var secretKey: String = _
- private val sslRpcEnabled = sparkConf.getBoolean(
- "spark.ssl.rpc.enabled", false)
+ private val sslRpcEnabled = sparkConf.getBoolean(SSL_RPC_ENABLED_CONF, false)
logInfo(log"SecurityManager: authentication ${MDC(LogKeys.AUTH_ENABLED,
if (authOn) "enabled" else "disabled")}" +
@@ -275,6 +274,12 @@ private[spark] class SecurityManager(
/**
* Checks whether network encryption should be enabled.
+ *
+ * This reports only whether AES/SASL *network* encryption is in effect, and
treats SSL RPC as
+ * *disabling* it (AES/SASL and SSL RPC are mutually exclusive transports).
To ask instead "is
+ * the RPC channel encrypted at all?" (treating SSL RPC as encryption and
requiring
+ * `spark.authenticate` for the AES/SASL path), use
[[SecurityManager.isRpcEncryptionEnabled]].
+ *
* @return Whether to enable encryption when connecting to services that
support it.
*/
def isEncryptionEnabled(): Boolean = {
@@ -454,4 +459,46 @@ private[spark] object SecurityManager {
// key used to store the spark secret in the Hadoop UGI
val SECRET_LOOKUP_KEY = new Text("sparkCookie")
+
+ // `spark.ssl.rpc.enabled` has no Scala ConfigEntry (it is read directly in
TransportConf), so
+ // its key is defined here as a single source of truth for the code that
needs to read it.
+ private[spark] val SSL_RPC_ENABLED_CONF = "spark.ssl.rpc.enabled"
+
+ /**
+ * Returns true if Spark's RPC channel is encrypted by any supported
mechanism: SSL-based RPC
+ * encryption (`spark.ssl.rpc.enabled`), or authentication
(`spark.authenticate`) enabled
+ * together with either AES-based encryption
(`spark.network.crypto.enabled`) or the separate
+ * (deprecated) SASL-based encryption
(`spark.authenticate.enableSaslEncryption`).
+ *
+ * This is intentionally distinct from the instance method
+ * [[SecurityManager.isEncryptionEnabled]]:
+ * - `isEncryptionEnabled` reports whether AES/SASL *network* encryption is
in effect and
+ * treats SSL RPC as *disabling* it (returning false when
`spark.ssl.rpc.enabled` is set),
+ * because AES/SASL and SSL RPC are mutually exclusive transports. It
also does not require
+ * `spark.authenticate`, so it can return true for a configuration that
does not actually
+ * encrypt the channel.
+ * - This predicate instead answers "is the RPC channel encrypted at all?",
treating SSL RPC
+ * as encryption and requiring `spark.authenticate` for the AES/SASL
path. It must not be
+ * replaced by `isEncryptionEnabled`.
+ *
+ * It is shared by the two credential-propagation paths so they cannot drift
apart: the
+ * direct-credential-provider path in `HadoopDelegationTokenManager` fails
startup via
+ * `require()`, while OIDC credential propagation warns by default and fails
startup only when
+ * `spark.security.oidc.requireRpcEncryption` is true.
+ */
+ private[spark] def isRpcEncryptionEnabled(conf: SparkConf): Boolean = {
+ conf.getBoolean(SSL_RPC_ENABLED_CONF, false) ||
+ (conf.get(NETWORK_AUTH_ENABLED) &&
+ (conf.get(Network.NETWORK_CRYPTO_ENABLED) ||
conf.get(SASL_ENCRYPTION_ENABLED)))
+ }
+
+ /**
+ * Human-readable remediation describing how to enable RPC channel
encryption. Shared by the
+ * OIDC credential-propagation warning and the direct-credential-provider
`require()` message so
+ * the two stay in sync with [[isRpcEncryptionEnabled]].
+ */
+ private[spark] val rpcEncryptionRemediation: String =
+ s"Enable RPC encryption via $SSL_RPC_ENABLED_CONF=true, or
${NETWORK_AUTH_ENABLED.key}=true " +
+ s"together with one of ${Network.NETWORK_CRYPTO_ENABLED.key} / " +
+ s"${SASL_ENCRYPTION_ENABLED.key}."
}
diff --git
a/core/src/main/scala/org/apache/spark/deploy/security/HadoopDelegationTokenManager.scala
b/core/src/main/scala/org/apache/spark/deploy/security/HadoopDelegationTokenManager.scala
index b98a71b13cba..dec0f2b31a07 100644
---
a/core/src/main/scala/org/apache/spark/deploy/security/HadoopDelegationTokenManager.scala
+++
b/core/src/main/scala/org/apache/spark/deploy/security/HadoopDelegationTokenManager.scala
@@ -30,12 +30,11 @@ import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.fs.FileSystem
import org.apache.hadoop.security.{Credentials, UserGroupInformation}
-import org.apache.spark.SparkConf
+import org.apache.spark.{SecurityManager, SparkConf}
import org.apache.spark.deploy.SparkHadoopUtil
import org.apache.spark.internal.Logging
import org.apache.spark.internal.LogKeys
import org.apache.spark.internal.config._
-import org.apache.spark.internal.config.Network.NETWORK_CRYPTO_ENABLED
import org.apache.spark.rpc.RpcEndpointRef
import
org.apache.spark.scheduler.cluster.CoarseGrainedClusterMessages.UpdateDelegationTokens
import org.apache.spark.security.HadoopDelegationTokenProvider
@@ -79,14 +78,10 @@ private[spark] class HadoopDelegationTokenManager(
"Both principal and keytab must be defined, or neither.")
if (sparkConf.get(DIRECT_CREDENTIAL_PROVIDERS_ENABLED)) {
- val hasEncryption = sparkConf.getBoolean("spark.ssl.rpc.enabled", false) ||
- (sparkConf.get(NETWORK_AUTH_ENABLED) &&
- (sparkConf.get(NETWORK_CRYPTO_ENABLED) ||
sparkConf.get(SASL_ENCRYPTION_ENABLED)))
- require(hasEncryption,
+ require(SecurityManager.isRpcEncryptionEnabled(sparkConf),
"RPC channel encryption must be enabled when " +
- "spark.security.directCredentialProviders.enabled is true: either " +
- "spark.ssl.rpc.enabled=true, or spark.authenticate=true together with " +
- "spark.network.crypto.enabled or
spark.authenticate.enableSaslEncryption. " +
+ s"${DIRECT_CREDENTIAL_PROVIDERS_ENABLED.key} is true. " +
+ s"${SecurityManager.rpcEncryptionRemediation} " +
"Credential tokens must not be transmitted over unencrypted channels.")
}
diff --git
a/core/src/main/scala/org/apache/spark/deploy/security/UserCredentialManager.scala
b/core/src/main/scala/org/apache/spark/deploy/security/UserCredentialManager.scala
index 9d6a72a48884..3f7ceea9c4bb 100644
---
a/core/src/main/scala/org/apache/spark/deploy/security/UserCredentialManager.scala
+++
b/core/src/main/scala/org/apache/spark/deploy/security/UserCredentialManager.scala
@@ -28,7 +28,7 @@ import scala.collection.mutable
import scala.jdk.CollectionConverters._
import scala.util.control.NonFatal
-import org.apache.spark.SparkConf
+import org.apache.spark.{SecurityManager, SparkConf}
import org.apache.spark.internal.Logging
import org.apache.spark.internal.LogKeys
import org.apache.spark.internal.config._
@@ -513,12 +513,59 @@ private[spark] object UserCredentialManager extends
Logging {
s"${SECURITY_OIDC_ENABLED.key} is true")
}
+ // Credentials are transmitted to executors over Spark's RPC channel (via
+ // UpdateUserCredentials broadcasts and TaskDescription delivery). If
RPC encryption is not
+ // configured, those credentials travel in cleartext. Per the SPIP,
enabling credential
+ // propagation without RPC encryption logs a warning by default;
operators can opt into a
+ // fail-fast mode via spark.security.oidc.requireRpcEncryption. This
check runs after the
+ // required-config checks above so a missing token file is not masked by
an encryption
+ // warning/error about an unrelated setting.
+ checkRpcEncryption(sparkConf)
+
val tokenIngestor = new FileTokenIngestor(Paths.get(tokenFile))
Some(new UserCredentialManager(
sparkConf, tokenIngestor, onCredentialsUpdate, selectionLoader))
}
}
+ /**
+ * Checks RPC channel encryption when OIDC credential propagation is
enabled, since credentials
+ * are otherwise transmitted over the RPC channel in cleartext.
+ *
+ * By default this only logs a warning, per the SPIP's warn-by-default
behavior. When
+ * `spark.security.oidc.requireRpcEncryption` is true, it instead fails fast
with an
+ * `IllegalArgumentException`, mirroring [[HadoopDelegationTokenManager]]'s
+ * direct-credential-provider path.
+ *
+ * The encryption predicate and the remediation hint are shared with
+ * [[HadoopDelegationTokenManager]]'s `require()` via [[SecurityManager]],
so the warning, the
+ * fail-fast message here, and the direct-provider `require()` message
cannot drift apart.
+ */
+ private[security] def checkRpcEncryption(sparkConf: SparkConf): Unit = {
+ if (!SecurityManager.isRpcEncryptionEnabled(sparkConf)) {
+ if (sparkConf.get(SECURITY_OIDC_REQUIRE_RPC_ENCRYPTION)) {
+ throw new IllegalArgumentException(
+ s"OIDC credential propagation (${SECURITY_OIDC_ENABLED.key}=true)
requires RPC channel " +
+ s"encryption when
${SECURITY_OIDC_REQUIRE_RPC_ENCRYPTION.key}=true, but it is not " +
+ s"configured. ${SecurityManager.rpcEncryptionRemediation}")
+ }
+ // The remediation hint is carried under MDC(LogKeys.MESSAGE, ...)
rather than
+ // MDC(LogKeys.REASON, ...): REASON is used elsewhere for the reason an
operation failed, so
+ // keeping the hint out of it leaves the structured `reason` field clean
for real failure
+ // reasons. We say credentials "may be" (not "will be") transmitted in
cleartext because the
+ // channel could still be protected outside Spark (e.g. a service mesh
or a private network),
+ // which Spark cannot detect. We also point operators at the fail-fast
mode so those who want
+ // Spark to enforce encryption (rather than warn) know how to opt in.
+ logWarning(log"OIDC credential propagation is enabled " +
+ log"(${MDC(LogKeys.CONFIG, SECURITY_OIDC_ENABLED.key)}=true) but RPC
channel encryption " +
+ log"is not configured. Credentials may be transmitted to executors
over an unencrypted " +
+ log"channel. " + log"${MDC(LogKeys.MESSAGE,
SecurityManager.rpcEncryptionRemediation)}" +
+ log" This is a warning by default; set " +
+ log"${MDC(LogKeys.CONFIG2,
SECURITY_OIDC_REQUIRE_RPC_ENCRYPTION.key)}=true to fail fast " +
+ log"instead.")
+ }
+ }
+
/**
* Selection phase of OIDC credential propagation, run early during
`SparkContext`
* initialization (before the driver's Hadoop `Configuration` is
materialized).
diff --git a/core/src/main/scala/org/apache/spark/internal/config/package.scala
b/core/src/main/scala/org/apache/spark/internal/config/package.scala
index 0d57cfaf9452..90f3e08f367e 100644
--- a/core/src/main/scala/org/apache/spark/internal/config/package.scala
+++ b/core/src/main/scala/org/apache/spark/internal/config/package.scala
@@ -1746,6 +1746,17 @@ package object config {
.checkValue(_ > 0, "The minimum renewal interval must be a positive time
value.")
.createWithDefaultString("30s")
+ private[spark] val SECURITY_OIDC_REQUIRE_RPC_ENCRYPTION =
+ ConfigBuilder("spark.security.oidc.requireRpcEncryption")
+ .doc("When OIDC credential propagation is enabled, whether to require
RPC channel " +
+ "encryption. When true, Spark refuses to start if RPC encryption is
not configured, " +
+ "instead of only logging a warning. Defaults to false to preserve the
warn-by-default " +
+ "behavior described in the SPIP.")
+ .version("4.4.0")
+ .withBindingPolicy(ConfigBindingPolicy.NOT_APPLICABLE)
+ .booleanConf
+ .createWithDefault(false)
+
private[spark] val DIRECT_CREDENTIAL_PROVIDERS_ENABLED =
ConfigBuilder("spark.security.directCredentialProviders.enabled")
.doc(
diff --git a/core/src/test/scala/org/apache/spark/SecurityManagerSuite.scala
b/core/src/test/scala/org/apache/spark/SecurityManagerSuite.scala
index f4ecc540155d..b9ec0726074a 100644
--- a/core/src/test/scala/org/apache/spark/SecurityManagerSuite.scala
+++ b/core/src/test/scala/org/apache/spark/SecurityManagerSuite.scala
@@ -513,5 +513,73 @@ class SecurityManagerSuite extends SparkFunSuite with
ResetSystemProperties {
private def encodeFileAsBase64(secretFile: File) = {
Base64.getEncoder.encodeToString(Files.readAllBytes(secretFile.toPath))
}
+
+ // ========== SecurityManager.isRpcEncryptionEnabled (shared RPC-encryption
predicate) ==========
+ //
+ // This predicate is distinct from the instance method
isEncryptionEnabled(): it treats SSL RPC
+ // as encryption (rather than as disabling AES/SASL) and requires
spark.authenticate for the
+ // AES/SASL path. The cases below pin that contract, including the
combinations that the OIDC
+ // credential-propagation warning and the direct-credential-provider
require() both depend on.
+
+ test("isRpcEncryptionEnabled: false when nothing is configured") {
+ assert(!SecurityManager.isRpcEncryptionEnabled(new SparkConf(loadDefaults
= false)))
+ }
+
+ test("isRpcEncryptionEnabled: true with SSL RPC encryption") {
+ val conf = new SparkConf(loadDefaults =
false).set(SecurityManager.SSL_RPC_ENABLED_CONF, "true")
+ assert(SecurityManager.isRpcEncryptionEnabled(conf))
+ }
+
+ test("isRpcEncryptionEnabled: true with authentication + network crypto") {
+ val conf = new SparkConf(loadDefaults = false)
+ .set(NETWORK_AUTH_ENABLED, true)
+ .set(Network.NETWORK_CRYPTO_ENABLED, true)
+ assert(SecurityManager.isRpcEncryptionEnabled(conf))
+ }
+
+ test("isRpcEncryptionEnabled: true with authentication + SASL encryption") {
+ val conf = new SparkConf(loadDefaults = false)
+ .set(NETWORK_AUTH_ENABLED, true)
+ .set(SASL_ENCRYPTION_ENABLED, true)
+ assert(SecurityManager.isRpcEncryptionEnabled(conf))
+ }
+
+ test("isRpcEncryptionEnabled: true with SSL RPC encryption together with
authentication + " +
+ "network crypto") {
+ // SSL RPC is a sufficient condition on its own; combining it with the AES
path must stay true.
+ val conf = new SparkConf(loadDefaults = false)
+ .set(SecurityManager.SSL_RPC_ENABLED_CONF, "true")
+ .set(NETWORK_AUTH_ENABLED, true)
+ .set(Network.NETWORK_CRYPTO_ENABLED, true)
+ assert(SecurityManager.isRpcEncryptionEnabled(conf))
+ }
+
+ test("isRpcEncryptionEnabled: true with SSL RPC encryption and network
crypto but no auth") {
+ // SSL RPC alone encrypts the channel; the missing spark.authenticate only
gates the AES/SASL
+ // path, so the disjunction is still true via SSL.
+ val conf = new SparkConf(loadDefaults = false)
+ .set(SecurityManager.SSL_RPC_ENABLED_CONF, "true")
+ .set(Network.NETWORK_CRYPTO_ENABLED, true)
+ assert(SecurityManager.isRpcEncryptionEnabled(conf))
+ }
+
+ test("isRpcEncryptionEnabled: false when authentication is enabled without
encryption") {
+ // Authentication alone (without crypto/SASL encryption) does not encrypt
the channel.
+ val conf = new SparkConf(loadDefaults = false).set(NETWORK_AUTH_ENABLED,
true)
+ assert(!SecurityManager.isRpcEncryptionEnabled(conf))
+ }
+
+ test("isRpcEncryptionEnabled: false when network crypto is enabled without
authentication") {
+ // spark.network.crypto.enabled only takes effect with
spark.authenticate=true; on its own it
+ // does not encrypt anything, so it must not be treated as RPC encryption.
(This is also where
+ // isEncryptionEnabled() would differ: it returns true here even though
nothing is encrypted.)
+ val conf = new SparkConf(loadDefaults =
false).set(Network.NETWORK_CRYPTO_ENABLED, true)
+ assert(!SecurityManager.isRpcEncryptionEnabled(conf))
+ }
+
+ test("isRpcEncryptionEnabled: false when SASL encryption is enabled without
authentication") {
+ val conf = new SparkConf(loadDefaults =
false).set(SASL_ENCRYPTION_ENABLED, true)
+ assert(!SecurityManager.isRpcEncryptionEnabled(conf))
+ }
}
diff --git
a/core/src/test/scala/org/apache/spark/deploy/security/UserCredentialManagerSuite.scala
b/core/src/test/scala/org/apache/spark/deploy/security/UserCredentialManagerSuite.scala
index e8ff807e7173..a1e49481ed33 100644
---
a/core/src/test/scala/org/apache/spark/deploy/security/UserCredentialManagerSuite.scala
+++
b/core/src/test/scala/org/apache/spark/deploy/security/UserCredentialManagerSuite.scala
@@ -28,7 +28,7 @@ import scala.jdk.CollectionConverters._
import org.scalatest.concurrent.Eventually.{eventually, timeout}
-import org.apache.spark.{SparkConf, SparkFunSuite}
+import org.apache.spark.{SecurityManager, SparkConf, SparkFunSuite}
import org.apache.spark.internal.config._
import org.apache.spark.security._
@@ -858,4 +858,106 @@ class UserCredentialManagerSuite extends SparkFunSuite {
AnotherFakeCredentialProvider.throwOnProperties = false
}
}
+
+ // ========== RPC encryption warning (OIDC wiring) ==========
+ //
+ // The RPC-encryption predicate itself
(SecurityManager.isRpcEncryptionEnabled) is unit-tested in
+ // SecurityManagerSuite. Here we only verify the OIDC-specific wiring: that
the warning fires (and
+ // does not fire) for the right configurations. Assertions match on a stable
fragment plus the
+ // WARN level rather than the full message text, so rewording the message
does not break them.
+
+ // A fragment that stays stable across rewording: the OIDC config key is
always named verbatim.
+ private val oidcWarningKeyFragment = SECURITY_OIDC_ENABLED.key
+
+ private def hasOidcEncryptionWarning(appender: LogAppender): Boolean =
+ appender.loggingEvents.exists { e =>
+ e.getLevel == org.apache.logging.log4j.Level.WARN &&
+ e.getMessage.getFormattedMessage.contains(oidcWarningKeyFragment) &&
+ e.getMessage.getFormattedMessage.contains("encryption")
+ }
+
+ test("checkRpcEncryption: warns when encryption is not configured") {
+ val appender = new LogAppender(oidcWarningKeyFragment)
+ withLogAppender(appender) {
+ UserCredentialManager.checkRpcEncryption(new SparkConf(loadDefaults =
false))
+ }
+ assert(hasOidcEncryptionWarning(appender))
+ }
+
+ test("checkRpcEncryption: does not warn when encryption is configured") {
+ val appender = new LogAppender(oidcWarningKeyFragment)
+ withLogAppender(appender) {
+ val conf = new SparkConf(loadDefaults = false)
+ .set(SecurityManager.SSL_RPC_ENABLED_CONF, "true")
+ UserCredentialManager.checkRpcEncryption(conf)
+ }
+ assert(!hasOidcEncryptionWarning(appender))
+ }
+
+ test("checkRpcEncryption: fails fast when requireRpcEncryption is true
without encryption") {
+ val conf = new SparkConf(loadDefaults = false)
+ .set(SECURITY_OIDC_REQUIRE_RPC_ENCRYPTION, true)
+ val e = intercept[IllegalArgumentException] {
+ UserCredentialManager.checkRpcEncryption(conf)
+ }
+ assert(e.getMessage.contains(SECURITY_OIDC_REQUIRE_RPC_ENCRYPTION.key))
+ assert(e.getMessage.contains("RPC channel"))
+ }
+
+ test("checkRpcEncryption: does not fail when requireRpcEncryption is true
with encryption") {
+ val appender = new LogAppender(oidcWarningKeyFragment)
+ withLogAppender(appender) {
+ val conf = new SparkConf(loadDefaults = false)
+ .set(SECURITY_OIDC_REQUIRE_RPC_ENCRYPTION, true)
+ .set(SecurityManager.SSL_RPC_ENABLED_CONF, "true")
+ UserCredentialManager.checkRpcEncryption(conf) // must not throw
+ }
+ // When encryption is configured, neither the warning nor the exception
fires.
+ assert(!hasOidcEncryptionWarning(appender))
+ }
+
+ test("create warns when OIDC is enabled without RPC encryption") {
+ // Guards the wiring: create() must invoke the encryption warning for an
enabled config.
+ val appender = new LogAppender(oidcWarningKeyFragment)
+ withLogAppender(appender) {
+ val conf = createSparkConf() // OIDC enabled, no RPC encryption
configured
+ UserCredentialManager.create(
+ conf, (_, _) => (), Some(new CredentialProviderLoader()))
+ }
+ assert(hasOidcEncryptionWarning(appender))
+ }
+
+ test("create fails fast when OIDC requires RPC encryption but it is not
configured") {
+ // Guards the wiring: create() must enforce when requireRpcEncryption is
set.
+ val conf = createSparkConf().set(SECURITY_OIDC_REQUIRE_RPC_ENCRYPTION,
true)
+ val e = intercept[IllegalArgumentException] {
+ UserCredentialManager.create(conf, (_, _) => (), Some(new
CredentialProviderLoader()))
+ }
+ assert(e.getMessage.contains(SECURITY_OIDC_REQUIRE_RPC_ENCRYPTION.key))
+ }
+
+ test("create: a missing token file is reported before the RPC-encryption
enforce error") {
+ // Pins the check order: with requireRpcEncryption=true and no RPC
encryption, both the
+ // token-file check and checkRpcEncryption would throw. The
required-config checks must run
+ // first, so the token-file error (not the encryption enforce error)
surfaces. Moving
+ // checkRpcEncryption before the token-file check would fail this test.
+ val conf = new SparkConf(loadDefaults = false)
+ .set(SECURITY_OIDC_ENABLED, true)
+ .set(SECURITY_OIDC_REQUIRE_RPC_ENCRYPTION, true)
+ // Deliberately not setting SECURITY_OIDC_IDENTITY_TOKEN_FILE and no RPC
encryption.
+ val e = intercept[IllegalArgumentException] {
+ UserCredentialManager.create(conf, (_, _) => (), Some(new
CredentialProviderLoader()))
+ }
+ assert(e.getMessage.contains(SECURITY_OIDC_IDENTITY_TOKEN_FILE.key))
+ assert(!e.getMessage.contains(SECURITY_OIDC_REQUIRE_RPC_ENCRYPTION.key))
+ }
+
+ test("create does not warn when OIDC is disabled") {
+ val appender = new LogAppender(oidcWarningKeyFragment)
+ withLogAppender(appender) {
+ val conf = new SparkConf(loadDefaults =
false).set(SECURITY_OIDC_ENABLED, false)
+ UserCredentialManager.create(conf, (_, _) => (), Some(new
CredentialProviderLoader()))
+ }
+ assert(!hasOidcEncryptionWarning(appender))
+ }
}
diff --git a/docs/security.md b/docs/security.md
index 2affb692603c..b921598b6d6b 100644
--- a/docs/security.md
+++ b/docs/security.md
@@ -1222,7 +1222,12 @@ delegation tokens rather than the original
ticket-granting ticket.
* Service credentials are not written to shuffle files, event logs, or
checkpoints.
* Credentials are carried over Spark's RPC channels and therefore rely on
Spark's existing RPC
encryption configuration. Operators are strongly encouraged to enable RPC
encryption whenever
- credential propagation is enabled. See [Network
Encryption](#network-encryption) for details.
+ credential propagation is enabled. If credential propagation is enabled
while RPC encryption is
+ not configured, Spark logs a warning at startup, since credentials would
otherwise be transmitted
+ to executors over an unencrypted channel. To turn this into a hard
requirement, set
+ `spark.security.oidc.requireRpcEncryption=true`, which makes Spark refuse to
start unless RPC
+ encryption is configured. See [Network Encryption](#network-encryption) for
+ details.
## Custom CredentialProvider
@@ -1313,6 +1318,19 @@ each provider.
</td>
<td>4.4.0</td>
</tr>
+<tr>
+ <td><code>spark.security.oidc.requireRpcEncryption</code></td>
+ <td><code>false</code></td>
+ <td>
+ When OIDC credential propagation is enabled, whether to require RPC
channel encryption. By
+ default (<code>false</code>), Spark only logs a warning if credential
propagation is enabled
+ without RPC encryption, since credentials would then travel over the RPC
channel in cleartext.
+ When set to <code>true</code>, Spark instead refuses to start unless RPC
encryption is
+ configured. Leave it <code>false</code> if the channel is protected
outside Spark (for
+ example, a service mesh or a private network).
+ </td>
+ <td>4.4.0</td>
+</tr>
<tr>
<td><code>spark.security.oidc.identityToken.file</code></td>
<td>(none)</td>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]