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

Ops.Networking -- Policies, Service Mesh, and Firewalls

"The database is accessible from the internet." -- every security audit, at least one finding.


The Problem

The security audit found three issues:

  1. The PostgreSQL database was accessible from the public internet. Port 5432 was open because the Kubernetes NetworkPolicy was never created. The team assumed the default namespace isolation was enough. It was not -- Kubernetes allows all traffic by default.

  2. mTLS between services was permissive, not strict. The Istio PeerAuthentication policy was copied from a Stack Overflow answer. It used PERMISSIVE mode, which means plaintext traffic was still accepted alongside mTLS. An attacker who gained access to the pod network could intercept traffic without TLS.

  3. The payment gateway egress had no restrictions. The OrderService could make outbound HTTP calls to any destination. If compromised, it could exfiltrate data to any IP address. No egress policy existed because nobody thought to create one.

These are not exotic vulnerabilities. They are configuration oversights. The team knew that database ports should be firewalled, that mTLS should be strict, and that egress should be controlled. They just never wrote the YAML. And nobody checked.

The Networking DSL turns network policies into typed attributes. The source generator emits Kubernetes NetworkPolicies, Istio PeerAuthentication and AuthorizationPolicies, Terraform security groups, and Docker Compose network configurations. The analyzer catches the oversights at compile time.

This DSL operates in two tiers: Container (Docker Compose networks, Toxiproxy for chaos) and Cloud (Kubernetes, Istio, Terraform). No InProcess tier because network topology is inherently about inter-process communication.


IngressRule

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class IngressRuleAttribute : Attribute
{
    public IngressRuleAttribute(string host, string path,
        string backendService, int backendPort) { }

    /// <summary>TLS termination point.</summary>
    public TlsTermination TlsTermination { get; init; }
        = TlsTermination.Edge;

    /// <summary>Rate limit (requests per second). 0 = unlimited.</summary>
    public int RateLimitRps { get; init; } = 0;

    /// <summary>Allowed HTTP methods. Null = all methods.</summary>
    public string[]? AllowedMethods { get; init; }

    /// <summary>Timeout for the backend connection in seconds.</summary>
    public int TimeoutSeconds { get; init; } = 30;

    /// <summary>Maximum request body size (e.g., "10m"). Null = default.</summary>
    public string? MaxBodySize { get; init; }
}

public enum TlsTermination
{
    /// <summary>TLS terminates at the ingress controller.</summary>
    Edge,

    /// <summary>TLS passthrough to the backend.</summary>
    Passthrough,

    /// <summary>Re-encrypt between ingress and backend.</summary>
    ReEncrypt
}

MtlsPolicy

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class MtlsPolicyAttribute : Attribute
{
    public MtlsPolicyAttribute(MtlsMode mode) { }

    /// <summary>Service mesh implementation.</summary>
    public ServiceMesh Mesh { get; init; } = ServiceMesh.Istio;

    /// <summary>Specific port to apply mTLS to. 0 = all ports.</summary>
    public int Port { get; init; } = 0;

    /// <summary>Peers that are excluded from mTLS (legacy services).</summary>
    public string[] ExcludePeers { get; init; } = Array.Empty<string>();
}

public enum MtlsMode
{
    /// <summary>Only mTLS traffic accepted. Plaintext rejected.</summary>
    Strict,

    /// <summary>Both mTLS and plaintext accepted. Migration mode.</summary>
    Permissive,

    /// <summary>mTLS disabled. Not recommended for production.</summary>
    Disabled
}

public enum ServiceMesh
{
    Istio,
    Linkerd,
    ConsulConnect
}

FirewallRule

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class FirewallRuleAttribute : Attribute
{
    public FirewallRuleAttribute(string name) { }

    /// <summary>CIDR ranges to allow (e.g., "10.0.0.0/8", "192.168.1.0/24").</summary>
    public string[] AllowCidrs { get; init; } = Array.Empty<string>();

    /// <summary>CIDR ranges to explicitly deny.</summary>
    public string[] DenyCidrs { get; init; } = Array.Empty<string>();

    /// <summary>Ports to apply the rule to.</summary>
    public int[] Ports { get; init; } = Array.Empty<int>();

    /// <summary>Protocol: TCP, UDP, ICMP, Any.</summary>
    public string Protocol { get; init; } = "TCP";

    /// <summary>Priority (lower number = higher priority).</summary>
    public int Priority { get; init; } = 100;

    /// <summary>Direction: Inbound, Outbound.</summary>
    public string Direction { get; init; } = "Inbound";
}

