Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Multi-Tenancy Guide

This guide explains how to set up multi-tenancy with Bindy using the dual-cluster model, RBAC configuration, and namespace isolation.

Table of Contents

Overview

Bindy supports multi-tenancy through two complementary approaches:

  1. Platform-Managed DNS: Centralized DNS infrastructure managed by platform teams
  2. Tenant-Managed DNS: Isolated DNS infrastructure managed by development teams

Both can coexist in the same cluster, providing flexibility for different organizational needs.

Key Principles

  • Namespace Isolation: DNSZones and records are always namespace-scoped
  • RBAC-Based Access: Kubernetes RBAC controls who can manage DNS resources
  • Cluster Model Flexibility: Choose namespace-scoped or cluster-scoped clusters based on needs
  • No Cross-Namespace Access: Records cannot reference zones in other namespaces

Tenancy Models

Model 1: Platform-Managed DNS

Use Case: Platform team provides shared DNS infrastructure for all applications.

graph TB
    subgraph "Platform Team ClusterRole"
        PlatformAdmin[Platform Admin]
    end

    subgraph "Cluster-Scoped Resources"
        GlobalCluster[Bind9GlobalCluster<br/>production-dns]
    end

    subgraph "Application Team A Namespace"
        ZoneA[DNSZone<br/>app-a.example.com]
        RecordsA[DNS Records]
    end

    subgraph "Application Team B Namespace"
        ZoneB[DNSZone<br/>app-b.example.com]
        RecordsB[DNS Records]
    end

    PlatformAdmin -->|manages| GlobalCluster
    GlobalCluster -.globalClusterRef.-> ZoneA
    GlobalCluster -.globalClusterRef.-> ZoneB
    ZoneA --> RecordsA
    ZoneB --> RecordsB

    style PlatformAdmin fill:#ff9800
    style GlobalCluster fill:#c8e6c9
    style ZoneA fill:#fff4e1
    style ZoneB fill:#fff4e1

Characteristics:

  • Platform team manages Bind9GlobalCluster (requires ClusterRole)
  • Application teams manage DNSZone and records in their namespace (requires Role)
  • Shared DNS infrastructure, distributed zone management
  • Suitable for production workloads

Model 2: Tenant-Managed DNS

Use Case: Development teams run isolated DNS infrastructure for testing/dev.

graph TB
    subgraph "Team A Namespace + Role"
        AdminA[Team A Admin]
        ClusterA[Bind9Cluster<br/>team-a-dns]
        ZoneA[DNSZone<br/>dev-a.local]
        RecordsA[DNS Records]
    end

    subgraph "Team B Namespace + Role"
        AdminB[Team B Admin]
        ClusterB[Bind9Cluster<br/>team-b-dns]
        ZoneB[DNSZone<br/>dev-b.local]
        RecordsB[DNS Records]
    end

    AdminA -->|manages| ClusterA
    AdminA -->|manages| ZoneA
    AdminA -->|manages| RecordsA
    ClusterA --> ZoneA
    ZoneA --> RecordsA

    AdminB -->|manages| ClusterB
    AdminB -->|manages| ZoneB
    AdminB -->|manages| RecordsB
    ClusterB --> ZoneB
    ZoneB --> RecordsB

    style AdminA fill:#2196f3
    style AdminB fill:#2196f3
    style ClusterA fill:#e1f5ff
    style ClusterB fill:#e1f5ff

Characteristics:

  • Each team manages their own Bind9Cluster (namespace-scoped Role)
  • Complete isolation between teams
  • Teams have full autonomy over DNS configuration
  • Suitable for development/testing environments

Platform Team Setup

Step 1: Create ClusterRole for Platform DNS Management

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: platform-dns-admin
rules:
# Manage cluster-scoped global clusters
- apiGroups: ["bindy.firestoned.io"]
  resources: ["bind9globalclusters"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

# View global cluster status
- apiGroups: ["bindy.firestoned.io"]
  resources: ["bind9globalclusters/status"]
  verbs: ["get", "list", "watch"]

# Manage bind9 instances across all namespaces (for global clusters)
- apiGroups: ["bindy.firestoned.io"]
  resources: ["bind9instances"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

# View instance status
- apiGroups: ["bindy.firestoned.io"]
  resources: ["bind9instances/status"]
  verbs: ["get", "list", "watch"]

Step 2: Bind ClusterRole to Platform Team

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: platform-team-dns-admin
subjects:
- kind: Group
  name: platform-team  # Your IdP/OIDC group name
  apiGroup: rbac.authorization.k8s.io
# Alternative: Bind to specific users
# - kind: User
#   name: alice@example.com
#   apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: platform-dns-admin
  apiGroup: rbac.authorization.k8s.io

Step 3: Create Bind9GlobalCluster

apiVersion: bindy.firestoned.io/v1alpha1
kind: Bind9GlobalCluster
metadata:
  name: shared-production-dns
  # No namespace - cluster-scoped
spec:
  version: "9.18"

  # Primary instances configuration
  primary:
    replicas: 3
    service:
      type: LoadBalancer
      annotations:
        service.beta.kubernetes.io/aws-load-balancer-type: "nlb"

  # Secondary instances configuration
  secondary:
    replicas: 2

  # Global BIND9 configuration
  global:
    options:
      - "recursion no"
      - "allow-transfer { none; }"
      - "notify yes"

  # Access control lists
  acls:
    trusted:
      - "10.0.0.0/8"
      - "172.16.0.0/12"

Step 4: Grant Application Teams DNS Zone Management

Create a Role in each application namespace:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: dns-zone-admin
  namespace: app-team-a
rules:
# Manage DNS zones and records
- apiGroups: ["bindy.firestoned.io"]
  resources:
    - "dnszones"
    - "arecords"
    - "aaaarecords"
    - "cnamerecords"
    - "mxrecords"
    - "txtrecords"
    - "nsrecords"
    - "srvrecords"
    - "caarecords"
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

# View resource status
- apiGroups: ["bindy.firestoned.io"]
  resources:
    - "dnszones/status"
    - "arecords/status"
    - "cnamerecords/status"
    - "mxrecords/status"
    - "txtrecords/status"
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: app-team-a-dns
  namespace: app-team-a
subjects:
- kind: Group
  name: app-team-a
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: dns-zone-admin
  apiGroup: rbac.authorization.k8s.io

Step 5: Application Teams Create DNSZones

Application teams can now create zones in their namespace:

apiVersion: bindy.firestoned.io/v1alpha1
kind: DNSZone
metadata:
  name: app-a-zone
  namespace: app-team-a
spec:
  zoneName: app-a.example.com
  globalClusterRef: shared-production-dns  # References platform cluster
  soaRecord:
    primaryNs: ns1.example.com.
    adminEmail: dns-admin.example.com.
    serial: 2025010101
    refresh: 3600
    retry: 600
    expire: 604800
    negativeTtl: 86400
  ttl: 3600

Development Team Setup

Step 1: Create Namespace for Team

apiVersion: v1
kind: Namespace
metadata:
  name: dev-team-alpha
  labels:
    team: dev-team-alpha
    environment: development

Step 2: Create Role for Full DNS Management

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: dns-full-admin
  namespace: dev-team-alpha
rules:
# Manage namespace-scoped clusters
- apiGroups: ["bindy.firestoned.io"]
  resources: ["bind9clusters"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

# Manage instances
- apiGroups: ["bindy.firestoned.io"]
  resources: ["bind9instances"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

# Manage zones and records
- apiGroups: ["bindy.firestoned.io"]
  resources:
    - "dnszones"
    - "arecords"
    - "aaaarecords"
    - "cnamerecords"
    - "mxrecords"
    - "txtrecords"
    - "nsrecords"
    - "srvrecords"
    - "caarecords"
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

# View status for all resources
- apiGroups: ["bindy.firestoned.io"]
  resources:
    - "bind9clusters/status"
    - "bind9instances/status"
    - "dnszones/status"
    - "arecords/status"
  verbs: ["get", "list", "watch"]

Step 3: Bind Role to Development Team

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: dev-team-alpha-dns
  namespace: dev-team-alpha
subjects:
- kind: Group
  name: dev-team-alpha
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: dns-full-admin
  apiGroup: rbac.authorization.k8s.io

Step 4: Development Team Creates Infrastructure

# Namespace-scoped cluster
apiVersion: bindy.firestoned.io/v1alpha1
kind: Bind9Cluster
metadata:
  name: dev-dns
  namespace: dev-team-alpha
spec:
  version: "9.18"
  primary:
    replicas: 1
  secondary:
    replicas: 1
---
# DNS zone referencing namespace-scoped cluster
apiVersion: bindy.firestoned.io/v1alpha1
kind: DNSZone
metadata:
  name: dev-zone
  namespace: dev-team-alpha
spec:
  zoneName: dev.local
  clusterRef: dev-dns  # References namespace-scoped cluster
  soaRecord:
    primaryNs: ns1.dev.local.
    adminEmail: admin.dev.local.
    serial: 2025010101
    refresh: 3600
    retry: 600
    expire: 604800
    negativeTtl: 300
  ttl: 300
---
# DNS record
apiVersion: bindy.firestoned.io/v1alpha1
kind: ARecord
metadata:
  name: test-server
  namespace: dev-team-alpha
spec:
  zoneRef: dev-zone
  name: test-server
  ipv4Address: "10.244.1.100"
  ttl: 60

RBAC Configuration

ClusterRole vs Role Decision Matrix

ResourceScopeRBAC TypeWho Gets It
Bind9GlobalClusterCluster-scopedClusterRole + ClusterRoleBindingPlatform team
Bind9ClusterNamespace-scopedRole + RoleBindingDevelopment teams
Bind9InstanceNamespace-scopedRole + RoleBindingTeams managing instances
DNSZoneNamespace-scopedRole + RoleBindingApplication teams
DNS RecordsNamespace-scopedRole + RoleBindingApplication teams

Example RBAC Hierarchy

graph TD
    subgraph "Cluster-Level RBAC"
        CR1[ClusterRole:<br/>platform-dns-admin]
        CRB1[ClusterRoleBinding:<br/>platform-team]
    end

    subgraph "Namespace-Level RBAC"
        R1[Role: dns-full-admin<br/>namespace: dev-team-alpha]
        RB1[RoleBinding: dev-team-alpha-dns]

        R2[Role: dns-zone-admin<br/>namespace: app-team-a]
        RB2[RoleBinding: app-team-a-dns]
    end

    CR1 --> CRB1
    R1 --> RB1
    R2 --> RB2

    CRB1 -.->|grants| PlatformTeam[platform-team group]
    RB1 -.->|grants| DevTeam[dev-team-alpha group]
    RB2 -.->|grants| AppTeam[app-team-a group]

    style CR1 fill:#ffccbc
    style R1 fill:#c5e1a5
    style R2 fill:#c5e1a5

Minimal Permissions for Application Teams

If application teams only need to manage DNS records (not clusters):

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: dns-record-editor
  namespace: app-team-a
rules:
# Only manage DNS zones and records
- apiGroups: ["bindy.firestoned.io"]
  resources:
    - "dnszones"
    - "arecords"
    - "cnamerecords"
    - "mxrecords"
    - "txtrecords"
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

# Read-only access to status
- apiGroups: ["bindy.firestoned.io"]
  resources:
    - "dnszones/status"
    - "arecords/status"
  verbs: ["get", "list", "watch"]

Security Best Practices

1. Namespace Isolation

Enforce strict namespace boundaries:

  • Records cannot reference zones in other namespaces
  • This is enforced by the controller using Api::namespaced()
  • No configuration needed - isolation is automatic
# team-a namespace
apiVersion: bindy.firestoned.io/v1alpha1
kind: ARecord
metadata:
  name: www
  namespace: team-a
spec:
  zoneRef: team-a-zone  # ✅ Same namespace
  name: www
  ipv4Address: "192.0.2.1"
---
# This FAILS - cross-namespace reference blocked
apiVersion: bindy.firestoned.io/v1alpha1
kind: ARecord
metadata:
  name: www
  namespace: team-a
spec:
  zoneRef: team-b-zone  # ❌ Different namespace - BLOCKED
  name: www
  ipv4Address: "192.0.2.1"

2. Least Privilege RBAC

Grant minimum necessary permissions:

# ✅ GOOD - Specific permissions
rules:
- apiGroups: ["bindy.firestoned.io"]
  resources: ["dnszones", "arecords"]
  verbs: ["get", "list", "create", "update"]

# ❌ BAD - Overly broad permissions
rules:
- apiGroups: ["bindy.firestoned.io"]
  resources: ["*"]
  verbs: ["*"]

3. Separate Platform and Tenant Roles

Keep platform and tenant permissions separate:

Role TypeManagesScope
Platform DNS AdminBind9GlobalClusterCluster-wide
Tenant Cluster AdminBind9Cluster, Bind9InstanceNamespace
Tenant Zone AdminDNSZone, RecordsNamespace
Tenant Record EditorRecords onlyNamespace

4. Audit and Monitoring

Enable audit logging for DNS changes:

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Log all changes to Bindy resources
- level: RequestResponse
  resources:
  - group: bindy.firestoned.io
    resources:
    - bind9globalclusters
    - bind9clusters
    - dnszones
    - arecords
    - mxrecords
  verbs: ["create", "update", "patch", "delete"]

5. NetworkPolicies for BIND9 Pods

Restrict network access to DNS pods:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: bind9-network-policy
  namespace: dns-system
spec:
  podSelector:
    matchLabels:
      app: bind9
  policyTypes:
  - Ingress
  ingress:
  # Allow DNS queries on port 53
  - from:
    - podSelector: {}  # All pods in namespace
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53
  # Allow Bindcar API access (internal only)
  - from:
    - podSelector:
        matchLabels:
          app: bindy-controller
    ports:
    - protocol: TCP
      port: 8080

Example Scenarios

Scenario 1: Multi-Region Production DNS

Requirement: Platform team manages production DNS across multiple regions.

# Platform creates global cluster per region
apiVersion: bindy.firestoned.io/v1alpha1
kind: Bind9GlobalCluster
metadata:
  name: production-dns-us-east
spec:
  version: "9.18"
  primary:
    replicas: 3
    service:
      type: LoadBalancer
  secondary:
    replicas: 3
  acls:
    trusted:
      - "10.0.0.0/8"
---
apiVersion: bindy.firestoned.io/v1alpha1
kind: Bind9GlobalCluster
metadata:
  name: production-dns-eu-west
spec:
  version: "9.18"
  primary:
    replicas: 3
    service:
      type: LoadBalancer
  secondary:
    replicas: 3
  acls:
    trusted:
      - "10.128.0.0/9"
---
# App teams create zones in their namespace
apiVersion: bindy.firestoned.io/v1alpha1
kind: DNSZone
metadata:
  name: api-zone-us
  namespace: api-service
spec:
  zoneName: api.example.com
  globalClusterRef: production-dns-us-east
  soaRecord: { /* ... */ }
---
apiVersion: bindy.firestoned.io/v1alpha1
kind: DNSZone
metadata:
  name: api-zone-eu
  namespace: api-service
spec:
  zoneName: api.eu.example.com
  globalClusterRef: production-dns-eu-west
  soaRecord: { /* ... */ }

Scenario 2: Development Team Sandboxes

Requirement: Each dev team has isolated DNS for testing.

# Dev Team Alpha namespace
apiVersion: bindy.firestoned.io/v1alpha1
kind: Bind9Cluster
metadata:
  name: alpha-dns
  namespace: dev-alpha
spec:
  version: "9.18"
  primary:
    replicas: 1
  secondary:
    replicas: 1
---
apiVersion: bindy.firestoned.io/v1alpha1
kind: DNSZone
metadata:
  name: alpha-zone
  namespace: dev-alpha
spec:
  zoneName: alpha.test.local
  clusterRef: alpha-dns
  soaRecord: { /* ... */ }
---
# Dev Team Beta namespace (completely isolated)
apiVersion: bindy.firestoned.io/v1alpha1
kind: Bind9Cluster
metadata:
  name: beta-dns
  namespace: dev-beta
spec:
  version: "9.18"
  primary:
    replicas: 1
  secondary:
    replicas: 1
---
apiVersion: bindy.firestoned.io/v1alpha1
kind: DNSZone
metadata:
  name: beta-zone
  namespace: dev-beta
spec:
  zoneName: beta.test.local
  clusterRef: beta-dns
  soaRecord: { /* ... */ }

Scenario 3: Hybrid - Platform + Tenant DNS

Requirement: Production uses platform DNS, dev teams use their own.

# Platform manages production global cluster
apiVersion: bindy.firestoned.io/v1alpha1
kind: Bind9GlobalCluster
metadata:
  name: production-dns
spec:
  version: "9.18"
  primary:
    replicas: 3
    service:
      type: LoadBalancer
  secondary:
    replicas: 2
---
# Production app references global cluster
apiVersion: bindy.firestoned.io/v1alpha1
kind: DNSZone
metadata:
  name: app-prod
  namespace: production
spec:
  zoneName: app.example.com
  globalClusterRef: production-dns  # Platform-managed
  soaRecord: { /* ... */ }
---
# Dev team manages their own cluster
apiVersion: bindy.firestoned.io/v1alpha1
kind: Bind9Cluster
metadata:
  name: dev-dns
  namespace: development
spec:
  version: "9.18"
  primary:
    replicas: 1
---
# Dev app references namespace-scoped cluster
apiVersion: bindy.firestoned.io/v1alpha1
kind: DNSZone
metadata:
  name: app-dev
  namespace: development
spec:
  zoneName: app.dev.local
  clusterRef: dev-dns  # Team-managed
  soaRecord: { /* ... */ }

Next Steps