Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,14 @@ Unlike the other SSL settings for the UI, the RPC SSL is *not* automatically ena
`spark.ssl.enabled` is set. It must be explicitly enabled, to ensure a safe migration path for users
upgrading Spark versions.

On Kubernetes, the passwords used by RPC SSL are propagated to executor pods using environment
variables, the same way as the authentication mechanics described under [Kubernetes](#kubernetes)
above. As with the authentication secret, any user who can list pods in the namespace can read
them, so access control rules should be properly set up or alternatively these passwords should
be supplied using `spark.kubernetes.executor.secretKeyRef`, for example
`spark.kubernetes.executor.secretKeyRef._SPARK_SSL_RPC_KEY_STORE_PASSWORD=<secret-name>:<key>`.
Binding variables this way takes precedence.

## AES-based Encryption (Legacy)

Spark supports AES-based encryption for RPC connections. For encryption to be enabled, RPC
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,20 @@ private[spark] class BasicExecutorFeatureStep(
case _ => Nil
}.getOrElse(Nil)

// SparkConf.isExecutorStartupConf withholds the spark.ssl.* passwords from the
// executor conf. Pass them through the environment, as the standalone worker
// does in CommandUtils. A name the user already binds, via
// spark.kubernetes.executor.secretKeyRef, spark.executorEnv or the pod template,
// is skipped and the user's value wins: buildEnvVars does not deduplicate and
// Kubernetes resolves a repeated name last-wins.
val userBoundEnvNames = kubernetesConf.secretEnvNamesToKeyRefs.keySet ++
kubernetesConf.environment.keySet ++
Option(pod.container).flatMap(c => Option(c.getEnv))
.map(_.asScala.map(_.getName).toSet).getOrElse(Set.empty)
val sslRpcPasswords = secMgr.getEnvironmentForSslRpcPasswords.filterNot {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One behavioral edge, no code change requested: the _SPARK_SSL_RPC_* env fallback lives in the shared SSLOptions.parse, so every spark.ssl.* namespace reads it, ahead of the defaults fallback. Global spark.ssl.* passwords are withheld from executors too, so once rpc is enabled another enabled namespace resolves the rpc password on executors while the driver uses its own, and the two sides disagree. Standalone injects the same env vars and behaves identically, and before this PR executors had no password on that path at all, so this breaks no working setup. Worth one sentence in the PR description or the docs.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a paragraph to the PR description

case (name, _) => userBoundEnvNames.contains(name)
}.toSeq

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs/security.md already says that on Kubernetes the auth secret is injected as an env var, so anyone who can list pods can read it. This PR puts keystore / truststore / key passwords on that same channel. That’s the intended standalone design, but it is a user-facing security change on K8s and should be called out next to the existing auth-secret paragraph. Users who don’t want literals can keep using spark.kubernetes.executor.secretKeyRef.SPARK_SSL_RPC*.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for your help! I'll take a look at the docs.

@sweb sweb Sep 4, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@uros-b I added it here: 2285333

I placed it in Network Encryption -> SSL Encryption but I am happy to move it to the explicit Kubernetes section within authentication.

val userOpts = kubernetesConf.get(EXECUTOR_JAVA_OPTIONS).toSeq.flatMap { opts =>
val subsOpts = Utils.substituteAppNExecIds(opts, kubernetesConf.appId,
kubernetesConf.executorId)
Expand Down Expand Up @@ -187,6 +201,7 @@ private[spark] class BasicExecutorFeatureStep(
++ attributes
++ kubernetesConf.environment
++ sparkAuthSecret
++ sslRpcPasswords

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fix only wires Kubernetes. On YARN, ExecutorRunnable.prepareCommand filters executor java opts with the same SparkConf.isExecutorStartupConf (passwords withheld there too), and prepareEnvironment only forwards env vars starting with SPARK plus spark.executorEnv; it never calls getEnvironmentForSslRpcPasswords. Under --master yarn the same configuration fails executors with exactly the symptom this PR describes.

ExecutorRunnable already receives securityMgr as a constructor param, so appending the same map at the end of prepareEnvironment, mirroring CommandUtils on standalone, is a one-liner. The HashMap overwrite semantics deduplicate naturally; note the driver-side value then overrides a same-name spark.executorEnv entry — the standalone behavior, opposite of the k8s-side user-value precedence. If YARN is out of scope, please say so in the description and open a follow-up JIRA.

@sweb sweb Sep 5, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's okay with you, I'd keep YARN out of this PR? I am happy to create a follow up JIRA / PR it that works for you. I added a scope note to the PR description and created this ticket: https://issues.apache.org/jira/browse/SPARK-59408

++ Seq(ENV_CLASSPATH -> kubernetesConf.get(EXECUTOR_CLASS_PATH).orNull)
++ allOpts) ++
KubernetesUtils.buildEnvVarsWithFieldRef(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import com.google.common.net.InternetDomainName
import io.fabric8.kubernetes.api.model._
import org.scalatest.BeforeAndAfter

import org.apache.spark.{SecurityManager, SparkConf, SparkException, SparkFunSuite, SparkIllegalArgumentException}
import org.apache.spark.{SecurityManager, SparkConf, SparkException, SparkFunSuite, SparkIllegalArgumentException, SSLOptions}
import org.apache.spark.deploy.k8s.{KubernetesExecutorConf, KubernetesTestConf, SecretVolumeUtils, SparkPod}
import org.apache.spark.deploy.k8s.Config._
import org.apache.spark.deploy.k8s.Constants._
Expand Down Expand Up @@ -417,6 +417,118 @@ class BasicExecutorFeatureStepSuite extends SparkFunSuite with BeforeAndAfter {
}
}

test("SSL RPC password propagation") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The three new tests all set spark.ssl.rpc.* passwords explicitly. The shape where a user sets spark.ssl.enabled=true with the global spark.ssl.keyStorePassword, sets no rpc-side password, and separately enables spark.ssl.rpc.enabled=true also works: rpcSSLOptions inherits the global password through the defaults fallback on the driver, and the env carries the inherited value (spark.ssl.enabled must be on, or nothing is inherited). That is one of the common usages this PR claims to fix, and right now no test guards the inheritance path — could you add one for this shape?

@sweb sweb Sep 5, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 2285333 in test SSL RPC passwords inherited from the global spark.ssl.* namespace propagate

val conf = baseConf.clone()
.set("spark.ssl.rpc.enabled", "true")
.set("spark.ssl.rpc.keyStorePassword", "keyStorePass")
.set("spark.ssl.rpc.keyPassword", "keyPass")
.set("spark.ssl.rpc.privateKeyPassword", "privateKeyPass")
.set("spark.ssl.rpc.trustStorePassword", "trustStorePass")

val step = new BasicExecutorFeatureStep(KubernetesTestConf.createExecutorConf(sparkConf = conf),
new SecurityManager(conf), defaultProfile)

val executor = step.configurePod(SparkPod.initialPod())
checkEnv(executor, conf, Map(
SSLOptions.ENV_RPC_SSL_KEY_STORE_PASSWORD -> "keyStorePass",
SSLOptions.ENV_RPC_SSL_KEY_PASSWORD -> "keyPass",
SSLOptions.ENV_RPC_SSL_PRIVATE_KEY_PASSWORD -> "privateKeyPass",
SSLOptions.ENV_RPC_SSL_TRUST_STORE_PASSWORD -> "trustStorePass"))
}

test("SSL RPC passwords shouldn't propagate if RPC SSL is disabled") {
val conf = baseConf.clone()
.set("spark.ssl.rpc.enabled", "false")
.set("spark.ssl.rpc.keyStorePassword", "keyStorePass")
.set("spark.ssl.rpc.trustStorePassword", "trustStorePass")

val step = new BasicExecutorFeatureStep(KubernetesTestConf.createExecutorConf(sparkConf = conf),
new SecurityManager(conf), defaultProfile)

val executor = step.configurePod(SparkPod.initialPod())
SSLOptions.SPARK_RPC_SSL_PASSWORD_ENVS.foreach { env =>
assert(!KubernetesFeaturesTestUtils.containerHasEnvVar(executor.container, env))
}
}

test("SSL RPC passwords shouldn't override an explicit secretKeyRef") {
val conf = baseConf.clone()
.set("spark.ssl.rpc.enabled", "true")
.set("spark.ssl.rpc.keyStorePassword", "keyStorePass")
.set("spark.ssl.rpc.trustStorePassword", "trustStorePass")

val step = new BasicExecutorFeatureStep(
KubernetesTestConf.createExecutorConf(
sparkConf = conf,
secretEnvNamesToKeyRefs = Map(
SSLOptions.ENV_RPC_SSL_KEY_STORE_PASSWORD -> "rpc-secret:keystore-password")),
new SecurityManager(conf), defaultProfile)

val executor = step.configurePod(SparkPod.initialPod())
checkEnv(executor, conf, Map(
SSLOptions.ENV_RPC_SSL_TRUST_STORE_PASSWORD -> "trustStorePass"))
}

test("SSL RPC passwords shouldn't override an explicit spark.executorEnv entry") {
val conf = baseConf.clone()
.set("spark.ssl.rpc.enabled", "true")
.set("spark.ssl.rpc.keyStorePassword", "keyStorePass")
.set("spark.ssl.rpc.trustStorePassword", "trustStorePass")
.set(s"spark.executorEnv.${SSLOptions.ENV_RPC_SSL_KEY_STORE_PASSWORD}", "userKeyStorePass")

val step = new BasicExecutorFeatureStep(KubernetesTestConf.createExecutorConf(sparkConf = conf),
new SecurityManager(conf), defaultProfile)

val executor = step.configurePod(SparkPod.initialPod())
checkEnv(executor, conf, Map(
SSLOptions.ENV_RPC_SSL_KEY_STORE_PASSWORD -> "userKeyStorePass",
SSLOptions.ENV_RPC_SSL_TRUST_STORE_PASSWORD -> "trustStorePass"))
}

test("SSL RPC passwords shouldn't override an env var predefined on the pod template") {
val conf = baseConf.clone()
.set("spark.ssl.rpc.enabled", "true")
.set("spark.ssl.rpc.keyStorePassword", "keyStorePass")
.set("spark.ssl.rpc.trustStorePassword", "trustStorePass")

val step = new BasicExecutorFeatureStep(KubernetesTestConf.createExecutorConf(sparkConf = conf),
new SecurityManager(conf), defaultProfile)

val templatePod = SparkPod.initialPod()
val templateContainer = new ContainerBuilder(templatePod.container)
.addNewEnv()
.withName(SSLOptions.ENV_RPC_SSL_KEY_STORE_PASSWORD)
.withNewValueFrom()
.withNewSecretKeyRef()
.withKey("keystore-password")
.withName("rpc-secret")
.endSecretKeyRef()
.endValueFrom()
.endEnv()
.build()

val executor = step.configurePod(SparkPod(templatePod.pod, templateContainer))
checkEnv(executor, conf, Map(
SSLOptions.ENV_RPC_SSL_KEY_STORE_PASSWORD -> null,
SSLOptions.ENV_RPC_SSL_TRUST_STORE_PASSWORD -> "trustStorePass"))
}

test("SSL RPC passwords inherited from the global spark.ssl.* namespace propagate") {
val conf = baseConf.clone()
.set("spark.ssl.enabled", "true")
.set("spark.ssl.keyStorePassword", "globalKeyStorePass")
.set("spark.ssl.trustStorePassword", "globalTrustStorePass")
.set("spark.ssl.rpc.enabled", "true")

val step = new BasicExecutorFeatureStep(KubernetesTestConf.createExecutorConf(sparkConf = conf),
new SecurityManager(conf), defaultProfile)

val executor = step.configurePod(SparkPod.initialPod())
checkEnv(executor, conf, Map(
SSLOptions.ENV_RPC_SSL_KEY_STORE_PASSWORD -> "globalKeyStorePass",
SSLOptions.ENV_RPC_SSL_TRUST_STORE_PASSWORD -> "globalTrustStorePass"))
}

test("SPARK-32661 test executor offheap memory") {
baseConf.set(MEMORY_OFFHEAP_ENABLED, true)
baseConf.set("spark.memory.offHeap.size", "42m")
Expand Down Expand Up @@ -776,6 +888,10 @@ class BasicExecutorFeatureStepSuite extends SparkFunSuite with BeforeAndAfter {
s"$ENV_JAVA_OPT_PREFIX${ind + extraJavaOptsStart}" -> opt
}.toMap

val duplicateEnvNames = executorPod.container.getEnv.asScala
.groupBy(_.getName).filter(_._2.size > 1).keys
assert(duplicateEnvNames.isEmpty, s"duplicate env names: ${duplicateEnvNames.mkString(", ")}")

val containerEnvs = executorPod.container.getEnv.asScala.map {
x => (x.getName, x.getValue)
}.toMap
Expand Down