Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

The Problem

Security is the operational concern most likely to be "planned for later" and never implemented.

RBAC drift. The authorization rules start in a spreadsheet titled "Role Permissions Matrix v3 FINAL (2).xlsx." A developer implements them in code. Six months later, the spreadsheet and the code disagree. A new role was added to the spreadsheet but not to the code. A permission was removed from the code but not from the spreadsheet. The penetration test finds that the Viewer role can delete orders because the [Authorize(Roles = "Admin")] attribute was copied from a different controller and nobody changed the role.

Audit logging gaps. The compliance team asks: "Can you show me who accessed patient records in the last 90 days?" The answer is no. The audit logging was supposed to be added in sprint 14. It is now sprint 38. The // TODO: add audit logging comment is still there.

Secret rotation. The database connection string in production uses a password that was set when the service was first deployed. That was two years ago. The password is Prod2024! and it is in three places: Azure Key Vault, an environment variable on the App Service, and a sticky note on the DevOps engineer's monitor. Nobody has rotated it because the rotation process is manual and undocumented.

Vulnerability scanning. The container image has 47 critical CVEs. The team knows because someone ran Trivy once, four months ago. There is no recurring scan. The DAST scan was run during the initial security review and never again.

What is missing:

  • RBAC rules in code. Every resource should declare who can access it. The compiler should verify that every public endpoint has an authorization rule.
  • Audit policies attached to entities. If an entity contains sensitive data, the audit policy should be declared on the entity, not in a separate middleware configuration file.
  • Secret rotation as a first-class concept. Rotation period, vault path, and expiry notification should be declared and enforced at build time.
  • Vulnerability scanning schedules in the same codebase. Scan type, severity threshold, and schedule should be attributes, not CI pipeline YAML that drifts from the code.

Attribute Definitions

// =================================================================
// Ops.Security.Lib -- Security DSL Attributes
// =================================================================

/// Security domain classification.
public enum SecurityDomain
{
    Auth,               // authentication and authorization
    DataProtection,     // encryption, PII handling
    Network,            // TLS, firewall, CORS
    Audit,              // logging, compliance trails
    Secrets             // vault, rotation, injection
}

/// Top-level security policy container.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class SecurityPolicyAttribute : Attribute
{
    public string Name { get; }
    public SecurityDomain Domain { get; }
    public string Description { get; init; } = "";
    public string Owner { get; init; } = "";
    public string ComplianceFramework { get; init; } = "";

    public SecurityPolicyAttribute(string name, SecurityDomain domain)
    {
        Name = name;
        Domain = domain;
    }
}

/// Per-resource authorization rule. Maps to an ASP.NET AuthorizationPolicy.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public sealed class RbacRuleAttribute : Attribute
{
    public string Role { get; }
    public string[] Permissions { get; }
    public string Resource { get; init; } = "";
    public string Condition { get; init; } = "";
    public bool DenyByDefault { get; init; } = true;

    public RbacRuleAttribute(string role, params string[] permissions)
    {
        Role = role;
        Permissions = permissions;
    }
}

/// Audit level for logging operations.
public enum AuditLevel
{
    Read,       // log read operations
    Write,      // log create/update/delete
    Admin,      // log administrative actions
    All         // log everything
}

/// Declares an audit policy for an entity or endpoint.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public sealed class AuditPolicyAttribute : Attribute
{
    public AuditLevel Level { get; }
    public int RetentionDays { get; init; } = 365;
    public string[] SensitiveFields { get; init; } = [];
    public bool IncludePayload { get; init; } = false;
    public string Destination { get; init; } = "default";

    public AuditPolicyAttribute(AuditLevel level) => Level = level;
}

/// Secret rotation policy.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class SecretRotationAttribute : Attribute
{
    public string Name { get; }
    public string VaultPath { get; }
    public int RotationPeriodDays { get; init; } = 90;
    public int NotifyBeforeDays { get; init; } = 14;
    public string[] NotifyChannels { get; init; } = [];
    public bool AutoRotate { get; init; } = false;

    public SecretRotationAttribute(string name, string vaultPath)
    {
        Name = name;
        VaultPath = vaultPath;
    }
}

/// Vulnerability scan kind.
public enum ScanKind
{
    SAST,           // static application security testing (source code)
    DAST,           // dynamic application security testing (running app)
    SCA,            // software composition analysis (dependencies)
    ContainerScan   // container image CVE scanning
}

/// Vulnerability scan schedule and thresholds.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class VulnerabilityScanAttribute : Attribute
{
    public ScanKind Kind { get; }
    public string MaxSeverity { get; init; } = "HIGH";
    public string Schedule { get; init; } = "0 2 * * *";
    public string[] ExcludePatterns { get; init; } = [];
    public bool FailOnViolation { get; init; } = true;

    public VulnerabilityScanAttribute(ScanKind kind) => Kind = kind;
}

/// Penetration test scope definition.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class PenetrationTestScopeAttribute : Attribute
{
    public string[] Endpoints { get; }
    public string Schedule { get; init; } = "quarterly";
    public string Methodology { get; init; } = "OWASP";
    public string[] ExcludeEndpoints { get; init; } = [];
    public string ReportFormat { get; init; } = "PDF";

    public PenetrationTestScopeAttribute(params string[] endpoints)
    {
        Endpoints = endpoints;
    }
}

Usage: Order Service Security Contract

[OpsTarget("order-service")]

// ── Security Policies ─────────────────────────────────────────
[SecurityPolicy("order-auth", SecurityDomain.Auth,
    Description = "Authorization rules for order management",
    ComplianceFramework = "SOC2")]
[SecurityPolicy("order-audit", SecurityDomain.Audit,
    Description = "Audit trail for order operations",
    Owner = "compliance-team")]
[SecurityPolicy("order-secrets", SecurityDomain.Secrets,
    Description = "Secret rotation for order service dependencies")]

// ── Secret Rotation ───────────────────────────────────────────
[SecretRotation("order-db-password", "kv/order-service/db-password",
    RotationPeriodDays = 90,
    NotifyBeforeDays = 14,
    NotifyChannels = ["#order-oncall", "security@company.com"],
    AutoRotate = true)]
[SecretRotation("payment-api-key", "kv/order-service/payment-api-key",
    RotationPeriodDays = 60,
    NotifyBeforeDays = 7,
    NotifyChannels = ["#order-oncall"])]
[SecretRotation("jwt-signing-key", "kv/order-service/jwt-signing-key",
    RotationPeriodDays = 180,
    NotifyBeforeDays = 30)]

// ── Vulnerability Scanning ────────────────────────────────────
[VulnerabilityScan(ScanKind.ContainerScan,
    MaxSeverity = "HIGH",
    Schedule = "0 2 * * *",
    FailOnViolation = true)]
[VulnerabilityScan(ScanKind.SCA,
    MaxSeverity = "CRITICAL",
    Schedule = "0 3 * * 1",
    ExcludePatterns = ["test/**"])]
[VulnerabilityScan(ScanKind.DAST,
    MaxSeverity = "MEDIUM",
    Schedule = "0 4 * * 0")]

// ── Penetration Test Scope ────────────────────────────────────
[PenetrationTestScope(
    "POST /api/orders", "GET /api/orders/{id}", "PUT /api/orders/{id}/status",
    "POST /api/orders/{id}/payment", "DELETE /api/orders/{id}",
    Schedule = "quarterly",
    Methodology = "OWASP",
    ExcludeEndpoints = ["/health", "/ready"])]

public partial class OrderSecurityContract { }

Per-Endpoint RBAC

public partial class OrderSecurityContract
{
    [RbacRule("Admin", "orders.create", "orders.read", "orders.update", "orders.delete",
        Resource = "Order")]
    [RbacRule("OrderManager", "orders.create", "orders.read", "orders.update",
        Resource = "Order")]
    [RbacRule("Viewer", "orders.read",
        Resource = "Order")]
    [AuditPolicy(AuditLevel.Write,
        RetentionDays = 730,
        SensitiveFields = ["PaymentCardLast4", "CustomerEmail"],
        IncludePayload = true)]
    public partial void OrderEndpoints();

    [RbacRule("Admin", "orders.payment.process", "orders.payment.refund",
        Resource = "Payment")]
    [RbacRule("PaymentProcessor", "orders.payment.process",
        Resource = "Payment",
        Condition = "order.Status == OrderStatus.Confirmed")]
    [AuditPolicy(AuditLevel.All,
        RetentionDays = 2555,  // 7 years for financial records
        SensitiveFields = ["CardNumber", "CVV", "BillingAddress"],
        IncludePayload = false)]  // never log payment payloads
    public partial void PaymentEndpoints();
}

Three-tier projection

[SecurityPolicy], [RbacRule], and [AuditPolicy] declarations project across all three tiers, enforcing the same access rules at three different layers. Local runs get ASP.NET authorization policies and audit middleware (application-layer authz). Container runs add scanning (Trivy/ZAP) and a hardening overlay that locks down what the container process can do (capabilities, filesystem, syscalls). Cloud runs add Kubernetes RBAC that locks down what the workload identity can do via the K8s API.

The three layers are complementary, not duplicates. ASP.NET enforces "which user can call which endpoint." Container hardening enforces "what the running process can touch on the host." K8s RBAC enforces "which pods can kubectl get pods on the namespace." A defense-in-depth posture needs all three.

Diagram
The security attributes drive a defense-in-depth projection: ASP.NET authorization and audit middleware locally, Trivy/ZAP scans and hardened compose overlays in containers, and K8s RBAC with secret-expiry alerts in the cloud.

Local Tier: AuthorizationPolicies.g.cs

