1use crate::constants::{
36 ALLOWED_USER_CONFIGMAP_PREFIX, ALLOWED_USER_MOUNT_PREFIXES, ALLOWED_USER_PVC_PREFIX,
37 ALLOWED_USER_SECRET_PREFIX,
38};
39use k8s_openapi::api::core::v1::{Volume, VolumeMount};
40use thiserror::Error;
41
42#[derive(Debug, Error, PartialEq, Eq)]
48pub enum VolumeRejection {
49 #[error(
50 "volume {name:?} uses forbidden source kind {kind}: only emptyDir, configMap, secret \
51 (with name prefix {ALLOWED_USER_SECRET_PREFIX:?}), or persistentVolumeClaim (with name \
52 prefix {ALLOWED_USER_PVC_PREFIX:?}) are permitted"
53 )]
54 ForbiddenSource { name: String, kind: &'static str },
55
56 #[error(
57 "volume {name:?} secret reference {secret:?} does not start with the required prefix \
58 {ALLOWED_USER_SECRET_PREFIX:?}"
59 )]
60 SecretNamePrefix { name: String, secret: String },
61
62 #[error(
63 "volume {name:?} configMap reference {config_map:?} does not start with the required \
64 prefix {ALLOWED_USER_CONFIGMAP_PREFIX:?}"
65 )]
66 ConfigMapNamePrefix { name: String, config_map: String },
67
68 #[error(
69 "volume {name:?} persistentVolumeClaim reference {pvc:?} does not start with the \
70 required prefix {ALLOWED_USER_PVC_PREFIX:?}"
71 )]
72 PvcNamePrefix { name: String, pvc: String },
73
74 #[error(
75 "volumeMount mountPath {path:?} is outside the allowed prefixes \
76 {ALLOWED_USER_MOUNT_PREFIXES:?}"
77 )]
78 MountPathOutsideAllowList { path: String },
79
80 #[error("volumeMount {field} {value:?} contains '..' (path traversal not permitted)")]
81 SubPathTraversal { field: &'static str, value: String },
82}
83
84pub fn validate_user_volumes(vols: &[Volume]) -> Result<(), VolumeRejection> {
91 for v in vols {
92 validate_one_volume(v)?;
93 }
94 Ok(())
95}
96
97pub fn validate_optional_user_volumes(vols: Option<&Vec<Volume>>) -> Result<(), VolumeRejection> {
104 match vols {
105 Some(vs) => validate_user_volumes(vs),
106 None => Ok(()),
107 }
108}
109
110pub fn validate_user_volume_mounts(mounts: &[VolumeMount]) -> Result<(), VolumeRejection> {
117 for m in mounts {
118 validate_one_volume_mount(m)?;
119 }
120 Ok(())
121}
122
123pub fn validate_optional_user_volume_mounts(
130 mounts: Option<&Vec<VolumeMount>>,
131) -> Result<(), VolumeRejection> {
132 match mounts {
133 Some(ms) => validate_user_volume_mounts(ms),
134 None => Ok(()),
135 }
136}
137
138fn validate_one_volume(v: &Volume) -> Result<(), VolumeRejection> {
143 let name = v.name.clone();
144
145 if v.host_path.is_some() {
149 return forbid(name, "hostPath");
150 }
151 if v.csi.is_some() {
152 return forbid(name, "csi");
153 }
154 if v.flex_volume.is_some() {
155 return forbid(name, "flexVolume");
156 }
157 if v.nfs.is_some() {
158 return forbid(name, "nfs");
159 }
160 if v.iscsi.is_some() {
161 return forbid(name, "iscsi");
162 }
163 if v.rbd.is_some() {
164 return forbid(name, "rbd");
165 }
166 if v.cephfs.is_some() {
167 return forbid(name, "cephfs");
168 }
169 if v.glusterfs.is_some() {
170 return forbid(name, "glusterfs");
171 }
172 if v.azure_file.is_some() {
173 return forbid(name, "azureFile");
174 }
175 if v.azure_disk.is_some() {
176 return forbid(name, "azureDisk");
177 }
178 if v.gce_persistent_disk.is_some() {
179 return forbid(name, "gcePersistentDisk");
180 }
181 if v.aws_elastic_block_store.is_some() {
182 return forbid(name, "awsElasticBlockStore");
183 }
184 if v.cinder.is_some() {
185 return forbid(name, "cinder");
186 }
187 if v.fc.is_some() {
188 return forbid(name, "fc");
189 }
190 if v.flocker.is_some() {
191 return forbid(name, "flocker");
192 }
193 if v.photon_persistent_disk.is_some() {
194 return forbid(name, "photonPersistentDisk");
195 }
196 if v.portworx_volume.is_some() {
197 return forbid(name, "portworxVolume");
198 }
199 if v.quobyte.is_some() {
200 return forbid(name, "quobyte");
201 }
202 if v.scale_io.is_some() {
203 return forbid(name, "scaleIO");
204 }
205 if v.storageos.is_some() {
206 return forbid(name, "storageos");
207 }
208 if v.vsphere_volume.is_some() {
209 return forbid(name, "vsphereVolume");
210 }
211 if v.projected.is_some() {
212 return forbid(name, "projected");
213 }
214 if v.ephemeral.is_some() {
215 return forbid(name, "ephemeral");
216 }
217 if v.git_repo.is_some() {
218 return forbid(name, "gitRepo");
219 }
220 if v.downward_api.is_some() {
221 return forbid(name, "downwardAPI");
222 }
223
224 if v.empty_dir.is_some() {
226 return Ok(());
227 }
228 if let Some(ref s) = v.secret {
229 let secret = s.secret_name.clone().unwrap_or_default();
230 if secret.starts_with(ALLOWED_USER_SECRET_PREFIX) && !secret.is_empty() {
231 return Ok(());
232 }
233 return Err(VolumeRejection::SecretNamePrefix { name, secret });
234 }
235 if let Some(ref cm) = v.config_map {
236 let config_map = cm.name.clone();
237 if config_map.starts_with(ALLOWED_USER_CONFIGMAP_PREFIX) {
238 return Ok(());
239 }
240 return Err(VolumeRejection::ConfigMapNamePrefix { name, config_map });
241 }
242 if let Some(ref pvc) = v.persistent_volume_claim {
243 let pvc_name = pvc.claim_name.clone();
244 if pvc_name.starts_with(ALLOWED_USER_PVC_PREFIX) {
245 return Ok(());
246 }
247 return Err(VolumeRejection::PvcNamePrefix {
248 name,
249 pvc: pvc_name,
250 });
251 }
252
253 forbid(name, "unknown/none")
256}
257
258fn forbid(name: String, kind: &'static str) -> Result<(), VolumeRejection> {
259 Err(VolumeRejection::ForbiddenSource { name, kind })
260}
261
262fn validate_one_volume_mount(m: &VolumeMount) -> Result<(), VolumeRejection> {
263 if !ALLOWED_USER_MOUNT_PREFIXES
264 .iter()
265 .any(|p| m.mount_path.starts_with(p))
266 {
267 return Err(VolumeRejection::MountPathOutsideAllowList {
268 path: m.mount_path.clone(),
269 });
270 }
271
272 if let Some(ref sub) = m.sub_path {
273 if sub.contains("..") {
274 return Err(VolumeRejection::SubPathTraversal {
275 field: "subPath",
276 value: sub.clone(),
277 });
278 }
279 }
280 if let Some(ref sub_expr) = m.sub_path_expr {
281 if sub_expr.contains("..") {
282 return Err(VolumeRejection::SubPathTraversal {
283 field: "subPathExpr",
284 value: sub_expr.clone(),
285 });
286 }
287 }
288 Ok(())
289}
290
291#[cfg(test)]
292#[path = "safe_volume_tests.rs"]
293mod safe_volume_tests;