Skip to content

feat: Add initial deny list for user mounted directories#1436

Open
lbeckman314 wants to merge 7 commits into
developfrom
feat/deny-lists
Open

feat: Add initial deny list for user mounted directories#1436
lbeckman314 wants to merge 7 commits into
developfrom
feat/deny-lists

Conversation

@lbeckman314

@lbeckman314 lbeckman314 commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Overview ⚙️

This PR adds a default "deny list" to user-submitted task volumes to prevent errors related to trying to mount to system directories.

Current Behavior ⚠️

  • Submitted task that mounts an input file to /dev:
➜ helm upgrade --install funnel calypr/funnel --version 0.1.99-rc.37 --values values.yaml

➜ kubectl get deployments -o wide
NAME            IMAGES
funnel-server   quay.io/ohsu-comp-bio/funnel:2026-05-27

➜ kubectl port-forward svc/funnel 8000:8000
➜ cat mount-test.json
{
  "name": "deny-input-dev",
  "description": "Issue #92: Input mounted to /dev (develop → accepted, feat/deny-lists → rejected)",
  "executors": [
    {
      "image": "alpine",
      "command": ["ls", "-la", "/dev"]
    }
  ],
  "inputs": [
    {
      "url": "https://raw.githubusercontent.com/calypr/funnel/refs/heads/develop/README.md",
      "path": "/dev"
    }
  ]
}

➜ funnel task create mount-test.json
d8pfs1s622hs739b7rh0
  • Able to replicate EXECUTOR_ERROR stemming from OCI StartError: 🐛
➜ funnel task get d8pfs1s622hs739b7rh0

{
  ...
  "id":  "d8pfs1s622hs739b7rh0",
  "logs":  [
    {
      "logs":  [
        {
          "exitCode":  128,
        }
      ],
      "systemLogs":  [
         ...
        "level='error' msg='Exec error' error='executor job d8pfs1s622hs739b7rh0-0 failed with exit code 128 (StartError): failed to create containerd task: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: error mounting \"/var/lib/kubelet/pods/64a9fb7a-5822-496c-992d-2c081d929297/containers/funnel-executor-d8pfs1s622hs739b7rh0/4dc52fb5\" to rootfs at \"/dev/termination-log\": open o_path procfd: open /run/containerd/io.containerd.runtime.v2.task/k8s.io/e4a407b35fca352200248175af22997988a90d0765dafbc358bc7fcf0f186d7c/rootfs/dev/termination-log: not a directory: unknown'"
      ]
    }
  ],
  "state":  "EXECUTOR_ERROR"
}

New Behavior ✔️

forbiddenPathPrefixes

  • /dev
  • /proc
  • /sys
  • /run
  • /var/run

Tested against PR #1436 (funnel:6e404ed):

➜ helm upgrade --install funnel calypr/funnel --version 0.1.99-rc.37 --values values.yaml --set image.tag=6e404ed

➜ kubectl get deployments -o wide
NAME            IMAGES
funnel-server   quay.io/ohsu-comp-bio/funnel:6e404ed

➜ kubectl port-forward svc/funnel 8000:8000
  • Getting 400 response code when attempting to submit tasks that mount /dev:

funnel

➜ funnel task create mount-test.json
Error: [STATUS CODE - 400]      {
  "error": "invalid task message:\nTask.Inputs[0].Path: \"/dev\" is not allowed as an input path\n"
} 

curl

➜ curl -X POST http://localhost:8000/v1/tasks --data @mount-test.json -w "\n[STATUS CODE - %{http_code}]\n"
{
  "error": "invalid task message:\nTask.Inputs[0].Path: \"/dev\" is not allowed as an input path\n"
}
[STATUS CODE - 400]

Breaking Changes? ❌

  • This can technically be considered a breaking change since Funnel now rejects tasks that would previously run (and error) due to mounting inputs/outputs to system directories.

Next Steps 🌀

  • Merge to develop
  • Create new image/tag (e.g. 2026-06-23)
  • Release in Helm Chart v0.1.99-rc.39

@github-actions

Copy link
Copy Markdown

Gen3 Integration Tests

filepath passed skipped SUBTOTAL
tests/test_gen3_workflow.py 25 4 29
TOTAL 25 4 29

@lbeckman314 lbeckman314 marked this pull request as ready for review June 16, 2026 23:27
@github-actions

Copy link
Copy Markdown

Gen3 Integration Tests

filepath passed skipped SUBTOTAL
tests/test_gen3_workflow.py 25 4 29
TOTAL 25 4 29

@github-actions

Copy link
Copy Markdown

Gen3 Integration Tests

filepath passed skipped SUBTOTAL
tests/test_gen3_workflow.py 25 4 29
TOTAL 25 4 29