The generator produces ASP.NET authorization policy registration from [RbacRule] attributes.

// <auto-generated by Ops.Security.Generator />
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;

public static class OrderAuthorizationPolicies
{
    public static IServiceCollection AddOrderAuthorizationPolicies(
        this IServiceCollection services)
    {
        services.AddAuthorizationBuilder()

            // Order resource policies
            .AddPolicy("Order.Create", policy =>
                policy.RequireRole("Admin", "OrderManager")
                      .RequireClaim("permission", "orders.create"))

            .AddPolicy("Order.Read", policy =>
                policy.RequireRole("Admin", "OrderManager", "Viewer")
                      .RequireClaim("permission", "orders.read"))

            .AddPolicy("Order.Update", policy =>
                policy.RequireRole("Admin", "OrderManager")
                      .RequireClaim("permission", "orders.update"))

            .AddPolicy("Order.Delete", policy =>
                policy.RequireRole("Admin")
                      .RequireClaim("permission", "orders.delete"))

            // Payment resource policies
            .AddPolicy("Payment.Process", policy =>
                policy.RequireRole("Admin", "PaymentProcessor")
                      .RequireClaim("permission", "orders.payment.process")
                      .AddRequirements(new OrderStatusRequirement(OrderStatus.Confirmed)))

            .AddPolicy("Payment.Refund", policy =>
                policy.RequireRole("Admin")
                      .RequireClaim("permission", "orders.payment.refund"));

        // Register conditional requirement handler
        services.AddSingleton<IAuthorizationHandler, OrderStatusRequirementHandler>();

        return services;
    }
}

/// Conditional authorization: PaymentProcessor can only process
/// payments for orders in Confirmed status.
public sealed class OrderStatusRequirement : IAuthorizationRequirement
{
    public OrderStatus RequiredStatus { get; }
    public OrderStatusRequirement(OrderStatus status) => RequiredStatus = status;
}

public sealed class OrderStatusRequirementHandler
    : AuthorizationHandler<OrderStatusRequirement>
{
    private readonly IOrderRepository _orders;

    public OrderStatusRequirementHandler(IOrderRepository orders)
        => _orders = orders;

    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        OrderStatusRequirement requirement)
    {
        if (context.Resource is HttpContext httpContext
            && httpContext.GetRouteValue("id") is string orderId)
        {
            var order = await _orders.GetByIdAsync(orderId);
            if (order?.Status == requirement.RequiredStatus)
            {
                context.Succeed(requirement);
            }
        }
    }
}

AuditMiddleware.g.cs

Structured audit logging middleware generated from [AuditPolicy] attributes. Sensitive fields are automatically redacted.

// <auto-generated by Ops.Security.Generator />
using System.Text.Json;
using Microsoft.AspNetCore.Http;

public sealed class OrderAuditMiddleware : IMiddleware
{
    private readonly IAuditLogger _auditLogger;
    private readonly TimeProvider _timeProvider;

    private static readonly Dictionary<string, AuditConfig> s_auditConfigs = new()
    {
        ["/api/orders"] = new(
            Level: AuditLevel.Write,
            RetentionDays: 730,
            SensitiveFields: ["PaymentCardLast4", "CustomerEmail"],
            IncludePayload: true),

        ["/api/orders/{id}/payment"] = new(
            Level: AuditLevel.All,
            RetentionDays: 2555,
            SensitiveFields: ["CardNumber", "CVV", "BillingAddress"],
            IncludePayload: false),
    };

    public OrderAuditMiddleware(IAuditLogger auditLogger, TimeProvider timeProvider)
    {
        _auditLogger = auditLogger;
        _timeProvider = timeProvider;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var path = context.Request.Path.Value ?? "";
        var config = ResolveConfig(path);

        if (config is null || !ShouldAudit(config, context.Request.Method))
        {
            await next(context);
            return;
        }

        var entry = new AuditEntry
        {
            Timestamp = _timeProvider.GetUtcNow(),
            UserId = context.User.FindFirst("sub")?.Value ?? "anonymous",
            UserName = context.User.Identity?.Name ?? "unknown",
            Action = $"{context.Request.Method} {path}",
            Resource = ExtractResource(path),
            IpAddress = context.Connection.RemoteIpAddress?.ToString() ?? "",
            UserAgent = context.Request.Headers.UserAgent.ToString(),
            CorrelationId = context.TraceIdentifier,
        };

        // Capture request payload (if configured and not sensitive)
        if (config.IncludePayload)
        {
            context.Request.EnableBuffering();
            using var reader = new StreamReader(context.Request.Body, leaveOpen: true);
            var body = await reader.ReadToEndAsync();
            context.Request.Body.Position = 0;
            entry.RequestPayload = RedactSensitiveFields(body, config.SensitiveFields);
        }

        await next(context);

        entry.StatusCode = context.Response.StatusCode;
        entry.Success = context.Response.StatusCode < 400;
        entry.RetentionDays = config.RetentionDays;

        await _auditLogger.LogAsync(entry);
    }

    private static string RedactSensitiveFields(string json, string[] sensitiveFields)
    {
        if (sensitiveFields.Length == 0) return json;

        try
        {
            var doc = JsonDocument.Parse(json);
            var redacted = new Dictionary<string, object?>();

            foreach (var prop in doc.RootElement.EnumerateObject())
            {
                redacted[prop.Name] = sensitiveFields.Contains(prop.Name, StringComparer.OrdinalIgnoreCase)
                    ? "***REDACTED***"
                    : prop.Value.Clone();
            }

            return JsonSerializer.Serialize(redacted);
        }
        catch
        {
            return "***UNPARSEABLE***";
        }
    }

    private static bool ShouldAudit(AuditConfig config, string method) => config.Level switch
    {
        AuditLevel.All => true,
        AuditLevel.Write => method is "POST" or "PUT" or "PATCH" or "DELETE",
        AuditLevel.Read => method is "GET",
        AuditLevel.Admin => false, // resolved via role check
        _ => false,
    };

    private static AuditConfig? ResolveConfig(string path)
    {
        // Exact match first, then pattern match for path parameters
        foreach (var (pattern, config) in s_auditConfigs)
        {
            if (PathMatches(path, pattern)) return config;
        }
        return null;
    }

    private static bool PathMatches(string path, string pattern)
    {
        // Simplified route matching — real implementation uses RoutePattern
        var patternSegments = pattern.Split('/');
        var pathSegments = path.Split('/');
        if (patternSegments.Length != pathSegments.Length) return false;

        for (int i = 0; i < patternSegments.Length; i++)
        {
            if (patternSegments[i].StartsWith('{')) continue; // path parameter
            if (patternSegments[i] != pathSegments[i]) return false;
        }
        return true;
    }

    private static string ExtractResource(string path)
    {
        var segments = path.Trim('/').Split('/');
        return segments.Length >= 2 ? segments[1] : "unknown";
    }
}

public sealed record AuditConfig(
    AuditLevel Level,
    int RetentionDays,
    string[] SensitiveFields,
    bool IncludePayload);

public sealed class AuditEntry
{
    public DateTimeOffset Timestamp { get; set; }
    public string UserId { get; set; } = "";
    public string UserName { get; set; } = "";
    public string Action { get; set; } = "";
    public string Resource { get; set; } = "";
    public string IpAddress { get; set; } = "";
    public string UserAgent { get; set; } = "";
    public string CorrelationId { get; set; } = "";
    public string? RequestPayload { get; set; }
    public int StatusCode { get; set; }
    public bool Success { get; set; }
    public int RetentionDays { get; set; }
}

SecretRotationService.g.cs

An IHostedService that monitors secret expiry and sends notifications before rotation is due.

// <auto-generated by Ops.Security.Generator />
using Microsoft.Extensions.Hosting;

public sealed class OrderSecretRotationService : BackgroundService
{
    private readonly ISecretVaultClient _vault;
    private readonly INotificationService _notifications;
    private readonly ILogger<OrderSecretRotationService> _logger;
    private readonly TimeProvider _timeProvider;

    private static readonly SecretRotationConfig[] s_secrets =
    [
        new("order-db-password", "kv/order-service/db-password",
            RotationPeriodDays: 90, NotifyBeforeDays: 14,
            Channels: ["#order-oncall", "security@company.com"],
            AutoRotate: true),
        new("payment-api-key", "kv/order-service/payment-api-key",
            RotationPeriodDays: 60, NotifyBeforeDays: 7,
            Channels: ["#order-oncall"],
            AutoRotate: false),
        new("jwt-signing-key", "kv/order-service/jwt-signing-key",
            RotationPeriodDays: 180, NotifyBeforeDays: 30,
            Channels: [],
            AutoRotate: false),
    ];

    public OrderSecretRotationService(
        ISecretVaultClient vault,
        INotificationService notifications,
        ILogger<OrderSecretRotationService> logger,
        TimeProvider timeProvider)
    {
        _vault = vault;
        _notifications = notifications;
        _logger = logger;
        _timeProvider = timeProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            foreach (var secret in s_secrets)
            {
                try
                {
                    var metadata = await _vault.GetSecretMetadataAsync(secret.VaultPath, ct);
                    var age = _timeProvider.GetUtcNow() - metadata.CreatedAt;
                    var daysUntilRotation = secret.RotationPeriodDays - age.Days;

                    if (daysUntilRotation <= 0)
                    {
                        _logger.LogCritical(
                            "Secret {Name} is OVERDUE for rotation by {Days} days",
                            secret.Name, Math.Abs(daysUntilRotation));

                        if (secret.AutoRotate)
                        {
                            await _vault.RotateSecretAsync(secret.VaultPath, ct);
                            _logger.LogInformation("Secret {Name} auto-rotated", secret.Name);
                        }

                        await NotifyAsync(secret,
                            $"Secret '{secret.Name}' is overdue for rotation by " +
                            $"{Math.Abs(daysUntilRotation)} days", ct);
                    }
                    else if (daysUntilRotation <= secret.NotifyBeforeDays)
                    {
                        _logger.LogWarning(
                            "Secret {Name} rotation due in {Days} days",
                            secret.Name, daysUntilRotation);

                        await NotifyAsync(secret,
                            $"Secret '{secret.Name}' rotation due in {daysUntilRotation} days", ct);
                    }
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Failed to check secret {Name}", secret.Name);
                }
            }

            await Task.Delay(TimeSpan.FromHours(6), ct);
        }
    }

    private async Task NotifyAsync(SecretRotationConfig secret, string message, CancellationToken ct)
    {
        foreach (var channel in secret.Channels)
        {
            await _notifications.SendAsync(channel, message, ct);
        }
    }
}

public sealed record SecretRotationConfig(
    string Name,
    string VaultPath,
    int RotationPeriodDays,
    int NotifyBeforeDays,
    string[] Channels,
    bool AutoRotate);

Container Tier: trivy-config.yaml

# Auto-generated by Ops.Security.Generator
# Source: OrderSecurityContract

# Trivy container scan configuration
severity:
  - CRITICAL
  - HIGH

exit-code: 1      # fail CI on violation
ignore-unfixed: true
timeout: 10m

# Scan schedule: daily at 02:00 UTC
# Implemented via CI pipeline cron trigger

vulnerability:
  type:
    - os
    - library

secret:
  config: trivy-secret.yaml

Container Tier: zap-scan-config.yaml

# Auto-generated by Ops.Security.Generator
# Source: OrderSecurityContract (DAST scan)

env:
  contexts:
    - name: order-service
      urls:
        - http://order-api:5000
      includePaths:
        - "http://order-api:5000/api/orders.*"
      excludePaths:
        - "http://order-api:5000/health"
        - "http://order-api:5000/ready"
      authentication:
        method: json
        parameters:
          loginUrl: "http://order-api:5000/api/auth/token"
          loginRequestData: '{"username":"dast-scanner","password":"${DAST_PASSWORD}"}'

  parameters:
    failOnRisk: Medium
    progressToStdout: true

  jobs:
    - type: spider
      parameters:
        context: order-service
        maxDuration: 5

    - type: activeScan
      parameters:
        context: order-service
        maxRuleDurationInMins: 10
        policy: Default Policy

    - type: report
      parameters:
        template: modern
        reportDir: /zap/reports
        reportFile: order-service-dast.html
      risks:
        - High
        - Medium
        - Low

Container Tier: docker-compose.security.yaml (hardening overlay)

The scanners above (trivy-config.yaml, zap-scan-config.yaml) detect vulnerabilities. They do not prevent a compromised process from doing damage. The hardening overlay does -- it drops every Linux capability the container doesn't strictly need, makes the root filesystem read-only, runs as a non-root user, and forbids privilege escalation. Generated from [SecurityPolicy] declarations on the Order service.

# <auto-generated by Ops.Security.Generator />
# docker-compose.security.yaml -- Hardening overlay for OrderSecurityContract
# Usage: docker compose -f docker-compose.ops.yaml -f docker-compose.security.yaml up

services:
  order-api:
    user: "1000:1000"                  # non-root, matches the Dockerfile USER directive
    read_only: true                    # rootfs is immutable
    tmpfs:
      - /tmp:size=64M,mode=1777        # writable scratch dir for ASP.NET temp files
      - /var/run:size=8M
    cap_drop:
      - ALL                            # drop every capability
    cap_add: []                        # add none back -- order-api doesn't need any
    security_opt:
      - no-new-privileges:true         # forbid privilege escalation via setuid binaries
      - seccomp=./seccomp-profile.json
      - apparmor=order-service-profile
    pids_limit: 200                    # process explosion guard
    ulimits:
      nofile:
        soft: 4096
        hard: 8192
    sysctls:
      net.ipv4.ip_unprivileged_port_start: 0
    labels:
      ops.security/policy: order-service-strict
      ops.security/source: "OrderSecurityContract.cs"

The same hardening flags translate 1:1 to the K8s Pod.spec.securityContext block in the Deployment manifest at the Cloud tier -- one source declaration, two enforcement formats.

Container Tier: seccomp-profile.json

Syscall allow-list generated from [SecurityPolicy]. The Order API needs only the syscalls a normal ASP.NET Core process makes (no mount, no ptrace, no kexec_load). Anything outside the allow-list returns EPERM.

{
  "$comment": "Auto-generated by Ops.Security.Generator from [SecurityPolicy] on OrderSecurityContract",
  "defaultAction": "SCMP_ACT_ERRNO",
  "defaultErrnoRet": 1,
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_X32"
  ],
  "syscalls": [
    {
      "names": [
        "accept", "accept4", "bind", "brk", "close", "connect", "dup", "dup2",
        "epoll_create1", "epoll_ctl", "epoll_wait", "execve", "exit", "exit_group",
        "fcntl", "fstat", "fsync", "futex", "getcwd", "getdents64", "getpid",
        "getppid", "getrandom", "gettid", "ioctl", "listen", "lseek", "madvise",
        "mmap", "mprotect", "munmap", "nanosleep", "open", "openat", "poll", "ppoll",
        "pread64", "pwrite64", "read", "readv", "recvfrom", "recvmsg", "rt_sigaction",
        "rt_sigprocmask", "rt_sigreturn", "sched_yield", "sendfile", "sendmsg",
        "sendto", "set_robust_list", "setsockopt", "socket", "socketpair", "stat",
        "statx", "sysinfo", "tgkill", "uname", "wait4", "write", "writev"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

A future analyzer (SEC005) will diff this list against new syscalls observed by strace during integration tests, alerting when the application starts needing capabilities the security model didn't grant.

Container Tier: apparmor-profile

# <auto-generated by Ops.Security.Generator />
# AppArmor profile for order-api container
# Loaded via the docker-compose.security.yaml overlay

#include <tunables/global>

profile order-service-profile flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>
  #include <abstractions/nameservice>

  # Read-only access to the application binaries
  /app/** r,
  /app/OrderApi mr,                    # mmap+read for the .NET binary

  # Writable scratch dirs (matches the tmpfs mounts in the compose overlay)
  /tmp/** rwk,
  /var/run/** rw,

  # Network access (the API listens on a single port)
  network inet stream,
  network inet6 stream,

  # Deny everything dangerous
  deny /etc/shadow r,
  deny /root/** rwklx,
  deny /proc/sys/** w,
  deny /sys/** w,
  deny mount,
  deny ptrace,
  deny capability sys_admin,
  deny capability sys_module,
  deny capability sys_ptrace,
}

Cloud Tier: Penetration Test Scope and Scan Schedules

# Auto-generated by Ops.Security.Generator
# terraform/security/order-scans/main.tf

# Container vulnerability scanning (daily)
resource "azurerm_container_registry_task" "trivy_scan" {
  name                  = "order-service-trivy-daily"
  container_registry_id = var.acr_id
  platform {
    os = "Linux"
  }

  timer_trigger {
    name     = "daily-scan"
    schedule = "0 2 * * *"
  }

  docker_step {
    dockerfile_path = "Dockerfile.trivy"
    context_path    = "https://github.com/org/order-service.git#main"
    arguments = {
      severity  = "HIGH,CRITICAL"
      exit_code = "1"
    }
  }
}

# Secret rotation monitoring
resource "azurerm_monitor_scheduled_query_rules_alert_v2" "secret_expiry" {
  name                = "order-secrets-expiry-warning"
  resource_group_name = var.resource_group_name
  location            = var.location
  scopes              = [var.log_analytics_workspace_id]

  criteria {
    query = <<-QUERY
      AppTraces
      | where AppRoleName == "order-service"
      | where Message contains "rotation due" or Message contains "OVERDUE"
      | where TimeGenerated > ago(1h)
      | summarize count() by Message
    QUERY

    time_aggregation_method = "Count"
    operator                = "GreaterThan"
    threshold               = 0
  }

  action {
    action_groups = [var.security_action_group_id]
  }
}

Cloud Tier: k8s/serviceaccount.yaml

For Kubernetes the same [SecurityPolicy] principal becomes a ServiceAccount. One SA per principal, annotated with the audit-policy retention class so auditors can correlate cluster-API access with application audit logs.

# <auto-generated by Ops.Security.Generator />
# k8s/serviceaccount.yaml -- Workload identity for OrderSecurityContract
# Source: [SecurityPolicy] attributes on OrderSecurityContract

apiVersion: v1
kind: ServiceAccount
metadata:
  name: order-service
  namespace: orders
  labels:
    app.kubernetes.io/name: order-service
    app.kubernetes.io/managed-by: ops.security.generator
  annotations:
    # Audit retention class -- correlates with [AuditPolicy(RetentionDays = 2555)]
    ops.security/audit-retention-days: "2555"
    ops.security/principal: order-service
    ops.security/source: "OrderSecurityContract.cs"
    # Azure Workload Identity binding (cluster-specific)
    azure.workload.identity/client-id: "${ORDER_SERVICE_AZURE_CLIENT_ID}"
automountServiceAccountToken: true
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: order-payment-processor
  namespace: orders
  labels:
    app.kubernetes.io/name: order-payment-processor
    app.kubernetes.io/managed-by: ops.security.generator
  annotations:
    ops.security/audit-retention-days: "2555"   # 7 years for financial records
    ops.security/principal: payment-processor
    ops.security/source: "OrderSecurityContract.cs"
automountServiceAccountToken: true

Cloud Tier: k8s/role.yaml

A Role per [RbacRule]. The permission strings (Order.Create, Order.Read, Payment.Process) are the exact values from AuthorizationPolicies.g.cs above -- the C# layer and the K8s layer share one source of truth. Rename a permission in the DSL declaration and both layers update on the next build.

# <auto-generated by Ops.Security.Generator />
# k8s/role.yaml -- Cluster-API permissions for OrderSecurityContract
# Source: [RbacRule] attributes on OrderSecurityContract

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: order-service-role
  namespace: orders
  labels:
    app.kubernetes.io/name: order-service
    app.kubernetes.io/managed-by: ops.security.generator
  annotations:
    # Permission names mirror ASP.NET policy names from AuthorizationPolicies.g.cs
    ops.security/policies: "Order.Create,Order.Read,Order.Update,Order.Delete"
rules:
  # Read its own ConfigMap (the one Configuration DSL emits at the Cloud tier)
  - apiGroups: [""]
    resources: ["configmaps"]
    resourceNames: ["order-service-config"]
    verbs: ["get", "watch"]

  # Read its own ExternalSecret material (External Secrets Operator stores it as v1 Secret)
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames: ["order-service-secrets"]
    verbs: ["get", "watch"]

  # Read its own Pods for self-discovery (used by Kestrel for graceful shutdown coordination)
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: order-payment-processor-role
  namespace: orders
  labels:
    app.kubernetes.io/name: order-payment-processor
    app.kubernetes.io/managed-by: ops.security.generator
  annotations:
    ops.security/policies: "Payment.Process,Payment.Refund"
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames: ["payment-gateway-secrets"]
    verbs: ["get", "watch"]

Cloud Tier: k8s/rolebinding.yaml

RoleBinding wires the ServiceAccounts to the Roles. The group names (Admin, OrderManager, PaymentProcessor) are the exact same identifiers used in the RequireRole(...) calls in AuthorizationPolicies.g.cs. The application-layer authz and the cluster-layer authz speak the same vocabulary.

# <auto-generated by Ops.Security.Generator />
# k8s/rolebinding.yaml -- ServiceAccount → Role bindings
# Source: [SecurityPolicy] + [RbacRule] on OrderSecurityContract

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: order-service-binding
  namespace: orders
  labels:
    app.kubernetes.io/name: order-service
    app.kubernetes.io/managed-by: ops.security.generator
  annotations:
    # Subject role names mirror RequireRole(...) calls in AuthorizationPolicies.g.cs
    ops.security/aspnet-roles: "Admin,OrderManager,Viewer"
subjects:
  - kind: ServiceAccount
    name: order-service
    namespace: orders
roleRef:
  kind: Role
  name: order-service-role
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: order-payment-processor-binding
  namespace: orders
  labels:
    app.kubernetes.io/name: order-payment-processor
    app.kubernetes.io/managed-by: ops.security.generator
  annotations:
    ops.security/aspnet-roles: "Admin,PaymentProcessor"
subjects:
  - kind: ServiceAccount
    name: order-payment-processor
    namespace: orders
roleRef:
  kind: Role
  name: order-payment-processor-role
  apiGroup: rbac.authorization.k8s.io

Why both layers? ASP.NET policies enforce application-layer authz: which authenticated user can call POST /api/orders. Kubernetes RBAC enforces cluster-layer authz: which workload identity can kubectl get configmaps in the orders namespace. They are complementary, not duplicates. A defense-in-depth posture needs both: a compromised application token shouldn't unlock the cluster API, and a compromised cluster credential shouldn't bypass the application's row-level security. Same applies to the Container hardening overlay vs the K8s RBAC: the overlay locks down what the container process can do at the OS level; the RBAC locks down what the workload identity can do at the K8s API level.

Generated RBAC Unit Tests

The generator also produces a test class that verifies every [RbacRule] is actually enforced:

// <auto-generated by Ops.Security.Generator />
public class OrderRbacTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public OrderRbacTests(WebApplicationFactory<Program> factory)
        => _factory = factory;

    [Theory]
    [InlineData("Admin", "POST", "/api/orders", 201)]
    [InlineData("OrderManager", "POST", "/api/orders", 201)]
    [InlineData("Viewer", "POST", "/api/orders", 403)]
    [InlineData("Admin", "GET", "/api/orders/1", 200)]
    [InlineData("Viewer", "GET", "/api/orders/1", 200)]
    [InlineData("Admin", "DELETE", "/api/orders/1", 200)]
    [InlineData("OrderManager", "DELETE", "/api/orders/1", 403)]
    [InlineData("Viewer", "DELETE", "/api/orders/1", 403)]
    public async Task Rbac_Enforcement(
        string role, string method, string path, int expectedStatus)
    {
        var client = _factory.CreateClient();
        client.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", TestTokens.ForRole(role));

        var request = new HttpRequestMessage(new HttpMethod(method), path);
        if (method is "POST" or "PUT")
        {
            request.Content = JsonContent.Create(OrderTestData.MinimalPayload);
        }

        var response = await client.SendAsync(request);

        Assert.Equal((HttpStatusCode)expectedStatus, response.StatusCode);
    }

    [Fact]
    public async Task Anonymous_Access_Denied_On_All_Endpoints()
    {
        var client = _factory.CreateClient();
        // No Authorization header

        var endpoints = new[]
        {
            ("POST", "/api/orders"),
            ("GET", "/api/orders/1"),
            ("PUT", "/api/orders/1/status"),
            ("DELETE", "/api/orders/1"),
            ("POST", "/api/orders/1/payment"),
        };

        foreach (var (method, path) in endpoints)
        {
            var request = new HttpRequestMessage(new HttpMethod(method), path);
            var response = await client.SendAsync(request);

            Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
        }
    }
}

Analyzer Diagnostics

ID Severity Rule Example
SEC001 Error Entity with sensitive data but no AuditPolicy Class Patient has [SensitiveData("SSN")] attribute but no [AuditPolicy]. All access to sensitive data must be audited.
SEC002 Warning Secret without rotation policy [Secret("db-password")] in Configuration DSL has no matching [SecretRotation] in Security DSL. Secrets must have rotation schedules.
SEC003 Error Public endpoint without RbacRule Controller action DeleteOrder has [HttpDelete] and is not marked [AllowAnonymous], but has no [RbacRule] or [Authorize] attribute. Default-deny means this endpoint is inaccessible — is that intentional?
SEC004 Warning DAST scan referencing nonexistent endpoint [PenetrationTestScope] includes /api/products but no controller exposes that path. The scan will report false 404s.
SEC005 Warning Audit retention below compliance minimum [AuditPolicy(RetentionDays = 90)] on a financial entity. SOC2 requires 365 days minimum for financial records.
SEC006 Info RbacRule role not found in identity provider [RbacRule("SuperAdmin", ...)] but the identity provider configuration does not include role SuperAdmin. The policy will never match.

SEC003 is the most important. It catches the "forgot to add authorization" bug at compile time. Every public endpoint must either have an [RbacRule], an [Authorize] attribute, or an explicit [AllowAnonymous]. No silent defaults.


Security to DataGovernance

Entities marked with [AuditPolicy(SensitiveFields = ["SSN", "Email"])] must have corresponding GDPR data maps in the DataGovernance DSL. The analyzer verifies:

// Security declares sensitive fields
[AuditPolicy(AuditLevel.All, SensitiveFields = ["SSN", "DateOfBirth"])]
public class PatientRecord { }

// DataGovernance must have a matching data map — SEC007 fires if missing
[DataMap(typeof(PatientRecord),
    Classification = DataClassification.PII,
    LawfulBasis = "Consent",
    RetentionDays = 2555)]

Security to Configuration

Every [SecretRotation] references a vault path. The Configuration DSL's [Secret] attribute must declare the same vault path. The analyzer verifies they agree:

// Configuration DSL declares the secret exists
[Secret("order-db-password", Vault = "azure-keyvault",
    VaultPath = "kv/order-service/db-password")]

// Security DSL declares the rotation policy
[SecretRotation("order-db-password", "kv/order-service/db-password",
    RotationPeriodDays = 90)]

If the vault paths differ, SEC008 fires. If a [Secret] has no matching [SecretRotation], SEC002 fires.

Security to Workflow

The [RbacRule] Role values integrate with the Workflow DSL's RequiresRole concept. If a workflow transition requires the OrderManager role, the Security DSL verifies that the role exists in the RBAC configuration:

// Workflow DSL declares role requirement
[WorkflowTransition("Confirmed", "Shipped", RequiresRole = "OrderManager")]

// Security DSL has the role defined in [RbacRule]
[RbacRule("OrderManager", "orders.update", Resource = "Order")]

If the workflow references a role that no [RbacRule] defines, the analyzer flags the inconsistency: the workflow transition will always fail because the authorization policy does not exist.

Security to Chaos

Security policies should be chaos-tested. Can the system handle an authentication provider outage? Is the audit logger resilient to its own storage failing? The Chaos DSL can target security infrastructure:

[ChaosExperiment("auth-provider-timeout", Tier = OpsExecutionTier.Container,
    Hypothesis = "When the identity provider times out, the service returns 503 " +
                 "and does not fall back to allowing unauthenticated access")]
[TargetService(typeof(IAuthenticationService))]
[FaultInjection(FaultKind.Timeout, DelayMs = 30_000)]
[SteadyStateProbe("unauthenticated_access_count", 0, Operator = "==")]

This tests the most dangerous security failure mode: a timeout that silently bypasses authentication. The steady-state probe verifies that zero unauthenticated requests succeed during the outage.

⬇ Download