1use crate::constants::{
37 ALLOWED_USER_CONFIGMAP_PREFIX, ALLOWED_USER_MOUNT_PREFIXES, ALLOWED_USER_PVC_PREFIX,
38 ALLOWED_USER_SECRET_PREFIX,
39};
40use k8s_openapi::api::core::v1::{Volume, VolumeMount};
41use thiserror::Error;
42
43#[derive(Debug, Error, PartialEq, Eq)]
49pub enum VolumeRejection {
50 #[error(
51 "volume {name:?} uses forbidden source kind {kind}: only emptyDir, configMap, secret \
52 (with name prefix {ALLOWED_USER_SECRET_PREFIX:?}), or persistentVolumeClaim (with name \
53 prefix {ALLOWED_USER_PVC_PREFIX:?}) are permitted"
54 )]
55 ForbiddenSource { name: String, kind: &'static str },
56
57 #[error(
58 "volume {name:?} secret reference {secret:?} does not start with the required prefix \
59 {ALLOWED_USER_SECRET_PREFIX:?}"
60 )]
61 SecretNamePrefix { name: String, secret: String },
62
63 #[error(
64 "volume {name:?} configMap reference {config_map:?} does not start with the required \
65 prefix {ALLOWED_USER_CONFIGMAP_PREFIX:?}"
66 )]
67 ConfigMapNamePrefix { name: String, config_map: String },
68
69 #[error(
70 "volume {name:?} persistentVolumeClaim reference {pvc:?} does not start with the \
71 required prefix {ALLOWED_USER_PVC_PREFIX:?}"
72 )]
73 PvcNamePrefix { name: String, pvc: String },
74
75 #[error(
76 "volumeMount mountPath {path:?} is outside the allowed prefixes \
77 {ALLOWED_USER_MOUNT_PREFIXES:?}"
78 )]
79 MountPathOutsideAllowList { path: String },
80
81 #[error("volumeMount mountPath {path:?} contains '..' (path traversal not permitted)")]
82 MountPathTraversal { path: String },
83
84 #[error("volumeMount {field} {value:?} contains '..' (path traversal not permitted)")]
85 SubPathTraversal { field: &'static str, value: String },
86
87 #[error(
88 "DNSSEC keysFrom secret reference {secret:?} does not start with the required prefix \
89 {ALLOWED_USER_SECRET_PREFIX:?}"
90 )]
91 DnssecKeySecretPrefix { secret: String },
92}
93
94pub fn validate_dnssec_key_secret_name(name: &str) -> Result<(), VolumeRejection> {
108 if name.starts_with(ALLOWED_USER_SECRET_PREFIX) {
109 return Ok(());
110 }
111 Err(VolumeRejection::DnssecKeySecretPrefix {
112 secret: name.to_string(),
113 })
114}
115
116pub fn validate_user_volumes(vols: &[Volume]) -> Result<(), VolumeRejection> {
123 for v in vols {
124 validate_one_volume(v)?;
125 }
126 Ok(())
127}
128
129pub fn validate_optional_user_volumes(vols: Option<&Vec<Volume>>) -> Result<(), VolumeRejection> {
136 match vols {
137 Some(vs) => validate_user_volumes(vs),
138 None => Ok(()),
139 }
140}
141
142pub fn validate_user_volume_mounts(mounts: &[VolumeMount]) -> Result<(), VolumeRejection> {
149 for m in mounts {
150 validate_one_volume_mount(m)?;
151 }
152 Ok(())
153}
154
155pub fn validate_optional_user_volume_mounts(
162 mounts: Option<&Vec<VolumeMount>>,
163) -> Result<(), VolumeRejection> {
164 match mounts {
165 Some(ms) => validate_user_volume_mounts(ms),
166 None => Ok(()),
167 }
168}
169
170fn validate_one_volume(v: &Volume) -> Result<(), VolumeRejection> {
175 let name = v.name.clone();
176
177 if v.host_path.is_some() {
181 return forbid(name, "hostPath");
182 }
183 if v.csi.is_some() {
184 return forbid(name, "csi");
185 }
186 if v.flex_volume.is_some() {
187 return forbid(name, "flexVolume");
188 }
189 if v.nfs.is_some() {
190 return forbid(name, "nfs");
191 }
192 if v.iscsi.is_some() {
193 return forbid(name, "iscsi");
194 }
195 if v.rbd.is_some() {
196 return forbid(name, "rbd");
197 }
198 if v.cephfs.is_some() {
199 return forbid(name, "cephfs");
200 }
201 if v.glusterfs.is_some() {
202 return forbid(name, "glusterfs");
203 }
204 if v.azure_file.is_some() {
205 return forbid(name, "azureFile");
206 }
207 if v.azure_disk.is_some() {
208 return forbid(name, "azureDisk");
209 }
210 if v.gce_persistent_disk.is_some() {
211 return forbid(name, "gcePersistentDisk");
212 }
213 if v.aws_elastic_block_store.is_some() {
214 return forbid(name, "awsElasticBlockStore");
215 }
216 if v.cinder.is_some() {
217 return forbid(name, "cinder");
218 }
219 if v.fc.is_some() {
220 return forbid(name, "fc");
221 }
222 if v.flocker.is_some() {
223 return forbid(name, "flocker");
224 }
225 if v.photon_persistent_disk.is_some() {
226 return forbid(name, "photonPersistentDisk");
227 }
228 if v.portworx_volume.is_some() {
229 return forbid(name, "portworxVolume");
230 }
231 if v.quobyte.is_some() {
232 return forbid(name, "quobyte");
233 }
234 if v.scale_io.is_some() {
235 return forbid(name, "scaleIO");
236 }
237 if v.storageos.is_some() {
238 return forbid(name, "storageos");
239 }
240 if v.vsphere_volume.is_some() {
241 return forbid(name, "vsphereVolume");
242 }
243 if v.projected.is_some() {
244 return forbid(name, "projected");
245 }
246 if v.ephemeral.is_some() {
247 return forbid(name, "ephemeral");
248 }
249 if v.git_repo.is_some() {
250 return forbid(name, "gitRepo");
251 }
252 if v.downward_api.is_some() {
253 return forbid(name, "downwardAPI");
254 }
255
256 if v.empty_dir.is_some() {
258 return Ok(());
259 }
260 if let Some(ref s) = v.secret {
261 let secret = s.secret_name.clone().unwrap_or_default();
262 if secret.starts_with(ALLOWED_USER_SECRET_PREFIX) && !secret.is_empty() {
263 return Ok(());
264 }
265 return Err(VolumeRejection::SecretNamePrefix { name, secret });
266 }
267 if let Some(ref cm) = v.config_map {
268 let config_map = cm.name.clone();
269 if config_map.starts_with(ALLOWED_USER_CONFIGMAP_PREFIX) {
270 return Ok(());
271 }
272 return Err(VolumeRejection::ConfigMapNamePrefix { name, config_map });
273 }
274 if let Some(ref pvc) = v.persistent_volume_claim {
275 let pvc_name = pvc.claim_name.clone();
276 if pvc_name.starts_with(ALLOWED_USER_PVC_PREFIX) {
277 return Ok(());
278 }
279 return Err(VolumeRejection::PvcNamePrefix {
280 name,
281 pvc: pvc_name,
282 });
283 }
284
285 forbid(name, "unknown/none")
288}
289
290fn forbid(name: String, kind: &'static str) -> Result<(), VolumeRejection> {
291 Err(VolumeRejection::ForbiddenSource { name, kind })
292}
293
294fn validate_one_volume_mount(m: &VolumeMount) -> Result<(), VolumeRejection> {
295 if m.mount_path.contains("..") {
301 return Err(VolumeRejection::MountPathTraversal {
302 path: m.mount_path.clone(),
303 });
304 }
305
306 if !ALLOWED_USER_MOUNT_PREFIXES
307 .iter()
308 .any(|p| m.mount_path.starts_with(p))
309 {
310 return Err(VolumeRejection::MountPathOutsideAllowList {
311 path: m.mount_path.clone(),
312 });
313 }
314
315 if let Some(ref sub) = m.sub_path {
316 if sub.contains("..") {
317 return Err(VolumeRejection::SubPathTraversal {
318 field: "subPath",
319 value: sub.clone(),
320 });
321 }
322 }
323 if let Some(ref sub_expr) = m.sub_path_expr {
324 if sub_expr.contains("..") {
325 return Err(VolumeRejection::SubPathTraversal {
326 field: "subPathExpr",
327 value: sub_expr.clone(),
328 });
329 }
330 }
331 Ok(())
332}
333
334#[cfg(test)]
335#[path = "safe_volume_tests.rs"]
336mod safe_volume_tests;