NetworkPolicy

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class NetworkPolicyAttribute : Attribute
{
    public NetworkPolicyAttribute(string name) { }

    /// <summary>
    /// Pod selector labels that this policy applies to.
    /// Format: "key=value" pairs.
    /// </summary>
    public string[] PodSelector { get; init; } = Array.Empty<string>();

    /// <summary>
    /// Allow ingress from these namespace/pod selectors.
    /// Format: "namespace:label=value" or "pod:label=value".
    /// </summary>
    public string[] AllowFrom { get; init; } = Array.Empty<string>();

    /// <summary>
    /// Allow egress to these namespace/pod selectors.
    /// </summary>
    public string[] AllowTo { get; init; } = Array.Empty<string>();

    /// <summary>
    /// If true, deny all traffic not explicitly allowed.
    /// This is the secure default.
    /// </summary>
    public bool DenyAll { get; init; } = true;

    /// <summary>
    /// Ports to allow in the policy.
    /// </summary>
    public int[] Ports { get; init; } = Array.Empty<int>();
}

EgressRule

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class EgressRuleAttribute : Attribute
{
    public EgressRuleAttribute(string name, string destination, int port) { }

    /// <summary>Protocol: TCP, UDP, Any.</summary>
    public string Protocol { get; init; } = "TCP";

    /// <summary>Description for audit trail.</summary>
    public string? Description { get; init; }

    /// <summary>Whether to log all traffic matching this rule.</summary>
    public bool LogTraffic { get; init; } = false;

    /// <summary>Rate limit outbound connections per second. 0 = unlimited.</summary>
    public int RateLimitCps { get; init; } = 0;
}

ServiceDiscovery

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class ServiceDiscoveryAttribute : Attribute
{
    public ServiceDiscoveryAttribute(string name, string protocol, int port) { }

    /// <summary>Health check path for service mesh health probing.</summary>
    public string? HealthCheckPath { get; init; }

    /// <summary>Load balancing algorithm: RoundRobin, LeastConnections, Random.</summary>
    public string LoadBalancing { get; init; } = "RoundRobin";

    /// <summary>Circuit breaker: max consecutive errors before ejection.</summary>
    public int CircuitBreakerThreshold { get; init; } = 5;

    /// <summary>Retry policy: number of retries on failure.</summary>
    public int Retries { get; init; } = 3;

    /// <summary>Timeout per request in milliseconds.</summary>
    public int TimeoutMs { get; init; } = 5000;
}

Usage: OrderService Networking

// --- Ingress ---

[IngressRule("orders.example.com", "/api/*",
    "order-api", 80,
    TlsTermination = TlsTermination.Edge,
    RateLimitRps = 1000,
    AllowedMethods = new[] { "GET", "POST", "PUT" },
    TimeoutSeconds = 30,
    MaxBodySize = "10m")]

[IngressRule("orders.example.com", "/health",
    "order-api", 80,
    TlsTermination = TlsTermination.Edge,
    AllowedMethods = new[] { "GET" })]

// --- mTLS ---

[MtlsPolicy(MtlsMode.Strict,
    Mesh = ServiceMesh.Istio)]

// --- Firewall ---

[FirewallRule("allow-order-api-ingress",
    AllowCidrs = new[] { "10.0.0.0/8" },
    Ports = new[] { 80, 443 },
    Protocol = "TCP",
    Priority = 100,
    Direction = "Inbound")]

[FirewallRule("restrict-postgres",
    AllowCidrs = new[] { "10.0.1.0/24" },
    DenyCidrs = new[] { "0.0.0.0/0" },
    Ports = new[] { 5432 },
    Protocol = "TCP",
    Priority = 50,
    Direction = "Inbound")]

// --- Network Policy ---

[NetworkPolicy("order-namespace-isolation",
    PodSelector = new[] { "app=order-api" },
    AllowFrom = new[] {
        "namespace:name=ingress-nginx",
        "pod:app=order-worker" },
    AllowTo = new[] {
        "pod:app=order-db",
        "pod:app=order-broker" },
    DenyAll = true,
    Ports = new[] { 80, 5432, 5672 })]

[NetworkPolicy("order-db-isolation",
    PodSelector = new[] { "app=order-db" },
    AllowFrom = new[] {
        "pod:app=order-api",
        "pod:app=order-worker" },
    DenyAll = true,
    Ports = new[] { 5432 })]

// --- Egress ---

[EgressRule("payment-gateway", "payments.stripe.com", 443,
    Protocol = "TCP",
    Description = "Stripe payment processing",
    LogTraffic = true)]

[EgressRule("email-service", "smtp.sendgrid.net", 587,
    Protocol = "TCP",
    Description = "Transactional email via SendGrid")]

// --- Service Discovery ---

[ServiceDiscovery("order-api", "HTTP", 80,
    HealthCheckPath = "/health",
    LoadBalancing = "LeastConnections",
    CircuitBreakerThreshold = 5,
    Retries = 3,
    TimeoutMs = 5000)]

public partial class OrderServiceNetworking { }

Eleven attributes. The complete networking posture for the OrderService. Ingress rules, mutual TLS, firewalls, namespace isolation, egress restrictions, and service mesh registration. All in one class. All compiler-validated.


docker-compose.networks.yaml

# <auto-generated from [NetworkPolicy] + [FirewallRule] attributes>
version: "3.8"

networks:
  order-ingress:
    driver: bridge
    # External-facing network for the API
  order-internal:
    driver: bridge
    internal: true
    # Internal-only: order-api, order-worker, order-db, order-broker
  order-db-isolated:
    driver: bridge
    internal: true
    # Most restricted: only order-api and order-worker can reach order-db

services:
  order-api:
    networks:
      - order-ingress
      - order-internal
  order-db:
    networks:
      - order-db-isolated
  order-worker:
    networks:
      - order-internal
      - order-db-isolated
  order-broker:
    networks:
      - order-internal

Docker Compose networks approximate Kubernetes NetworkPolicies. The internal: true flag prevents external access. The network assignment mirrors the AllowFrom and AllowTo declarations. It is not a perfect simulation -- Docker Compose cannot enforce per-port policies the way Kubernetes can -- but it catches the obvious mistakes: the database on a public network, services that should not talk to each other sharing a network.

Toxiproxy Configuration

For Container-tier chaos testing, the Networking DSL generates Toxiproxy proxies that sit between services:

{
  "proxies": [
    {
      "name": "order-api-to-order-db",
      "listen": "0.0.0.0:15432",
      "upstream": "order-db:5432",
      "enabled": true
    },
    {
      "name": "order-api-to-order-broker",
      "listen": "0.0.0.0:15672",
      "upstream": "order-broker:5672",
      "enabled": true
    },
    {
      "name": "order-api-to-payment-gateway",
      "listen": "0.0.0.0:10443",
      "upstream": "payments.stripe.com:443",
      "enabled": true
    }
  ]
}

The Chaos DSL (Part 10) injects latency, jitter, and connection resets through these proxies. The Networking DSL generates the proxy topology. The two DSLs compose: Networking defines the paths, Chaos defines the faults.


k8s-network-policy.yaml

# <auto-generated from [NetworkPolicy("order-namespace-isolation")] attributes>
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: order-namespace-isolation
  namespace: order-service
spec:
  podSelector:
    matchLabels:
      app: order-api
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              name: ingress-nginx
        - podSelector:
            matchLabels:
              app: order-worker
      ports:
        - protocol: TCP
          port: 80
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: order-db
      ports:
        - protocol: TCP
          port: 5432
    - to:
        - podSelector:
            matchLabels:
              app: order-broker
      ports:
        - protocol: TCP
          port: 5672

---
# <auto-generated from [NetworkPolicy("order-db-isolation")] attributes>
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: order-db-isolation
  namespace: order-service
spec:
  podSelector:
    matchLabels:
      app: order-db
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: order-api
        - podSelector:
            matchLabels:
              app: order-worker
      ports:
        - protocol: TCP
          port: 5432

Two NetworkPolicies. The first isolates the order-api pod: only the ingress controller and order-worker can reach it on port 80, and it can only reach order-db on 5432 and order-broker on 5672. The second isolates the database: only order-api and order-worker can connect on 5432. Everything else is denied by default because DenyAll = true.

This is the YAML that was never written manually. The security audit finding ("database accessible from the internet") becomes impossible because the DenyAll = true default plus explicit AllowFrom declarations mean the database is isolated from the moment the manifest is applied.

istio-peer-authentication.yaml

# <auto-generated from [MtlsPolicy(Strict)] attributes>
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
  name: order-service-mtls
  namespace: order-service
spec:
  selector:
    matchLabels:
      app: order-api
  mtls:
    mode: STRICT

Six lines of YAML. But six lines that were copy-pasted wrong in the Stack Overflow version (using PERMISSIVE instead of STRICT). The attribute says MtlsMode.Strict. The generator emits STRICT. No ambiguity.

istio-authorization-policy.yaml

# <auto-generated from [EgressRule] + [ServiceDiscovery] attributes>
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: order-api-egress-policy
  namespace: order-service
spec:
  selector:
    matchLabels:
      app: order-api
  action: ALLOW
  rules:
    - to:
        - operation:
            hosts:
              - "payments.stripe.com"
            ports:
              - "443"
    - to:
        - operation:
            hosts:
              - "smtp.sendgrid.net"
            ports:
              - "587"
    - to:
        - operation:
            hosts:
              - "order-db.order-service.svc.cluster.local"
            ports:
              - "5432"
    - to:
        - operation:
            hosts:
              - "order-broker.order-service.svc.cluster.local"
            ports:
              - "5672"

The authorization policy is the Istio equivalent of a firewall. Only the declared egress destinations are allowed. If the OrderService is compromised, it cannot call evil-server.attacker.com:443 because that destination is not in the policy. The egress rules from the C# attributes become an allowlist.

terraform/networking/main.tf

# <auto-generated from [FirewallRule] + [NetworkPolicy] attributes>

# --- VPC ---

resource "azurerm_virtual_network" "order_vnet" {
  name                = "order-vnet"
  resource_group_name = var.resource_group_name
  location            = var.location
  address_space       = ["10.0.0.0/16"]

  tags = var.tags
}

resource "azurerm_subnet" "order_api_subnet" {
  name                 = "order-api-subnet"
  resource_group_name  = var.resource_group_name
  virtual_network_name = azurerm_virtual_network.order_vnet.name
  address_prefixes     = ["10.0.1.0/24"]
}

resource "azurerm_subnet" "order_db_subnet" {
  name                 = "order-db-subnet"
  resource_group_name  = var.resource_group_name
  virtual_network_name = azurerm_virtual_network.order_vnet.name
  address_prefixes     = ["10.0.2.0/24"]

  service_endpoints = ["Microsoft.Sql"]

  delegation {
    name = "postgresql-delegation"
    service_delegation {
      name = "Microsoft.DBforPostgreSQL/flexibleServers"
    }
  }
}

# --- Network Security Groups ---

resource "azurerm_network_security_group" "order_api_nsg" {
  name                = "order-api-nsg"
  location            = var.location
  resource_group_name = var.resource_group_name

  security_rule {
    name                       = "allow-order-api-ingress"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_ranges    = ["80", "443"]
    source_address_prefix      = "10.0.0.0/8"
    destination_address_prefix = "*"
  }

  tags = var.tags
}

resource "azurerm_network_security_group" "order_db_nsg" {
  name                = "order-db-nsg"
  location            = var.location
  resource_group_name = var.resource_group_name

  security_rule {
    name                       = "restrict-postgres"
    priority                   = 50
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "5432"
    source_address_prefix      = "10.0.1.0/24"
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "deny-all-postgres"
    priority                   = 200
    direction                  = "Inbound"
    access                     = "Deny"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "5432"
    source_address_prefix      = "0.0.0.0/0"
    destination_address_prefix = "*"
  }

  tags = var.tags
}

# --- Subnet-NSG Associations ---

resource "azurerm_subnet_network_security_group_association" "order_api" {
  subnet_id                 = azurerm_subnet.order_api_subnet.id
  network_security_group_id = azurerm_network_security_group.order_api_nsg.id
}

resource "azurerm_subnet_network_security_group_association" "order_db" {
  subnet_id                 = azurerm_subnet.order_db_subnet.id
  network_security_group_id = azurerm_network_security_group.order_db_nsg.id
}

# --- Variables ---

variable "resource_group_name" { type = string }
variable "location" { type = string }
variable "tags" {
  type    = map(string)
  default = {}
}

The Terraform maps [FirewallRule] attributes to Azure Network Security Groups. The DenyCidrs = new[] { "0.0.0.0/0" } on the PostgreSQL firewall rule becomes an explicit deny rule at priority 200. The AllowCidrs = new[] { "10.0.1.0/24" } becomes an allow rule at priority 50. The database is firewalled from the internet at the network layer, not just the Kubernetes layer.


NET001: Service Exposed Without TLS

error NET001: [IngressRule("orders.example.com", "/api/*", ...)] exposes
service 'order-api' on port 80 but no [CertificateSpec] exists for
'orders.example.com'. All public-facing services must have TLS.

The analyzer cross-references [IngressRule] with [CertificateSpec] from the Infrastructure DSL. If a host is exposed but has no certificate, the build fails. This is a hard error because exposing a service without TLS is a security violation, not a style preference.

NET002: Database Port Without Firewall

error NET002: [ContainerSpec("order-db", "postgres")] exposes port 5432
but no [FirewallRule] restricts access to port 5432. Database ports must
have explicit firewall rules.

The analyzer scans all [ContainerSpec] attributes for known database ports (5432, 3306, 1433, 27017, 6379) and verifies that a corresponding [FirewallRule] exists. This catches the "database accessible from the internet" finding before the security audit.

NET003: Egress to Unknown Destination

warning NET003: [EgressRule("analytics", "analytics.thirdparty.io", 443)]
references an external destination not in the approved egress list. Verify
this destination is authorized for data transfer.

The analyzer maintains an optional approved-destinations list (configurable via an assembly-level attribute). Egress rules pointing to destinations not on the list generate warnings, prompting security review. This does not block the build by default -- it surfaces the question.

NET004: mTLS Disabled in Production

error NET004: [MtlsPolicy(MtlsMode.Disabled)] is set on
'OrderServiceNetworking'. mTLS cannot be disabled in production. Use
MtlsMode.Strict or MtlsMode.Permissive (for migration only).

MtlsMode.Disabled is a valid value for local development and testing. But if the build configuration is Release or the environment tag is production, the analyzer blocks it. Permissive mode generates a warning in production. Only Strict is accepted without diagnostics.


Networking to Infrastructure (Ops.Infrastructure)

The Networking DSL reads ports from [ContainerSpec] attributes to auto-suggest firewall rules:

// Infrastructure DSL declares the container
[ContainerSpec("order-db", "postgres",
    Ports = new[] { "5432:5432" },
    Networks = new[] { "order-net" })]

// Networking DSL validates that a firewall rule exists for port 5432.
// The generator can also auto-generate a default restrictive rule:
// [FirewallRule("auto-restrict-order-db-5432",
//     AllowCidrs = new[] { "10.0.1.0/24" },
//     Ports = new[] { 5432 })]

The generator does not silently create firewall rules. It emits a diagnostic suggesting the rule and provides the attribute to copy-paste. The developer makes the explicit decision. The compiler ensures the decision is made.

Networking to Security (Ops.Security)

The Security DSL (Part 17) defines RBAC roles, authentication policies, and secret management. The Networking DSL complements it:

// Security DSL defines who can access the service
[RbacRole("order-reader", Permissions = new[] { "orders:read" })]

// Networking DSL defines how traffic reaches the service
[IngressRule("orders.example.com", "/api/*", "order-api", 80)]
[MtlsPolicy(MtlsMode.Strict)]

// Together: authenticated users with order-reader role, over mTLS,
// through the ingress controller, to the order-api service.
// Security handles identity. Networking handles transport.

Networking to Chaos (Ops.Chaos)

The Chaos DSL (Part 10) injects network faults. At the Container tier, it uses Toxiproxy. The Networking DSL generates the Toxiproxy topology:

// Networking DSL defines the service connections
[EgressRule("payment-gateway", "payments.stripe.com", 443)]

// The generator produces a Toxiproxy proxy for this connection.
// The Chaos DSL adds toxics to the proxy:
[ChaosExperiment(typeof(OrderService), "payment-latency",
    SteadyState = "p99 < 500ms",
    Toxic = "latency", ToxicArgs = "latency=2000")]

// Container tier: Toxiproxy adds 2000ms latency to the
// payment gateway proxy. The test verifies p99 stays under 500ms
// (it will not -- that is the point of the experiment).

The networking topology feeds the chaos topology. Without the Networking DSL, the Chaos DSL would need to redeclare every service connection. With it, the connections are discovered from attributes and the proxies are generated automatically.


The Default-Deny Principle

The most important property of the Networking DSL is the default. Kubernetes, by itself, allows all traffic. Istio in permissive mode allows all traffic. Cloud security groups with no rules allow all traffic. The default is "open."

The Networking DSL inverts this. The NetworkPolicy attribute has DenyAll = true as the default. If you declare a policy, everything not explicitly allowed is denied. If you forget to declare a policy, the analyzer fires (NET002 for database ports, NET001 for public services).

This is not defense in depth. This is defense by default. The developer does not need to remember to add a firewall rule. The compiler reminds them. The developer does not need to verify mTLS is strict. The analyzer verifies it.

The security audit finding ("the database is accessible from the internet") becomes a build error:

error NET002: [ContainerSpec("order-db", "postgres")] exposes port 5432
but no [FirewallRule] restricts access to port 5432.

The finding is caught at compile time, not at audit time. The fix is an attribute, not a JIRA ticket. The verification is continuous, not annual.

⬇ Download