LuciferYang commented on code in PR #58010:
URL: https://github.com/apache/spark/pull/58010#discussion_r4173516236
##########
resource-managers/kubernetes/core/src/main/scala/org/apache/spark/deploy/k8s/features/BasicExecutorFeatureStep.scala:
##########
@@ -142,6 +142,21 @@ private[spark] class BasicExecutorFeatureStep(
case _ => Nil
}.getOrElse(Nil)
+ // SparkConf.isExecutorStartupConf withholds the spark.ssl.* passwords
from the
+ // executor conf. The pod allocator resolves them from the driver
SecurityManager
+ // and passes them in 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))
Review Comment:
The skip-list reads only the template container's `env`, not its `envFrom`.
A user who binds `_SPARK_SSL_RPC_*` through `envFrom: secretRef` on the
executor template gets a same-name literal added to `env` on every executor pod
after this change. Kubernetes gives `env` precedence over `envFrom` for a
duplicate key, so the literal also replaces the Secret's value. That falls
under the pod-template workaround the description says is unaffected, and
neither "bindings still win" nor "no literal password is added beside them"
holds for it.
The keys behind an `envFrom` are unknown without reading the Secret, so the
safe option is to skip the injection whenever the container has any `envFrom`,
with a one-time WARN that lists the variable names it would have injected. The
cost is that a user with an unrelated `envFrom` does not get this fix and still
binds the names via `secretKeyRef` as today, but their binding is never
replaced by a literal. Please also add an `envFrom` test and a sentence in the
docs.
##########
resource-managers/kubernetes/core/src/main/scala/org/apache/spark/deploy/k8s/KubernetesConf.scala:
##########
@@ -216,7 +216,8 @@ class KubernetesExecutorConf(
val executorId: String,
val driverPod: Option[Pod],
val resourceProfileId: Int = DEFAULT_RESOURCE_PROFILE_ID,
- customAuthSecret: Option[String] = None)
+ customAuthSecret: Option[String] = None,
+ val sslRpcPasswordEnvs: Map[String, String] = Map.empty)
Review Comment:
The primary constructor gained `sslRpcPasswordEnvs`, but the Java-friendly
constructor from SPARK-59500 still forwards `Map.empty`. A Java caller either
gets no SSL passwords, so its executors fail exactly as before this fix, or has
to build `scala.Option` and `scala.collection.immutable.Map` values to call the
primary constructor, which is what that constructor was added to avoid.
If this lands in 4.4.0, neither constructor has shipped yet, so adding a
`java.util.Map[String, String]` parameter to the Java one (null meaning empty)
is cheap now. One more assertion in the existing Java-constructor test in
`KubernetesConfSuite` would cover it.
##########
resource-managers/kubernetes/core/src/main/scala/org/apache/spark/scheduler/cluster/k8s/ExecutorPodsAllocator.scala:
##########
@@ -536,7 +536,8 @@ class ExecutorPodsAllocator(
applicationId,
driverPod,
resourceProfileId,
- Option(secMgr.getSecretKey()))
+ Option(secMgr.getSecretKey()),
+ secMgr.getEnvironmentForSslRpcPasswords)
Review Comment:
The injected values are whatever the driver's `rpcSSLOptions` resolved,
which may come from the driver's Hadoop credential provider. If executors read
the same provider through a core-site.xml under `HADOOP_CONF_DIR`, they already
get the passwords at startup today, with nothing in the pod spec. After this
change the driver still writes those values as literals into every executor
pod; the executor never uses them, but they are exposed. The skip-list cannot
see this case, and unlike `spark.authenticate.secret.executor.file` for the
auth secret there is no switch to turn it off.
What the executor is missing at startup is only the SparkConf passwords that
`isExecutorStartupConf` withholds; values from a credential provider or the
driver's environment already have to be provided on the executor side today.
Could the injection apply only when the value that finally takes effect comes
from SparkConf (the rpc or global `spark.ssl` namespace), and leave the rest
for the user to bind as today?
##########
resource-managers/kubernetes/core/src/test/scala/org/apache/spark/deploy/k8s/features/BasicExecutorFeatureStepSuite.scala:
##########
@@ -421,6 +421,140 @@ class BasicExecutorFeatureStepSuite extends SparkFunSuite
with BeforeAndAfter {
checkEnv(executor, conf, Map(SecurityManager.ENV_AUTH_SECRET ->
"my-auth-secret"))
}
+ test("SSL RPC password propagation") {
+ 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 secMgr = new SecurityManager(conf)
+ val step = new BasicExecutorFeatureStep(
+ KubernetesTestConf.createExecutorConf(
+ sparkConf = conf,
+ sslRpcPasswordEnvs = secMgr.getEnvironmentForSslRpcPasswords),
+ 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") {
Review Comment:
This test asserts that no `_SPARK_SSL_RPC_*` reaches the pod when RPC SSL is
off, but in that case `getEnvironmentForSslRpcPasswords` already returns an
empty map, so it still passes with the Kubernetes change fully reverted. What
it actually covers is the SSL option parsing in core.
It could move to `SecurityManagerSuite` and assert the empty map directly;
if it stays here, a name that says what it covers would help.
##########
resource-managers/kubernetes/core/src/main/scala/org/apache/spark/deploy/k8s/features/BasicExecutorFeatureStep.scala:
##########
@@ -142,6 +142,21 @@ private[spark] class BasicExecutorFeatureStep(
case _ => Nil
}.getOrElse(Nil)
+ // SparkConf.isExecutorStartupConf withholds the spark.ssl.* passwords
from the
+ // executor conf. The pod allocator resolves them from the driver
SecurityManager
+ // and passes them in 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 ++
Review Comment:
The comment says the passwords travel "as the standalone worker does in
CommandUtils", then that a user-bound name is skipped so the user's value wins.
`CommandUtils` does the opposite: it appends the passwords with `++=` after
`spark.executorEnv`, so the worker-side value overrides the user's, and the
passwords come from the worker rather than the driver. The stated reason
(`buildEnvVars` does not dedup, Kubernetes takes the last duplicate) holds for
`spark.executorEnv` and template env, but not for `secretKeyRef`: that binding
is appended later by `EnvSecretsFeatureStep` and would win even without the
skip.
SPARK-59408 will likely copy this when wiring YARN, so it would help to
state that the precedence differs from standalone, and that for a
`secretKeyRef` binding the skip is what keeps a plaintext copy out of the pod
spec.
##########
resource-managers/kubernetes/core/src/main/scala/org/apache/spark/deploy/k8s/KubernetesConf.scala:
##########
@@ -356,9 +357,11 @@ private[spark] object KubernetesConf {
appId: String,
driverPod: Option[Pod],
resourceProfileId: Int = DEFAULT_RESOURCE_PROFILE_ID,
- authSecret: Option[String] = None): KubernetesExecutorConf = {
+ authSecret: Option[String] = None,
+ sslRpcPasswordEnvs: Map[String, String] = Map.empty):
KubernetesExecutorConf = {
Review Comment:
The fix only takes effect through the three allocators passing
`secMgr.getEnvironmentForSslRpcPasswords`, but the new `createExecutorConf`
parameter defaults to `Map.empty`, so dropping the argument at any call site
still compiles and every test still passes: the allocator suites stub
`buildFromFeatures` with `any(classOf[KubernetesExecutorConf])`, and the new
tests hand-build the map at the feature-step level.
The cheapest guard is to remove the default on `createExecutorConf`, so the
compiler pins all three call sites. For a test-level check, capture the conf
the way `DeploymentAllocatorSuite` already does with
`invocation.getArgument[KubernetesExecutorConf](0)` and assert on it;
`authSecret` can be covered in the same place.
##########
docs/security.md:
##########
@@ -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>`.
Review Comment:
This paragraph tells users to supply the passwords through `secretKeyRef`,
but only names `_SPARK_SSL_RPC_KEY_STORE_PASSWORD`. The other three
(`_SPARK_SSL_RPC_KEY_PASSWORD`, `_SPARK_SSL_RPC_PRIVATE_KEY_PASSWORD`,
`_SPARK_SSL_RPC_TRUST_STORE_PASSWORD`) appear nowhere in the docs, so a user
following the example still has the key and trust store passwords in plaintext.
Please list all four names, and add a line for the driver: in cluster mode,
passwords set in SparkConf are written in plaintext to the driver's ConfigMap,
so the driver should bind the same variables through
`spark.kubernetes.driver.secretKeyRef` rather than SparkConf.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]