Add Kubernetes.ForbiddenPathPrefixes to the Funnel config so operators can
customize the deny list of container paths that user-submitted tasks may not
mount inputs, outputs, volumes, or working directories into. When set, the
configured list replaces Funnel's built-in defaults (/dev, /proc, /sys, /run,
/var/run); when empty, the defaults still apply.

Validate and InitTask now accept the deny list as a parameter (the tes package
cannot import config without an import cycle), and the server's CreateTask path
passes the configured prefixes. Client- and worker-side pre-flight validation
continue to use the defaults.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Assisted-by: Claude Code:claude [Claude Code]
@github-actions

Copy link
Copy Markdown

Gen3 Integration Tests

filepath passed skipped SUBTOTAL
tests/test_gen3_workflow.py 25 4 29
TOTAL 25 4 29

@nss10 nss10 left a comment

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.

Current test did not work when Forbidden path is added in the configuration

@matthewpeterkort

matthewpeterkort commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

This PR appears to be working locally:

Testing existing Funnel server: http://localhost:8000
PASS: exact /dev denied -> HTTP 400
PASS: nested /dev denied -> HTTP 400
PASS: input /secret allowed -> HTTP 200
PASS: output /secret allowed -> HTTP 200
PASS: volume /secret allowed -> HTTP 200
PASS: workdir /secret allowed -> HTTP 200
PASS: segment boundary /devices allowed -> HTTP 200
PASS: segment boundary /data/dev allowed -> HTTP 200

when using the below testing script and the default funnel server config with the basic backend.

#!/usr/bin/env bash
set -euo pipefail

# Test an already-running Funnel server. Override with FUNNEL_URL if needed.
BASE_URL="${FUNNEL_URL:-http://localhost:8000}"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

echo "Testing existing Funnel server: $BASE_URL"

if ! curl -fsS "$BASE_URL/ga4gh/tes/v1/service-info" >/dev/null 2>&1; then
  echo "Funnel is not reachable at $BASE_URL"
  exit 1
fi

post_task() {
  local label="$1"
  local expected="$2"
  local task="$3"
  local body="$TMP/body"
  local status

  status="$(curl -sS -o "$body" -w '%{http_code}' \
    -X POST "$BASE_URL/ga4gh/tes/v1/tasks" \
    -H 'Content-Type: application/json' \
    -d "$task")"

  if [[ "$status" != "$expected" ]]; then
    echo "FAIL: $label: expected HTTP $expected, got $status"
    cat "$body"
    exit 1
  fi

  echo "PASS: $label -> HTTP $status"
}

# Active configuration is expected to contain /dev but not /secret.
post_task "exact /dev denied" 400 \
  '{"executors":[{"image":"alpine","command":["echo","test"]}],"inputs":[{"url":"file:///src","path":"/dev"}]}'

post_task "nested /dev denied" 400 \
  '{"executors":[{"image":"alpine","command":["echo","test"]}],"inputs":[{"url":"file:///src","path":"/dev/sda"}]}'

post_task "input /secret allowed" 200 \
  '{"executors":[{"image":"alpine","command":["echo","test"]}],"inputs":[{"url":"file:///src","path":"/secret/input"}]}'

post_task "output /secret allowed" 200 \
  '{"executors":[{"image":"alpine","command":["echo","test"]}],"outputs":[{"url":"file:///dst","path":"/secret/output"}]}'

post_task "volume /secret allowed" 200 \
  '{"executors":[{"image":"alpine","command":["echo","test"]}],"volumes":["/secret"]}'

post_task "workdir /secret allowed" 200 \
  '{"executors":[{"image":"alpine","command":["echo","test"],"workdir":"/secret/work"}]}'

post_task "segment boundary /devices allowed" 200 \
  '{"executors":[{"image":"alpine","command":["echo","test"]}],"inputs":[{"url":"file:///src","path":"/devices/data"}]}'

post_task "segment boundary /data/dev allowed" 200 \
  '{"executors":[{"image":"alpine","command":["echo","test"]}],"inputs":[{"url":"file:///src","path":"/data/dev"}]}'

echo
echo "Existing-server deny-list smoke tests passed."

using server config

 ForbiddenPathPrefixes: 
    - /dev

@github-actions

Copy link
Copy Markdown

Gen3 Integration Tests

filepath passed skipped SUBTOTAL
tests/test_gen3_workflow.py 25 4 29
TOTAL 25 4 29

…l and not ban anything by default and only ban what the user specifies in server config
@matthewpeterkort matthewpeterkort requested a review from nss10 July 14, 2026 17:48
@github-actions

Copy link
Copy Markdown

Gen3 Integration Tests

filepath passed skipped SUBTOTAL
tests/test_gen3_workflow.py 25 4 29
TOTAL 25 4 29

@matthewpeterkort

Copy link
Copy Markdown
Contributor

Updated behavior:

There are no default paths specified in the empty server config by default, users must specify any paths that they want banned, but they are given the ones that should be banned in config/default-config.yaml for them to implement if they want to.

tested using same script described above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants