Skip to content

BehaviorModeling mode does not parse AppArmor mount events - missing mount data in ArmorProfileModel #307

@Danny-Wei

Description

@Danny-Wei

Problem Statement

vArmor's BehaviorModeling mode fails to collect mount operation data from AppArmor audit events. This creates an inconsistency where:

  • BPF enforcer: Correctly captures and stores mount events in BPF.Mounts
  • AppArmor enforcer: Mount events are parsed but discarded as "unknown" operations

This gap prevents users from obtaining complete behavior models when using AppArmor-based policies and breaks policy-advisor recommendations for mount-related rules.

Root Cause Analysis

1. Missing Operation Type Recognition

In internal/behavior/preprocessor/apparmor.go, the opType() function (lines 170-217) does not recognize mount-related AppArmor operations:

// Current opType() only handles these operations:
// - file_*, inode_*, create, bind, connect, listen, accept, etc.
// - Missing: mount, umount, remount operations

When AppArmor emits a mount operation event, it falls through to the unknown category and gets logged in Unhandled events instead of being processed as a mount operation.

2. Missing Mount Data Structure

In apis/varmor/v1beta1/armorprofilemodel_types.go:

  • BPF struct (line 79-86): Has Mounts []Mount field
  • AppArmor struct (line 62-71): Missing Mounts field entirely
type BPF struct {
    // ... other fields ...
    Mounts       []Mount  `json:"mounts,omitempty"`  // <-- BPF has this
}

type AppArmor struct {
    Profiles     []string `json:"profiles,omitempty"`
    Executions   []string `json:"executions,omitempty"`
    Files        []File   `json:"files,omitempty"`
    Capabilities []string `json:"capabilities,omitempty"`
    Network      *Network `json:"networks,omitempty"`
    Ptraces      []Ptrace `json:"ptraces,omitempty"`
    Signals      []Signal `json:"signals,omitempty"`
    Unhandled    []string `json:"unhandled,omitempty"`
    // Missing: Mounts []Mount
}

3. Missing Event Parsing Logic

In internal/behavior/preprocessor/apparmor.go:parseAppArmorEventForTree(), there are dedicated handlers for:

  • exec operations (line 242-251)
  • file operations (line 256-337)
  • capable operations (line 340-345)
  • net operations (line 348-369)
  • ptrace operations (line 373-399)
  • signal operations (line 402-434)

Missing: No handler for mount, umount, or remount operations.

4. Policy-Advisor Cannot Use Mount Data

In tools/policy-advisor/policy-advisor.py, the behavior data retrieval functions like retrieve_executions_from_behavior_data(), retrieve_files_from_behavior_data(), etc., cannot retrieve mount operations from AppArmor behavior data because it's never stored in the first place.

Impact

1. Incomplete Behavior Models

Users relying on AppArmor-based BehaviorModeling cannot capture mount operation patterns, leading to incomplete security profiles.

2. Policy-Advisor Recommendations Are Incomplete

The policy-advisor tool cannot make informed decisions about mount-related built-in rules (e.g., disallow-mount, disallow-umount) when analyzing AppArmor behavior data.

Example rules that cannot be properly advised:

  • disallow-mount - Cannot determine if app needs mount operations
  • disallow-umount - Cannot determine if app needs umount operations

3. DefenseInDepth Mode Limitations

When generating allowlist profiles for DefenseInDepth mode, mount operations are not included in the generated AppArmor profiles, potentially breaking applications that require mount operations.

Environment

  • vArmor version: v0.7.0+
  • Kubernetes version: Any supported version
  • Node OS: Linux with AppArmor enabled
  • Enforcer: AppArmor (with or without BPF)

Proposed Fix

1. Add Mounts field to AppArmor struct

File: apis/varmor/v1beta1/armorprofilemodel_types.go

Add Mounts []Mount field to the AppArmor struct:

type AppArmor struct {
    Profiles     []string `json:"profiles,omitempty"`
    Executions   []string `json:"executions,omitempty"`
    Files        []File   `json:"files,omitempty"`
    Capabilities []string `json:"capabilities,omitempty"`
    Network      *Network `json:"networks,omitempty"`
    Ptraces      []Ptrace `json:"ptraces,omitempty"`
    Signals      []Signal `json:"signals,omitempty"`
    Mounts       []Mount  `json:"mounts,omitempty"`  // <-- ADD THIS
    Unhandled    []string `json:"unhandled,omitempty"`
}

2. Add mount operation recognition to opType()

File: internal/behavior/preprocessor/apparmor.go

Update the opType() function to recognize mount operations:

func (p *DataPreprocessor) opType(event *AaLogRecord) string {
    if strings.HasPrefix(event.Operation, "file_") ||
        strings.HasPrefix(event.Operation, "inode_") ||
        // ... existing operations ...
        event.Operation == "getattr" ||
        event.Operation == "setattr" ||
        event.Operation == "xattr" {
        // ... existing logic for file/network operations ...
    }
    
    // Add mount operation recognition
    if event.Operation == "mount" || 
        event.Operation == "umount" || 
        event.Operation == "remount" {
        return "mount"
    }
    
    return "unknown"
}

3. Add mount event handling in parseAppArmorEventForTree()

File: internal/behavior/preprocessor/apparmor.go

Add a handler for mount operations after the existing signal handler (around line 434):

// Mount
if opType == "mount" {
    if event.Name == "" || event.DeniedMask == "" {
        return nil
    }
    
    // Parse mount flags from DeniedMask or Info fields
    flags := parseMountFlags(event)
    
    // Determine mount type from event info
    mountType := parseMountType(event)
    
    for i, mount := range p.behaviorData.DynamicResult.AppArmor.Mounts {
        if mount.Path == event.Name {
            // Merge flags
            for _, flag := range flags {
                if !varmorutils.InStringArray(flag, mount.Flags) {
                    p.behaviorData.DynamicResult.AppArmor.Mounts[i].Flags = append(
                        p.behaviorData.DynamicResult.AppArmor.Mounts[i].Flags, flag)
                }
            }
            return nil
        }
    }
    
    mount := varmor.Mount{
        Path:  event.Name,
        Type:  mountType,
        Flags: flags,
    }
    p.behaviorData.DynamicResult.AppArmor.Mounts = append(
        p.behaviorData.DynamicResult.AppArmor.Mounts, mount)
    return nil
}

4. Update policy-advisor to use mount data

File: tools/policy-advisor/policy-advisor.py

Add a function to retrieve mount operations from behavior data:

def retrieve_mounts_from_behavior_data(behavior_data):
    mounts = []
    if not behavior_data:
        return mounts
    
    # Check for BPF mount data
    if "data" in behavior_data and "dynamicResult" in behavior_data["data"]:
        dynamic_result = behavior_data["data"]["dynamicResult"]
        
        # Get mount data from BPF
        if "bpf" in dynamic_result and "mounts" in dynamic_result["bpf"]:
            for mount in dynamic_result["bpf"]["mounts"]:
                mounts.append({
                    "path": mount.get("path", ""),
                    "type": mount.get("type", ""),
                    "flags": mount.get("flags", [])
                })
        
        # Get mount data from AppArmor (after the fix)
        if "appArmor" in dynamic_result and "mounts" in dynamic_result["appArmor"]:
            for mount in dynamic_result["appArmor"]["mounts"]:
                mounts.append({
                    "path": mount.get("path", ""),
                    "type": mount.get("type", ""),
                    "flags": mount.get("flags", [])
                })
    
    return mounts

Then update the skip_the_rule_with_behavior_data() function to check for mount conflicts:

def skip_the_rule_with_behavior_data(rule, enforcers, behavior_data):
    if not has_common_item(enforcers, rule["enforcers"]):
        return True
    if "conflicts" in rule:
        # ... existing checks for capabilities, syscalls, executions, files ...
        
        # Add check for mount operations
        if "mounts" in rule["conflicts"]:
            mounts = retrieve_mounts_from_behavior_data(behavior_data)
            # Check if any mount operation conflicts with the rule
            for conflict_mount in rule["conflicts"]["mounts"]:
                for app_mount in mounts:
                    if mount_matches_conflict(app_mount, conflict_mount):
                        return True
    return False

5. Update profile generation for DefenseInDepth mode

File: internal/profile/apparmor/apparmor.go and related files

Update the profile generation logic to include mount rules from AppArmor behavior data:

// In the profile generation function, add:
if data.AppArmor != nil && len(data.AppArmor.Mounts) > 0 {
    for _, mount := range data.AppArmor.Mounts {
        // Generate mount rules for the AppArmor profile
        rule := generateMountRule(mount)
        profile.Rules = append(profile.Rules, rule)
    }
}

6. Add mount-related fields to the Mount struct (if needed)

If the current Mount struct doesn't support all necessary fields for AppArmor mount events, consider extending it:

type Mount struct {
    Path       string   `json:"path"`
    Type       string   `json:"type,omitempty"`
    Flags      []string `json:"flags,omitempty"`
    // Optional additional fields for AppArmor-specific mount info
    Source     string   `json:"source,omitempty"`  // Source device/path
    Data       string   `json:"data,omitempty"`    // Mount options/data
}

Workaround

Until this issue is fixed, users who need mount operation data in their behavior models can:

  1. Use BPF enforcer alongside AppArmor: The BPF enforcer does capture mount events, so using enforcer: AppArmorBPF will ensure mount data is available via the BPF behavior data:
spec:
  policy:
    enforcer: AppArmorBPF  # BPF will capture mount events
    mode: BehaviorModeling
  1. Manually analyze audit logs: For critical applications requiring mount operation analysis, users can manually parse AppArmor audit logs from /var/log/audit/audit.log or /var/log/kern.log:
# Search for mount-related AppArmor events
grep 'apparmor.*mount' /var/log/audit/audit.log
grep 'apparmor.*mount' /var/log/kern.log
  1. Use a sidecar for mount monitoring: Deploy a privileged sidecar container that monitors mount operations using tools like auditctl or bpftrace:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-with-mount-monitor
spec:
  template:
    spec:
      containers:
      - name: main-app
        image: myapp:latest
      - name: mount-monitor
        image: alpine:latest
        securityContext:
          privileged: true
        command:
        - /bin/sh
        - -c
        - |
          # Monitor mount syscalls
          auditctl -a always,exit -F arch=b64 -S mount -S umount -S umount2 -k mount_ops
          tail -f /var/log/audit/audit.log | grep mount_ops

Testing

To verify the fix works correctly:

  1. Unit tests: Add test cases in internal/behavior/preprocessor/apparmor_test.go to verify mount events are parsed correctly:
func Test_parseAppArmorEventForTree_Mount(t *testing.T) {
    testCases := []struct {
        name          string
        event         *AaLogRecord
        expectedMount varmor.Mount
    }{
        {
            name: "simple tmpfs mount",
            event: &AaLogRecord{
                Operation: "mount",
                Name:      "/tmp/testmount",
                DeniedMask: "rw",
                // ... other fields
            },
            expectedMount: varmor.Mount{
                Path:  "/tmp/testmount",
                Type:  "tmpfs",
                Flags: []string{"rw"},
            },
        },
        // ... more test cases
    }
    
    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            p := NewDataPreprocessor(...)
            err := p.parseAppArmorEventForTree(tc.event)
            assert.NilError(t, err)
            
            // Verify mount was added to AppArmor.Mounts
            found := false
            for _, mount := range p.behaviorData.DynamicResult.AppArmor.Mounts {
                if mount.Path == tc.expectedMount.Path {
                    found = true
                    assert.DeepEqual(t, mount.Type, tc.expectedMount.Type)
                    assert.DeepEqual(t, mount.Flags, tc.expectedMount.Flags)
                }
            }
            assert.Assert(t, found, "Expected mount not found in AppArmor.Mounts")
        })
    }
}
  1. Integration test: Create an end-to-end test that:
    • Deploys a workload that performs mount operations
    • Enables BehaviorModeling with AppArmor enforcer
    • Verifies mount data appears in ArmorProfileModel
func TestBehaviorModeling_MountEvents_AppArmor(t *testing.T) {
    // Setup test environment
    ctx := context.Background()
    
    // Create VarmorPolicy with BehaviorModeling mode
    policy := &varmor.VarmorPolicy{
        ObjectMeta: metav1.ObjectMeta{
            Name:      "test-mount-modeling",
            Namespace: "default",
        },
        Spec: varmor.VarmorPolicySpec{
            Target: varmor.Target{
                Kind: "Deployment",
                Selector: &metav1.LabelSelector{
                    MatchLabels: map[string]string{
                        "app": "mount-test",
                    },
                },
            },
            Policy: varmor.Policy{
                Enforcer: "AppArmor",
                Mode:     "BehaviorModeling",
                ModelingOptions: &varmor.ModelingOptions{
                    Duration: 5, // 5 minutes
                },
            },
        },
    }
    
    // Create the policy
    err := k8sClient.Create(ctx, policy)
    require.NoError(t, err)
    defer k8sClient.Delete(ctx, policy)
    
    // Wait for modeling to complete
    var profileName string
    require.Eventually(t, func() bool {
        var currentPolicy varmor.VarmorPolicy
        err := k8sClient.Get(ctx, types.NamespacedName{Name: policy.Name, Namespace: policy.Namespace}, &currentPolicy)
        if err != nil {
            return false
        }
        // Check if modeling completed
        for _, condition := range currentPolicy.Status.Conditions {
            if condition.Type == "ModelingCompleted" && condition.Status == "True" {
                profileName = currentPolicy.Status.ProfileName
                return true
            }
        }
        return false
    }, 10*time.Minute, 10*time.Second, "Modeling should complete within 10 minutes")
    
    // Get the ArmorProfileModel
    var apm varmor.ArmorProfileModel
    err = k8sClient.Get(ctx, types.NamespacedName{Name: profileName, Namespace: "varmor"}, &apm)
    require.NoError(t, err)
    
    // Verify mount data is present in AppArmor behavior data
    require.NotNil(t, apm.Data.DynamicResult.AppArmor, "AppArmor behavior data should exist")
    require.NotEmpty(t, apm.Data.DynamicResult.AppArmor.Mounts, "AppArmor mount data should not be empty")
    
    // Log the mount data for debugging
    t.Logf("Found %d mount operations:", len(apm.Data.DynamicResult.AppArmor.Mounts))
    for i, mount := range apm.Data.DynamicResult.AppArmor.Mounts {
        t.Logf("  Mount %d: path=%s, type=%s, flags=%v", i+1, mount.Path, mount.Type, mount.Flags)
    }
}
  1. Manual verification: After implementing the fix, manually verify that:
    • AppArmor mount audit events are correctly parsed
    • Mount data appears in ArmorProfileModel
    • Policy-advisor can make mount-related recommendations
