-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add SeaweedFS namespace isolation security test to CI #3141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
google-oss-prow
merged 17 commits into
kubeflow:master
from
akagami-harsh:test-namespace-isolation
May 26, 2025
+326
−13
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
c0bdea4
add test-swfs-namespace-isolation.sh script
akagami-harsh 4e00eec
add script in the gh action
akagami-harsh bf49b6b
add update flag
akagami-harsh 7311e81
add python s3 helper script
akagami-harsh 2a15fc4
simplify test_swfs_namespace_isolation.sh script
akagami-harsh ceb2a45
update swfs pipelines gha
akagami-harsh 3674219
Update pipeline_swfs_test.yaml
juliusvonkohout 532b89a
Update test_swfs_namespace_isolation.sh
juliusvonkohout 11a7d5d
Update test_swfs_namespace_isolation.sh
juliusvonkohout 7f2d402
Update test_swfs_namespace_isolation.sh
juliusvonkohout f6b7702
Update test_swfs_namespace_isolation.sh
juliusvonkohout 5740525
Update seaweedfs-pvc.yaml
juliusvonkohout 4f4ba1d
Update pipeline_swfs_test.yaml
juliusvonkohout e89ff33
Update pipeline_swfs_test.yaml
juliusvonkohout fc36245
Update pipeline_swfs_test.yaml
juliusvonkohout dbd0d8d
Update pipeline_swfs_test.yaml
juliusvonkohout f1e0a38
Update test_swfs_namespace_isolation.sh
juliusvonkohout File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,4 +8,4 @@ spec: | |
| - ReadWriteOnce | ||
| resources: | ||
| requests: | ||
| storage: 20Gi | ||
| storage: 5Gi | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| #!/usr/bin/env python3 | ||
| """ | ||
| S3 helper script for SeaweedFS namespace isolation testing. | ||
| Uses boto3 to perform S3 operations for security testing. | ||
| """ | ||
|
|
||
| import sys | ||
| import boto3 | ||
| from botocore.exceptions import ClientError, NoCredentialsError | ||
| import argparse | ||
|
|
||
|
|
||
| def create_s3_client(access_key, secret_key, endpoint_url): | ||
| """Create S3 client with given credentials.""" | ||
| return boto3.client( | ||
| 's3', | ||
| aws_access_key_id=access_key, | ||
| aws_secret_access_key=secret_key, | ||
| endpoint_url=endpoint_url, | ||
| region_name='us-east-1' # Required but not used by SeaweedFS | ||
| ) | ||
|
|
||
|
|
||
| def upload_file(access_key, secret_key, endpoint_url, bucket, key, content): | ||
| """Upload a file to S3.""" | ||
| try: | ||
| s3_client = create_s3_client(access_key, secret_key, endpoint_url) | ||
| s3_client.put_object( | ||
| Bucket=bucket, | ||
| Key=key, | ||
| Body=content.encode('utf-8') | ||
| ) | ||
| print(f"✓ Successfully uploaded file to s3://{bucket}/{key}") | ||
| return True | ||
| except Exception as e: | ||
| print(f"✗ Failed to upload file: {e}") | ||
| return False | ||
|
|
||
|
|
||
| def download_file(access_key, secret_key, endpoint_url, bucket, key): | ||
| """Download a file from S3.""" | ||
| try: | ||
| s3_client = create_s3_client(access_key, secret_key, endpoint_url) | ||
| response = s3_client.get_object(Bucket=bucket, Key=key) | ||
| content = response['Body'].read().decode('utf-8') | ||
| print(f"✓ Successfully downloaded file from s3://{bucket}/{key}") | ||
| print(f"File content: {content}") | ||
| return True, content | ||
| except ClientError as e: | ||
| error_code = e.response['Error']['Code'] | ||
| if error_code in ['NoSuchKey', 'AccessDenied', 'Forbidden']: | ||
| print(f"✓ Access denied as expected: {error_code}") | ||
| return False, None | ||
| else: | ||
| print(f"✗ Unexpected error: {e}") | ||
| return False, None | ||
| except Exception as e: | ||
| print(f"✗ Failed to download file: {e}") | ||
| return False, None | ||
|
|
||
|
|
||
| def main(): | ||
| parser = argparse.ArgumentParser(description='S3 operations for SeaweedFS testing') | ||
| parser.add_argument('operation', choices=['upload', 'download'], help='Operation to perform') | ||
| parser.add_argument('--access-key', required=True, help='AWS access key') | ||
| parser.add_argument('--secret-key', required=True, help='AWS secret key') | ||
| parser.add_argument('--endpoint-url', required=True, help='S3 endpoint URL') | ||
| parser.add_argument('--bucket', required=True, help='S3 bucket name') | ||
| parser.add_argument('--key', required=True, help='S3 object key') | ||
| parser.add_argument('--content', help='Content to upload (for upload operation)') | ||
|
|
||
| args = parser.parse_args() | ||
|
|
||
| if args.operation == 'upload': | ||
| if not args.content: | ||
| print("Error: --content is required for upload operation") | ||
| sys.exit(1) | ||
| success = upload_file(args.access_key, args.secret_key, args.endpoint_url, | ||
| args.bucket, args.key, args.content) | ||
| sys.exit(0 if success else 1) | ||
|
|
||
| elif args.operation == 'download': | ||
| success, content = download_file(args.access_key, args.secret_key, args.endpoint_url, | ||
| args.bucket, args.key) | ||
| # For security test: success=True means unauthorized access (bad) | ||
| # success=False means access denied (good) | ||
| if args.key.startswith('private-artifacts/') and '/' in args.key[18:]: | ||
| # This is a cross-namespace access attempt | ||
| sys.exit(1 if success else 0) | ||
| else: | ||
| sys.exit(0 if success else 1) | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| #!/bin/bash | ||
| set -euxo pipefail | ||
|
|
||
| echo "SeaweedFS Security Test - Unauthorized Access Check" | ||
| echo "Testing if one namespace can access files from another namespace" | ||
|
|
||
| # Check dependencies | ||
| for cmd in kubectl python3; do | ||
| if ! command -v $cmd &> /dev/null; then | ||
| echo "Error: $cmd is required but not installed" | ||
| exit 1 | ||
| fi | ||
| done | ||
|
|
||
| # Install boto3 if not available | ||
| if ! python3 -c "import boto3" 2>/dev/null; then | ||
| echo "Installing boto3..." | ||
| pip3 install boto3 | ||
| fi | ||
|
|
||
| PORT_FORWARD_PID="" | ||
| # Cleanup function | ||
| cleanup() { | ||
| echo "Cleaning up..." | ||
| if [ -n "$PORT_FORWARD_PID" ]; then | ||
| kill $PORT_FORWARD_PID 2>/dev/null || true | ||
| fi | ||
| rm -f test-file.txt accessed-file.txt | ||
| kubectl delete profile test-profile-1 test-profile-2 --ignore-not-found | ||
| } | ||
| trap cleanup EXIT | ||
|
|
||
| # Create test profiles | ||
| create_profiles() { | ||
| echo "Creating test profiles..." | ||
|
|
||
| # Create both profiles | ||
| kubectl apply -f - <<EOF | ||
| apiVersion: kubeflow.org/v1 | ||
| kind: Profile | ||
| metadata: | ||
| name: test-profile-1 | ||
| spec: | ||
| owner: | ||
| kind: User | ||
| name: test-user-1@example.com | ||
| --- | ||
| apiVersion: kubeflow.org/v1 | ||
| kind: Profile | ||
| metadata: | ||
| name: test-profile-2 | ||
| spec: | ||
| owner: | ||
| kind: User | ||
| name: test-user-2@example.com | ||
| EOF | ||
|
|
||
| # Wait for namespaces | ||
| echo "Waiting for namespaces..." | ||
| for i in {1..6}; do | ||
| if kubectl get namespace test-profile-1 test-profile-2 >/dev/null 2>&1; then | ||
| echo "Namespaces created" | ||
| return 0 | ||
| fi | ||
| sleep 10 | ||
| done | ||
|
|
||
| echo "Error: Namespaces not created" | ||
| exit 1 | ||
| } | ||
|
|
||
| # Wait for S3 credentials | ||
| wait_for_credentials() { | ||
| local namespace=$1 | ||
| echo "Waiting for S3 credentials in $namespace..." | ||
|
|
||
| for i in {1..6}; do | ||
| if kubectl get secret -n $namespace mlpipeline-minio-artifact >/dev/null 2>&1; then | ||
| echo "Credentials found" | ||
| return 0 | ||
| fi | ||
| sleep 10 | ||
| done | ||
|
|
||
| echo "Error: No credentials found" | ||
| return 1 | ||
| } | ||
|
|
||
| # Get credentials for namespace | ||
| get_credentials() { | ||
| local namespace=$1 | ||
| local access_key=$(kubectl get secret -n $namespace mlpipeline-minio-artifact -o jsonpath='{.data.accesskey}' | base64 -d) | ||
| local secret_key=$(kubectl get secret -n $namespace mlpipeline-minio-artifact -o jsonpath='{.data.secretkey}' | base64 -d) | ||
| echo "$access_key:$secret_key" | ||
| } | ||
|
|
||
| # Setup port forward to SeaweedFS | ||
| setup_port_forward() { | ||
| if [ -n "$PORT_FORWARD_PID" ]; then | ||
| return 0 # Already running | ||
| fi | ||
|
|
||
| echo "Setting up port-forward..." | ||
| local pod=$(kubectl get pod -n kubeflow -l app=seaweedfs -o jsonpath='{.items[0].metadata.name}') | ||
| kubectl port-forward -n kubeflow pod/$pod 8333:8333 >/dev/null 2>&1 & | ||
| PORT_FORWARD_PID=$! | ||
| sleep 3 | ||
| } | ||
|
|
||
| # Upload test file | ||
| upload_file() { | ||
| local namespace=$1 | ||
| echo "Uploading test file to $namespace..." | ||
|
|
||
| local credentials=$(get_credentials $namespace) | ||
| local access_key=$(echo $credentials | cut -d: -f1) | ||
| local secret_key=$(echo $credentials | cut -d: -f2) | ||
|
|
||
| setup_port_forward | ||
|
|
||
| python3 tests/gh-actions/s3_test_helper.py upload \ | ||
| --access-key "$access_key" \ | ||
| --secret-key "$secret_key" \ | ||
| --endpoint-url "http://localhost:8333" \ | ||
| --bucket "mlpipeline" \ | ||
| --key "private-artifacts/$namespace/test-file.txt" \ | ||
| --content "Test file for $namespace" | ||
| } | ||
|
|
||
| # Test unauthorized access | ||
| test_unauthorized_access() { | ||
| local from_namespace=$1 | ||
| local target_namespace=$2 | ||
|
|
||
| echo "Testing unauthorized access from $from_namespace to $target_namespace..." | ||
|
|
||
| local credentials=$(get_credentials $from_namespace) | ||
| local access_key=$(echo $credentials | cut -d: -f1) | ||
| local secret_key=$(echo $credentials | cut -d: -f2) | ||
|
|
||
| setup_port_forward | ||
|
|
||
| # Try to access the other namespace's file | ||
| # Note: Python script returns 0 when access is denied (good), 1 when access succeeds (bad) | ||
| if python3 tests/gh-actions/s3_test_helper.py download \ | ||
| --access-key "$access_key" \ | ||
| --secret-key "$secret_key" \ | ||
| --endpoint-url "http://localhost:8333" \ | ||
| --bucket "mlpipeline" \ | ||
| --key "private-artifacts/$target_namespace/test-file.txt"; then | ||
|
|
||
| echo "Security OK: Access denied as expected" | ||
| return 0 | ||
| else | ||
| echo "SECURITY ISSUE: Unauthorized access successful!" | ||
| return 1 | ||
| fi | ||
| } | ||
|
|
||
| # Main test function | ||
| main() { | ||
| echo "Starting security test..." | ||
|
|
||
| # Create test profiles | ||
| create_profiles | ||
|
|
||
| # Wait for credentials to be created | ||
| echo "Waiting for profile controller to create credentials..." | ||
| sleep 30 | ||
|
|
||
| wait_for_credentials "test-profile-1" || { | ||
| echo "Failed to get credentials for test-profile-1" | ||
| exit 1 | ||
| } | ||
|
|
||
| wait_for_credentials "test-profile-2" || { | ||
| echo "Failed to get credentials for test-profile-2" | ||
| exit 1 | ||
| } | ||
|
|
||
| # Upload file to first namespace | ||
| upload_file "test-profile-1" || { | ||
| echo "Failed to upload file" | ||
| exit 1 | ||
| } | ||
|
|
||
| # Test unauthorized access | ||
| if test_unauthorized_access "test-profile-2" "test-profile-1"; then | ||
| echo "SECURITY TEST PASSED: No unauthorized access detected" | ||
| else | ||
| echo "SECURITY TEST FAILED: Unauthorized access detected" | ||
| echo "This indicates a security vulnerability in the SeaweedFS setup" | ||
| exit 1 | ||
| fi | ||
| } | ||
|
|
||
| main |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.