Version: 1.0 Date: 2025-10-28 Related: BASTION_SECURITY_REQUIREMENTS.md
This document provides specific security test scenarios for Azure Bastion integration. Each test validates a security requirement from BASTION_SECURITY_REQUIREMENTS.md.
Test Environment Requirements:
- Azure subscription with Bastion, VNet, and test VMs
- Two Azure AD user accounts (for multi-user tests)
- Service principal with limited RBAC roles (for auth tests)
- Local development environment with azlin installed
Requirement: REQ-AUTH-001 Severity: P0 BLOCKER
Test Steps:
- Logout from Azure CLI:
az logout - Clear service principal environment variables
- Attempt Bastion operation:
azlin bastion create --resource-group test-rg - Expected: Failure with error: "No Azure credentials available. Please run: az login"
- Login:
az login - Retry Bastion operation
- Expected: Success
Pass Criteria:
- Bastion operations fail without valid credentials
- Clear error message with remediation steps
- No credential prompts (batch mode only)
Requirement: REQ-AUTH-002 Severity: P0 BLOCKER
Test Scenarios:
Scenario 1: No Network Contributor Role
- Create service principal with Reader role only
- Attempt to deploy Bastion:
azlin bastion create --resource-group test-rg - Expected: Failure with error mentioning "Network Contributor" role required
Scenario 2: No VM User Login Role
- Service principal has Network Contributor
- Attempt VM connection via Bastion:
azlin connect test-vm --use-bastion - Expected: Failure with error mentioning "Virtual Machine User Login" role required
Pass Criteria:
- Clear error messages specifying missing RBAC role
- Error includes Azure CLI command to grant role
- No cryptic Azure ARM error messages
Requirement: REQ-AUTH-003 Severity: P0 BLOCKER
Test Steps:
- Clean environment: remove all credential files
- Use Azure CLI authentication:
az login - Deploy Bastion:
azlin bastion create --resource-group test-rg - Check filesystem for new credential files:
find ~/.azlin -type f -name "*cred*" -o -name "*token*" -o -name "*secret*"
- Expected: No new credential files created
- Verify config.toml contains no secrets:
cat ~/.azlin/config.toml | grep -iE "secret|token|password"
- Expected: No matches
Pass Criteria:
- No credential files created by azlin
- Config files contain no secrets
- Credentials delegated to Azure CLI or SDK
Requirement: REQ-AUTH-004 Severity: P0 BLOCKER
Test Steps:
- Create service principal with invalid client secret
- Set environment variable:
export AZLIN_SP_CLIENT_SECRET="test-secret-12345" - Attempt Bastion operation with service principal profile
- Capture error output
- Verify: Error output does NOT contain "test-secret-12345"
- Verify: Error output contains "[REDACTED]" or "****" instead
- Check logs:
cat ~/.azlin/logs/azlin.log - Verify: Logs do NOT contain "test-secret-12345"
Pass Criteria:
- No secrets in error messages
- No secrets in log files
- Sanitization markers ([REDACTED] or ****) present
Requirement: REQ-ACCESS-001 Severity: P0 BLOCKER
Test Scenarios:
Scenario 1: Valid Private IP
- Create VM with private IP in RFC 1918 range: 10.0.1.4
- Connect via Bastion:
azlin connect test-vm --use-bastion - Expected: Connection proceeds (IP validation passes)
Scenario 2: Invalid Private IP
- Mock VM with invalid private IP: 300.0.1.4
- Attempt connection:
azlin connect test-vm --use-bastion - Expected: Failure with error: "Invalid private IP address: 300.0.1.4"
Scenario 3: Public IP in Private-Only VM
- Create VM without public IP (private-only)
- Query VM details
- Verify: No public IP returned
- Connect via Bastion:
azlin connect test-vm --use-bastion - Expected: Connection uses private IP through Bastion
Pass Criteria:
- Invalid IP addresses rejected
- Clear error messages
- Private-only VMs connect successfully
Requirement: REQ-ACCESS-002 Severity: P0 BLOCKER
Test Steps:
- Deploy Bastion in VNet-A
- Deploy VM in VNet-B (different, unpeered VNet)
- Attempt connection:
azlin connect vm-in-vnetb --use-bastion - Expected: Failure with error: "VM 'vm-in-vnetb' is in VNet 'VNet-B', but Bastion 'bastion-westus' is in VNet 'VNet-A'. VNet peering required."
- Create VNet peering between VNet-A and VNet-B
- Retry connection:
azlin connect vm-in-vnetb --use-bastion - Expected: Connection succeeds
Pass Criteria:
- VNet mismatch detected before tunnel creation
- Clear error message with remediation steps
- Connection succeeds after peering established
Requirement: REQ-ACCESS-003 Severity: P1 HIGH
Test Steps:
- Create NSG with overly restrictive rules (deny all inbound)
- Associate NSG with AzureBastionSubnet
- Attempt Bastion deployment or connection
- Expected: Warning message: "Bastion subnet NSG may block traffic. Required inbound: TCP 443 from Internet. Required outbound: TCP 22/3389 to VNet."
- Update NSG with correct rules
- Retry operation
- Expected: Success, no warnings
Pass Criteria:
- NSG misconfigurations detected
- Clear warnings with required rules
- Operations proceed (warn, don't block)
Requirement: REQ-ACCESS-004 Severity: P1 HIGH
Test Steps:
- Create VM with NSG denying SSH (TCP 22)
- Attempt Bastion connection:
azlin connect test-vm --use-bastion - Expected: Warning: "VM 'test-vm' NSG does not allow SSH from Bastion. Add rule: Source=AzureBastionSubnet, Dest Port=22, Protocol=TCP, Action=Allow"
- Add NSG rule as instructed
- Retry connection
- Expected: Connection succeeds
Pass Criteria:
- NSG issues detected before connection attempt
- Clear remediation guidance
- Connection succeeds after NSG fix
Requirement: REQ-TUNNEL-001 Severity: P0 BLOCKER
Test Steps:
- Create Bastion tunnel:
azlin connect test-vm --use-bastion - In tunnel creation logs, capture local port number (e.g., 52341)
- From same machine, verify tunnel is accessible:
Expected: Connection succeeds
nc -zv 127.0.0.1 52341
- From different machine on same network, attempt connection:
Expected: Connection refused (port not open to network)
nc -zv <machine-ip> 52341
- Verify tunnel process binding:
Expected: Shows binding to 127.0.0.1:52341, NOT 0.0.0.0:52341
netstat -tuln | grep 52341
Pass Criteria:
- Tunnel accessible only from localhost
- Not accessible from network
- Binding verified in process list
Requirement: REQ-TUNNEL-002 Severity: P0 BLOCKER
Test Steps:
- Create 10 Bastion tunnels in sequence
- Record local port for each tunnel
- Verify: All ports are unique
- Verify: All ports are in ephemeral range (49152-65535)
- Verify: Ports appear randomly distributed (no sequential pattern)
- Close all tunnels
- Create another tunnel
- Verify: Port is different from previous 10
Pass Criteria:
- All ports unique
- Ports in ephemeral range
- No predictable pattern
Requirement: REQ-TUNNEL-003 Severity: P0 BLOCKER
Test Steps:
- Mock
az network bastion tunnelto hang (no port opened) - Attempt connection:
azlin connect test-vm --use-bastion - Verify: Connection times out after 10 seconds
- Verify: Error message: "Tunnel creation timed out after 10 seconds"
- Verify: Tunnel process is terminated (no orphans)
Pass Criteria:
- Timeout enforced
- Clear error message
- No orphaned processes
Requirement: REQ-TUNNEL-004 Severity: P0 BLOCKER
Test Scenarios:
Scenario 1: Normal SSH Exit
- Create tunnel and connect:
azlin connect test-vm --use-bastion - Exit SSH session normally (type
exit) - Verify: Tunnel process terminates
- Check process list:
ps aux | grep "az network bastion tunnel" - Expected: No tunnel processes
Scenario 2: SIGINT (Ctrl+C)
- Create tunnel and connect
- Send SIGINT: Press Ctrl+C
- Verify: Tunnel process terminates
- Verify: No orphaned processes
Scenario 3: SIGTERM
- Create tunnel and connect
- From another terminal, kill SSH process:
kill <ssh-pid> - Verify: Tunnel process terminates
- Verify: No orphaned processes
Scenario 4: SIGKILL (Hard Kill)
- Create tunnel and connect
- From another terminal, force kill:
kill -9 <ssh-pid> - Verify: Tunnel process terminates within 5 seconds
- If not terminated, atexit handler should clean up
Pass Criteria:
- Tunnel cleanup on all exit scenarios
- No orphaned tunnel processes
- Cleanup verified in process list
Requirement: REQ-TUNNEL-005 Severity: P1 HIGH
Test Steps:
- Create tunnel:
azlin connect test-vm --use-bastion - Inspect tunnel subprocess:
ps -o pid,ppid,user,args -p <tunnel-pid>
- Verify: Tunnel process has no stdin/stdout/stderr attached to terminal
- Verify: Tunnel process args do NOT contain
shell=True - Verify: Tunnel process is child of azlin process (correct parent)
Pass Criteria:
- No terminal I/O inheritance
- No shell=True usage
- Correct process hierarchy
Requirement: REQ-TUNNEL-006 Severity: P1 HIGH
Test Steps:
- Create 3 tunnels to different VMs
- Run command:
azlin bastion tunnels --list - Expected Output:
Active Bastion Tunnels: VM Name Local Port Bastion Name Created test-vm-1 52341 bastion-westus 2025-10-28 10:30:00 test-vm-2 52342 bastion-westus 2025-10-28 10:31:00 test-vm-3 52343 bastion-eastus 2025-10-28 10:32:00 - Close one tunnel (exit SSH session)
- Re-run:
azlin bastion tunnels --list - Verify: Closed tunnel no longer listed
- Restart azlin process
- Re-run:
azlin bastion tunnels --list - Expected: Empty list (state not persisted, as designed)
Pass Criteria:
- Active tunnels correctly listed
- Closed tunnels removed from list
- State not persisted across restarts
Requirement: REQ-TUNNEL-007 Severity: P0 BLOCKER
Test Steps:
- Run azlin as non-root user
- Create tunnel
- Check tunnel process ownership:
ps -o user,pid,args -p <tunnel-pid>
- Verify: Process owned by current user (not root)
- Verify: No setuid/setgid bits set
- Attempt to run azlin with sudo (if supported)
- Verify: Tunnel processes still owned by original user (not root)
Pass Criteria:
- Tunnel processes owned by current user
- No privilege escalation
- Runs correctly without root
Requirement: REQ-TUNNEL-008 Severity: P1 HIGH
Test Steps:
- Login to Azure CLI:
az login - Wait for token to be near expiration (or mock expired token)
- Attempt tunnel creation
- Expected: Error or prompt: "Azure CLI token expired or expiring soon. Please re-authenticate: az login"
- Re-authenticate:
az login - Retry tunnel creation
- Expected: Success
Pass Criteria:
- Expired tokens detected
- Clear re-authentication prompt
- Connection succeeds after re-auth
Requirement: REQ-CONFIG-001 Severity: P0 BLOCKER
Test Steps:
- Create Bastion configuration
- Attempt to save config with secret field:
config = { "bastion": { "name": "test-bastion", "client_secret": "should-not-save" # Malicious } } ConfigManager.save_config(config)
- Expected: Exception raised: "Secrets not allowed in Bastion config"
- Read config file:
cat ~/.azlin/config.toml - Verify: No "client_secret" field present
Pass Criteria:
- Secret fields rejected
- Config file contains no secrets
- Clear error message
Requirement: REQ-CONFIG-002 Severity: P0 BLOCKER
Test Steps:
- Create config file with insecure permissions:
echo "[bastion]" > ~/.azlin/config.toml chmod 0644 ~/.azlin/config.toml
- Run azlin command that reads config
- Expected: Warning: "Config file has insecure permissions 0644, fixing to 0600"
- Check permissions:
stat -c "%a" ~/.azlin/config.toml
- Expected: 600
Pass Criteria:
- Insecure permissions detected
- Auto-fix to 0600
- Warning message displayed
Requirement: REQ-CONFIG-003 Severity: P0 BLOCKER
Test Scenarios:
Scenario 1: Path Traversal in Config Path
azlin config load --config-file "../../etc/passwd"Expected: Error: "Invalid config path: ../../etc/passwd"
Scenario 2: Absolute Path to Sensitive File
azlin config load --config-file "/etc/shadow"Expected: Error: "Config path outside allowed directories"
Scenario 3: Symlink to Sensitive File
ln -s /etc/passwd ~/.azlin/evil-config.toml
azlin config load --config-file ~/.azlin/evil-config.tomlExpected: Error after symlink resolution
Pass Criteria:
- All path traversal attempts blocked
- Symlinks resolved and validated
- Clear error messages
Requirement: REQ-CONFIG-004 Severity: P1 HIGH
Test Steps:
- Enable debug logging:
azlin --debug bastion create ... - Capture log output
- Search for subscription IDs:
grep -E "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" azlin.log - Verify: All subscription IDs are masked:
12345678-****-****-****-************ - Verify: Bastion names and resource groups are NOT masked (not sensitive)
Pass Criteria:
- Subscription IDs masked in logs
- Resource names visible (not overly masked)
- Consistent masking pattern
Requirement: REQ-CONFIG-005 Severity: P1 HIGH
Test Scenarios:
Invalid Names:
azlin bastion create --name "bastion_invalid" # Underscore not allowed
azlin bastion create --name "-bastion" # Starts with hyphen
azlin bastion create --name "bastion-" # Ends with hyphen
azlin bastion create --name "a"*81 # Too long (>80 chars)
azlin bastion create --name "bastion@test" # Special charValid Names:
azlin bastion create --name "bastion-westus" # Valid
azlin bastion create --name "Bastion123" # Valid (alphanumeric)
azlin bastion create --name "b" # Valid (1 char)Pass Criteria:
- Invalid names rejected with clear error
- Valid names accepted
- Error message explains naming rules
Requirement: REQ-ERROR-001 Severity: P0 BLOCKER
Test Steps:
- Set invalid service principal secret
- Trigger authentication error
- Capture error output and stack trace
- Search for secret pattern:
echo "$ERROR_OUTPUT" | grep -iE "secret|password|token"
- Verify: No actual secret values found
- Verify: Sanitization markers present ([REDACTED] or ****)
Pass Criteria:
- No secrets in error messages
- No secrets in stack traces
- Sanitization applied consistently
Requirement: REQ-ERROR-002 Severity: P1 HIGH
Test Steps:
- Trigger various errors (auth failure, network failure, RBAC failure)
- Capture error messages in non-debug mode
- Verify: No internal IPs exposed
- Verify: No file paths exposed
- Verify: No process IDs exposed
- Enable debug mode:
--debug - Re-trigger same errors
- Verify: Internal details NOW visible (debug mode only)
Pass Criteria:
- Internal details hidden in normal mode
- Internal details visible in debug mode
- User-friendly error messages
Requirement: REQ-ERROR-003 Severity: P1 HIGH
Test Scenarios:
Scenario 1: RBAC Error Trigger RBAC error, verify error message includes:
- Required role name
- Azure CLI command to grant role
- Link to RBAC documentation
Scenario 2: NSG Error Trigger NSG blocking SSH, verify error message includes:
- NSG rule to add
- Azure CLI command to add rule
- Link to NSG documentation
Scenario 3: Authentication Error Trigger auth error, verify error message includes:
- Authentication command (
az login) - Link to authentication docs
Pass Criteria:
- All errors include remediation steps
- Azure CLI commands are copy-pasteable
- Links to documentation provided
Requirement: REQ-ERROR-004 Severity: P0 BLOCKER
Test Steps:
- Create VM with private IP only (no public IP)
- Configure VM to use Bastion
- Disable or delete Bastion
- Attempt connection:
azlin connect test-vm - Expected: Connection FAILS with error: "VM 'test-vm' is configured for Bastion-only access, but Bastion 'bastion-westus' is unavailable."
- Verify: No fallback to public IP (there is none)
- Verify: No bypass to direct private IP connection
- Attempt with force flag:
azlin connect test-vm --force-direct - Expected: Still fails (no public IP available)
Pass Criteria:
- Bastion failure prevents connection
- No security bypass
- Clear error message
- Force flag documented but respects security
Requirement: REQ-ERROR-005 Severity: P1 HIGH
Test Steps:
- Mock tunnel creation to fail intermittently
- Attempt connection:
azlin connect test-vm --use-bastion - Verify: Automatic retry with exponential backoff
- Verify: Maximum 3 retry attempts
- Verify: Connection succeeds on 2nd attempt (simulated)
- Mock all 3 attempts to fail
- Verify: Final error message with troubleshooting tips
- Verify: No orphaned processes left behind
Pass Criteria:
- Automatic retries with backoff
- Max 3 attempts
- Clean failure after retries exhausted
- No resource leaks
Requirement: REQ-MULTIUSER-001 Severity: P0 BLOCKER
Test Steps:
- User A: Login:
az login(user-a@domain.com) - User A: Create tunnel:
azlin connect test-vm --use-bastion - User A: Capture tunnel local port (e.g., 52341)
- User B: Login:
az login(user-b@domain.com) in separate terminal - User B: Create tunnel to same VM:
azlin connect test-vm --use-bastion - User B: Capture tunnel local port (e.g., 52342)
- Verify: User A and User B have different local ports
- Verify: User A's tunnel NOT accessible to User B (no shared state)
- Verify: Both tunnels use their respective Azure credentials
Pass Criteria:
- Each user has independent tunnel
- No shared credentials or state
- Both users can connect simultaneously
Requirement: REQ-MULTIUSER-002 Severity: P1 HIGH
Test Steps:
- Grant User A "VM User Login" role on test-vm
- Grant User B "Reader" role only (no VM login)
- User A: Connect:
azlin connect test-vm --use-bastion - Expected: Success
- User B: Connect:
azlin connect test-vm --use-bastion - Expected: Failure with error: "Insufficient permissions. Required role: 'Virtual Machine User Login'"
Pass Criteria:
- RBAC enforced at Azure level
- Clear error for insufficient permissions
- No bypass mechanism
Requirement: REQ-MULTIUSER-003 Severity: P1 HIGH
Test Steps:
- Connect via Bastion:
azlin connect test-vm --use-bastion - Check audit log:
cat ~/.azlin/connection_history.json - Verify: Entry contains:
- Timestamp
- User (from Azure credentials)
- VM name
- Connection method: "bastion"
- Bastion name
- Result: "success"
- Verify log file permissions:
stat -c "%a" ~/.azlin/connection_history.json
- Expected: 600
- Attempt connection to VM without permission (fail)
- Check audit log again
- Verify: Entry with result: "failure" and error reason
Pass Criteria:
- All connections logged
- Log format includes required fields
- Log file has 0600 permissions
- Both success and failure logged
Requirement: REQ-MULTIUSER-004 Severity: P2 MEDIUM
Test Steps:
- Connect via Bastion multiple times
- Wait 5 minutes (Activity Log delay)
- Run KQL query in Azure Portal Log Analytics:
AzureActivity | where ResourceProvider == "Microsoft.Network" | where ResourceType == "bastionHosts" | where TimeGenerated > ago(1h) | project TimeGenerated, Caller, OperationNameValue, ActivityStatusValue, ResourceId
- Verify: Entries for Bastion tunnel creation
- Verify: Entries include Caller (user identity)
- Document working KQL queries in
docs/BASTION_SECURITY_MONITORING.md
Pass Criteria:
- KQL queries documented
- Activity Log captures Bastion access
- Queries return expected results
Requirement: REQ-DEPLOY-001 Severity: P0 BLOCKER
Test Scenarios:
Scenario 1: Wrong Subnet Name
azlin bastion create --resource-group test-rg --subnet "BastionSubnet"Expected: Error: "Bastion subnet must be named 'AzureBastionSubnet'"
Scenario 2: Subnet Too Small Create subnet with /27 prefix (32 addresses), attempt Bastion deployment Expected: Error: "Bastion subnet must be at least /26 (64 addresses). Current: /27"
Scenario 3: Valid Subnet Create subnet named "AzureBastionSubnet" with /26 prefix Expected: Deployment succeeds
Pass Criteria:
- Wrong subnet names rejected
- Insufficient subnet size rejected
- Valid configurations accepted
Requirement: REQ-DEPLOY-002 Severity: P0 BLOCKER
Test Steps:
- Create AzureBastionSubnet
- Deploy VM in same subnet (violation)
- Attempt Bastion deployment
- Expected: Error: "AzureBastionSubnet contains non-Bastion resources: [vm-name]. Remove resources before deploying Bastion."
- Remove VM from subnet
- Retry Bastion deployment
- Expected: Success
Pass Criteria:
- Non-Bastion resources detected
- Clear error message
- Deployment proceeds only after cleanup
Requirement: REQ-DEPLOY-003 Severity: P1 HIGH
Test Steps:
- Detect local network CIDR (e.g., 192.168.1.0/24)
- Attempt VNet deployment with overlapping CIDR: 192.168.1.0/24
- Expected: Warning: "VNet CIDR 192.168.1.0/24 overlaps with local network. This may cause routing issues. Use --force to proceed."
- Proceed without --force
- Expected: Deployment aborted
- Retry with --force flag
- Expected: Deployment proceeds (user acknowledged risk)
Pass Criteria:
- Overlap detection works
- Clear warning with risks explained
- Force flag allows override
Requirement: REQ-DEPLOY-004 Severity: P0 BLOCKER
Test Steps:
- Attempt Bastion deployment with Basic SKU Public IP (if possible to specify)
- Expected: Error: "Bastion requires Standard SKU Public IP. Basic SKU not supported."
- Allow auto-creation with correct SKU
- Verify: Public IP created is Standard SKU with Static allocation
- Query public IP:
az network public-ip show --name bastion-pip --resource-group test-rg
- Verify: "sku": "Standard", "publicIPAllocationMethod": "Static"
Pass Criteria:
- Incorrect SKU rejected
- Correct SKU auto-selected
- Static allocation enforced
Requirement: REQ-DEPLOY-005 Severity: P1 HIGH
Test Steps:
- Deploy VM with public IP:
azlin new test-vm --size Standard_D2s_v3 - Verify VM has public IP:
az vm show --name test-vm --resource-group test-rg --query "publicIps" - Remove public IP:
azlin vm remove-public-ip test-vm - Expected: Confirmation prompt: "This will make VM accessible only via Bastion. Continue? (y/N)"
- Confirm: y
- Verify: Public IP removed
- Attempt direct connection:
azlin connect test-vm - Expected: Error: "VM has no public IP. Use --use-bastion flag to connect via Bastion."
- Connect via Bastion:
azlin connect test-vm --use-bastion - Expected: Success
Pass Criteria:
- Public IP removal requires confirmation
- Clear warning about impact
- Bastion connection works after removal
Requirement: REQ-DEPLOY-006 Severity: P1 HIGH
Test Steps:
- Deploy Bastion without specifying NSG
- Verify: No NSG auto-created on AzureBastionSubnet
- Verify: Bastion functions correctly (Azure manages traffic)
- Consult documentation:
docs/BASTION_NSG_GUIDE.md - Verify: Documentation includes:
- Statement: "NSG on Bastion subnet is optional"
- If NSG required (compliance): rules listed
- Warning about NSG misconfigurations
Pass Criteria:
- No NSG auto-created
- Documentation complete
- Bastion works without NSG
Requirement: All existing SEC-* requirements Severity: P0 BLOCKER
Test Steps:
- Run existing security test suite:
pytest tests/unit/test_auth_security.py - Verify: All existing security tests pass
- Run SSH key tests:
pytest tests/unit/test_ssh_keys.py - Verify: Key permissions still enforced (0600)
- Run log sanitization tests:
pytest tests/unit/test_log_sanitizer.py - Verify: Log sanitization still works
- Run service principal tests:
pytest tests/unit/test_service_principal_auth.py - Verify: Service principal auth unchanged
Pass Criteria:
- All existing security tests pass
- No regressions in existing security controls
- Bastion code does not weaken existing security
Requirement: Backward compatibility Severity: P0 BLOCKER
Test Steps:
- Create VM with public IP (traditional setup)
- Connect without Bastion flag:
azlin connect test-vm - Expected: Direct SSH connection (not via Bastion)
- Verify: Connection speed comparable to before Bastion feature
- Verify: No Bastion tunnel created
Pass Criteria:
- Direct SSH connections work as before
- No performance degradation
- Bastion opt-in only (not forced)
Attack: Local attacker attempts to connect to another user's tunnel
Test Steps:
- User A creates tunnel on port 52341
- User B (different local user) attempts connection:
ssh -p 52341 azureuser@127.0.0.1
- Expected: Connection refused (tunnel bound to User A's session)
- User B attempts to list User A's tunnel:
azlin bastion tunnels --list # As User B - Expected: Only User B's tunnels listed (no cross-user visibility)
Pass Criteria:
- Tunnel not accessible to other local users
- No cross-user state visibility
Attack: Attacker attempts to read sensitive files via config path manipulation
Test Steps:
- Attempt to load /etc/passwd as config:
azlin config load --config-file /etc/passwd
- Expected: Error (path outside allowed directories)
- Attempt symlink attack:
ln -s /etc/shadow ~/.azlin/config.toml azlin config load - Expected: Error after symlink resolution
- Attempt directory traversal:
azlin config load --config-file "../../../etc/passwd" - Expected: Error (path traversal blocked)
Pass Criteria:
- All path traversal attempts blocked
- No sensitive file access
Attack: Inject shell commands via VM/Bastion names
Test Steps:
- Attempt command injection in VM name:
azlin connect "test-vm; rm -rf /" --use-bastion - Expected: Error (invalid VM name format)
- Attempt in Bastion name:
azlin bastion create --name "bastion; curl evil.com" - Expected: Error (invalid Bastion name format)
- Verify no shell=True used:
grep -r "shell=True" src/azlin/ - Expected: No matches (shell=True prohibited)
Pass Criteria:
- All injection attempts blocked
- Input validation prevents shell execution
- No shell=True in codebase
Attack: Reuse expired Azure CLI token
Test Steps:
- Login:
az login - Capture token:
az account get-access-token - Wait for token expiration (or mock expired token)
- Attempt Bastion operation with expired token
- Expected: Error: "Azure CLI token expired. Please re-authenticate: az login"
- Use
az account clearto clear tokens - Attempt Bastion operation
- Expected: Error (no credentials available)
Pass Criteria:
- Expired tokens rejected
- No token caching by azlin
- Re-authentication required
Attack: Access VM without proper RBAC role
Test Steps:
- Create service principal with Reader role only (no VM login role)
- Configure azlin to use this service principal
- Attempt VM connection:
azlin connect test-vm --use-bastion - Expected: Failure at Azure Bastion level (RBAC enforced)
- Attempt to forge RBAC role in config:
[auth.profiles.hacker] roles = ["VM Administrator Login"] # Fake role
- Expected: Ignored (roles not trusted from config)
Pass Criteria:
- RBAC enforced by Azure (not bypassable in azlin)
- Config cannot override Azure RBAC
Attack: Leave orphaned tunnel processes for persistent access
Test Steps:
- Create tunnel:
azlin connect test-vm --use-bastion - From another terminal, kill azlin process:
kill -9 <azlin-pid> - Wait 10 seconds
- Check for orphaned tunnel processes:
ps aux | grep "az network bastion tunnel"
- Expected: No tunnel processes (atexit cleanup worked)
- If orphaned, verify they time out within 5 minutes (Azure limit)
Pass Criteria:
- Tunnel processes cleaned up on hard kill
- No persistent orphaned tunnels
- Azure timeout as fallback
Attack: Inject credentials into logs via error messages
Test Steps:
- Trigger error with credential in input:
azlin connect "vm-name-with-secret-abc123" --use-bastion - Check logs:
cat ~/.azlin/logs/azlin.log - Verify: "secret-abc123" is masked: "secret-[REDACTED]"
- Inject credential in Bastion name:
azlin bastion create --name "bastion-password-12345" - Check logs
- Verify: "password-12345" is masked
Pass Criteria:
- Credentials in inputs sanitized in logs
- Pattern-based sanitization catches variants
Test: Create excessive tunnels to exhaust resources
Test Steps:
- Create 100 tunnels in parallel
- Verify: System remains responsive
- Verify: Azlin enforces reasonable limits (e.g., max 10 concurrent tunnels)
- Attempt to create 11th tunnel
- Expected: Error: "Maximum concurrent tunnels (10) reached. Close existing tunnels."
Pass Criteria:
- Resource limits enforced
- System remains stable
- Clear error when limit reached
Test: Verify tunnel cleanup doesn't hang
Test Steps:
- Create 10 tunnels
- Kill all SSH sessions simultaneously
- Measure: Time for all tunnels to clean up
- Expected: All tunnels cleaned up within 10 seconds
Pass Criteria:
- Fast cleanup (<10 seconds)
- No hung processes
- No resource leaks
Test: Verify CIS control compliance
Test Steps:
- Verify Bastion deployment follows CIS 6.2: "Ensure Azure Bastion is used"
- Verify VM without public IP (CIS recommendation)
- Verify NSG rules allow only necessary traffic
- Generate CIS compliance report (if tooling available)
Pass Criteria:
- CIS controls satisfied
- Documented deviations (if any)
Test: Verify audit logging meets regulatory requirements
Test Steps:
- Enable Azure Activity Log
- Perform Bastion connection
- Query Activity Log after 5 minutes
- Verify: Log entry includes:
- Timestamp (accurate)
- User identity (from Azure AD)
- Resource accessed (VM ID)
- Action performed (SSH connection)
- Result (success/failure)
- Verify logs retained per retention policy
Pass Criteria:
- All required audit fields present
- Logs match regulatory requirements (GDPR, HIPAA, etc.)
Unit Tests:
pytest tests/unit/test_bastion_security.py -vIntegration Tests:
pytest tests/integration/test_bastion_integration.py -vFull Security Suite:
pytest tests/ -k security -vCoverage Report:
pytest --cov=azlin.bastion_manager --cov=azlin.bastion_config --cov-report=htmlBefore marking Bastion feature as security-approved:
- All P0 tests passing
- All P1 tests passing (or documented exceptions)
- Penetration tests completed with no critical findings
- Code review by security-focused engineer
- Documentation reviewed for security guidance
- Compliance tests passing (if applicable)
- Performance tests passing (no DoS vulnerabilities)
- Security regression tests passing
- CI/CD pipeline includes security tests
- Security monitoring configured (Azure Activity Log)
Security Approval: _______________________ Date: _______
Engineering Approval: _______________________ Date: _______
Document any known security limitations or accepted risks here
- BASTION_SECURITY_REQUIREMENTS.md
- Azure Bastion Security Documentation
- OWASP Testing Guide
- CIS Azure Foundations Benchmark
END OF SECURITY TESTING GUIDE