fix(iscsi): block device allowlist confinement#432
fix(iscsi): block device allowlist confinement#432ambient-code[bot] wants to merge 4 commits intomainfrom
Conversation
…rbitrary device exposure Block device paths passed via add_lun(is_block=True) were accepted without confinement. Add a block_device_allowlist config param that must be explicitly populated; resolved paths are checked against it. Also add @validate_call to all @export methods for input type validation. Closes #352 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
✅ Deploy Preview for jumpstarter-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
The iSCSI driver depends on rtslib_fb which requires libudev, a Linux-only library. Add a module-level pytest.skip to prevent import errors on macOS CI runners. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CI Fix: macOS test failuresRoot cause: The new Fix applied (commit if sys.platform != "linux":
pytest.skip("iSCSI driver requires Linux (libudev)", allow_module_level=True)This ensures the test module is cleanly skipped on non-Linux platforms rather than failing with an import error. Other CI observations:
|
| host: str = field(default="") | ||
| port: int = 3260 | ||
| remove_created_on_close: bool = False # Keep disk images persistent by default | ||
| block_device_allowlist: List[str] = field(default_factory=list) |
There was a problem hiding this comment.
I guess this parameter would need to be documented in the README.md of the driver.
There was a problem hiding this comment.
Good catch! I've added block_device_allowlist to both the config parameters table and the YAML example in the driver README (commit 9232932).
Add the new block_device_allowlist config parameter to the README config table and YAML example, as requested in PR review. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| @@ -0,0 +1,92 @@ | |||
| """Tests for iSCSI driver block device allowlist and path confinement.""" | |||
| resolved_path = os.path.realpath(file_path) | ||
| if not self.block_device_allowlist: | ||
| raise ISCSIError( | ||
| "block_device_allowlist is empty; configure allowed block device paths " | ||
| "to use is_block=True" | ||
| ) | ||
| if resolved_path not in self.block_device_allowlist: |
There was a problem hiding this comment.
Allowlist entries are not resolved/canonicalized
The user-supplied file_path is resolved via os.path.realpath(), but the entries in block_device_allowlist are compared as-is. This means if an operator configures a symlink path in the allowlist (e.g., /dev/disk/by-id/my-disk), requests for that same path will resolve to the real device (e.g., /dev/sda) and fail the check.
The behavior is secure (fails-closed), but it will be confusing for operators who naturally use stable device naming paths like /dev/disk/by-id/ or /dev/disk/by-path/.
Consider either:
- Resolving allowlist entries during
__post_init__withos.path.realpath(), or - Updating the README to document that only canonical (non-symlink) paths should be used and removing the
/dev/disk/by-id/my-diskexample
AI-generated, human reviewed
There was a problem hiding this comment.
Good catch. I will resolve allowlist entries via os.path.realpath() in __post_init__ so operators can use stable symlink paths like /dev/disk/by-id/.... This also makes the README example correct as-is.
| @@ -42,6 +42,9 @@ export: | |||
| root_dir: "/var/lib/iscsi" | |||
| target_name: "demo" | |||
| remove_created_on_close: false # Keep disk images persistent (default) | |||
There was a problem hiding this comment.
This example shows /dev/disk/by-id/my-disk as an allowlist entry, but symlink paths like this won't work as expected. The driver resolves user-supplied paths via os.path.realpath() before checking against the allowlist, but allowlist entries themselves are not resolved. So a request for /dev/disk/by-id/my-disk would resolve to something like /dev/sda and then fail the allowlist check.
If the driver is updated to resolve allowlist entries, this example is fine. Otherwise, this should probably show a canonical device path instead.
AI-generated, human reviewed
| ) | ||
| if resolved_path not in self.block_device_allowlist: | ||
| raise ISCSIError( | ||
| f"Block device path '{resolved_path}' is not in the configured allowlist" |
There was a problem hiding this comment.
Nit: including the resolved path in the error message could reveal filesystem layout details (e.g., real device names behind symlinks) to callers in a multi-tenant context. Consider whether this level of detail is appropriate, or if a more generic message would be better.
AI-generated, human reviewed
There was a problem hiding this comment.
This error is raised server-side on the exporter host. The operator configuring block_device_allowlist is the same trusted admin running the exporter -- they already know the device topology. The resolved path in the error message helps them debug allowlist mismatches (e.g. figuring out what a symlink resolved to). In the Jumpstarter architecture, untrusted client callers receive a generic gRPC error, not the raw Python exception message.
| host: str = field(default="") | ||
| port: int = 3260 | ||
| remove_created_on_close: bool = False # Keep disk images persistent by default | ||
| block_device_allowlist: List[str] = field(default_factory=list) |
There was a problem hiding this comment.
Minor: there is no validation that allowlist entries are absolute paths. A relative path here would silently never match anything (since os.path.realpath always returns absolute paths), so it fails-closed, but it would be a silent configuration error. A simple check in __post_init__ could catch this early and save operators debugging time.
AI-generated, human reviewed
There was a problem hiding this comment.
Agreed. Will add validation in __post_init__ to reject non-absolute paths in the allowlist with a clear ConfigurationError.
| @@ -186,6 +188,7 @@ def _cleanup_orphan_storage_objects(self): | |||
| self.logger.debug(f"No orphan storage object cleanup performed: {e}") | |||
|
|
|||
| @export | |||
There was a problem hiding this comment.
Nit: @validate_call on methods like clear_all_luns(self), start(self), stop(self), get_host(self), get_port(self), get_target_iqn(self), and list_luns(self) adds pydantic validation overhead but these methods take no user-supplied parameters. Consider whether the decorator is needed on the 7 zero-parameter methods, or if it should only be applied to add_lun, remove_lun, and decompress which actually accept user input.
AI-generated, human reviewed
There was a problem hiding this comment.
This follows the established project pattern -- the opendal driver applies @validate_call to every @export method uniformly. Keeping consistency across drivers is more maintainable than selectively applying it. The overhead on zero-param methods is negligible (no pydantic model is constructed when there are no parameters to validate).
| from typing import Any, Dict, List, Optional | ||
|
|
||
| from jumpstarter_driver_opendal.driver import Opendal | ||
| from pydantic import validate_call |
There was a problem hiding this comment.
Minor: pydantic is imported here but is not listed as a direct dependency in pyproject.toml -- it is only available transitively via jumpstarter. If the core package ever changes its pydantic dependency, this driver would break. Consider adding pydantic as an explicit dependency.
AI-generated, human reviewed
There was a problem hiding this comment.
This is consistent with other drivers in the repo (e.g. jumpstarter-driver-opendal, jumpstarter-driver-can, jumpstarter-driver-qemu all import pydantic transitively via jumpstarter). Pydantic is a core dependency of jumpstarter itself (used in the driver base class). Adding it as an explicit dep to every driver would be a repo-wide policy change -- better handled in a separate issue/PR if desired.
| @@ -1,5 +1,5 @@ | |||
| version = 1 | |||
| revision = 3 | |||
| revision = 2 | |||
There was a problem hiding this comment.
This lockfile diff includes unrelated changes -- jumpstarter-driver-noyito-relay, hid, pytest-mock, and a revision number change (3 to 2). Could these be split out into a separate commit to keep the security fix self-contained?
AI-generated, human reviewed
There was a problem hiding this comment.
The lockfile drift is from the branch being behind main. Will rebase onto main to get a clean lockfile diff.
| @@ -0,0 +1,92 @@ | |||
| """Tests for iSCSI driver block device allowlist and path confinement.""" | |||
There was a problem hiding this comment.
Heads up: these tests won't be discovered by pytest when running via make pkg-test-jumpstarter-driver-iscsi. The pyproject.toml for this package sets testpaths = ["src"], but there is no src/ directory -- the test file lives under jumpstarter_driver_iscsi/. This means pytest will look in a nonexistent directory and find zero tests.
This is a pre-existing configuration issue (not introduced by this PR), but since this PR adds security-critical tests, it would be worth fixing testpaths to point to jumpstarter_driver_iscsi (or removing the directive entirely) so these tests actually run in CI.
AI-generated, human reviewed
There was a problem hiding this comment.
Good find. The testpaths = ["src"] in pyproject.toml is indeed incorrect for this package layout. Will fix it to testpaths = ["jumpstarter_driver_iscsi"] in this PR since the new tests depend on it.
…stpaths - Resolve block_device_allowlist entries via os.path.realpath() in __post_init__ so operators can use stable symlink paths like /dev/disk/by-id/ - Validate that all allowlist entries are absolute paths at init time, raising ConfigurationError for relative paths - Fix pyproject.toml testpaths from ["src"] to ["jumpstarter_driver_iscsi"] so tests are discoverable by pytest - Add tests for allowlist validation and symlink resolution at init Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Review response summaryAddressed review feedback from @raballew (commit bda4152): Changes made
Pushed back on
Still TODO
|
Summary
block_device_allowlistconfiguration parameter toISCSIdriver that must be explicitly populated to allow block device exposureos.path.realpath()to resolve symlinks before checking@validate_calldecorators to all 10@exportmethods for pydantic input type validationCloses #352
Test plan
make lint-fix)🤖 Generated with Claude Code