# 1. Deploy a workload that performs mounts
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mount-test-app
  labels:
    app: mount-test-app
    sandbox.varmor.org/enable: "true"
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mount-test-app
  template:
    metadata:
      labels:
        app: mount-test-app
    spec:
      containers:
      - name: app
        image: alpine:latest
        command:
        - /bin/sh
        - -c
        - |
          # Create and mount a tmpfs
          mkdir -p /mnt/test
          mount -t tmpfs -o size=10m tmpfs /mnt/test
          echo "Mount successful" > /mnt/test/status
          # Keep running
          while true; do
            sleep 10
          done
        securityContext:
          privileged: true
EOF

# 2. Create a BehaviorModeling policy
kubectl apply -f - <<EOF
apiVersion: crd.varmor.org/v1beta1
kind: VarmorPolicy
metadata:
  name: test-mount-behavior
  namespace: default
spec:
  target:
    kind: Deployment
    selector:
      matchLabels:
        app: mount-test-app
  policy:
    enforcer: AppArmor
    mode: BehaviorModeling
    modelingOptions:
      duration: 5
EOF

# 3. Wait for modeling to complete
kubectl wait --for=condition=Ready vpol/test-mount-behavior --timeout=600s

# 4. Check the ArmorProfileModel for mount data
PROFILE_NAME=$(kubectl get vpol test-mount-behavior -o jsonpath='{.status.profileName}')
echo "=== Checking for mount data in ArmorProfileModel ==="
kubectl get ArmorProfileModel -n varmor $PROFILE_NAME -o json | jq '.data.dynamicResult.appArmor.mounts'

# 5. Check if mount data is present (should show mount entries after the fix)
if kubectl get ArmorProfileModel -n varmor $PROFILE_NAME -o json | jq -e '.data.dynamicResult.appArmor.mounts | length > 0' > /dev/null 2>&1; then
    echo "✓ SUCCESS: Mount data is present in ArmorProfileModel"
    kubectl get ArmorProfileModel -n varmor $PROFILE_NAME -o json | jq '.data.dynamicResult.appArmor.mounts'
else
    echo "✗ ISSUE: Mount data is missing from ArmorProfileModel"
    echo "  This indicates AppArmor mount events are not being parsed correctly"
fi

# 6. Export behavior data for policy-advisor testing
kubectl get ArmorProfileModel -n varmor $PROFILE_NAME -o json > /tmp/test_behavior_data.json

# 7. Test with policy-advisor
echo ""
echo "=== Testing policy-advisor with behavior data ==="
python3 tools/policy-advisor/policy-advisor.py AppArmor -m /tmp/test_behavior_data.json 2>&1 | head -50

Related Issues

Additional Context

This issue was identified while analyzing the behavior modeling data flow. The BPF enforcer correctly handles mount events (as seen in internal/behavior/preprocessor/bpf.go lines 129-147), but AppArmor audit events containing mount operations are not being processed similarly.

The AppArmor audit events for mount operations typically look like:

type=AVC msg=audit(1735689600.123:456): apparmor=\"ALLOWED\" operation=\"mount\" info=\"fstype=tmpfs\" name=\"/mnt/test\" pid=12345 comm=\"mount\" requested_mask=\"mount\" denied_mask=\"mount\"

These events contain all necessary information (path, filesystem type, flags) to populate the Mount struct, but the current implementation doesn't recognize the mount operation.

Severity

Medium-High

This issue affects:

  • Completeness of behavior models for AppArmor-based policies
  • Accuracy of policy-advisor recommendations
  • Completeness of DefenseInDepth profiles

While not causing crashes or security vulnerabilities directly, it significantly impacts the utility of the BehaviorModeling feature for real-world applications that perform mount operations.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions