Skip to content

Authentication & Authorization

This guide provides a comprehensive reference for SPOG's security model, covering both authentication (verifying identity) and authorization (granting permissions). You will learn how JWT tokens carry user identity, how REGO policies make access decisions, and how to write custom policies that match your organizational structure.

What You'll Learn

  • How SPOG's security model separates authentication from authorization
  • JWT token structure and how claims become available in policies
  • REGO syntax fundamentals: OR logic, AND logic, and label checking
  • How to build helper modules that create reusable policy abstractions
  • Access control patterns: RBAC, ABAC, scopes, and dynamic scopes
  • How to use the debug interfaces for policy development and testing
  • Best practices for writing secure, maintainable authorization policies

Prerequisites

This guide assumes familiarity with the Architecture Overview, which introduces the three-layer permission model. For background on cluster labels and how they work, see the Labels & Filter Queries guide. All REGO examples use label patterns from the quickstart deployment.


Security Model Overview

SPOG implements a defense-in-depth security model with clear separation between authentication and authorization.

Authentication vs Authorization

Authentication answers "Who are you?" - It verifies user identity through credentials, tokens, or identity providers. SPOG supports multiple authentication methods: static users for development/testing and OIDC for production (including LDAP via OIDC providers like Keycloak).

Authorization answers "What can you do?" - After authentication establishes identity, the REGO policy engine evaluates what actions that identity can perform on which resources. Authorization decisions consider both user attributes (roles, groups) and resource attributes (cluster labels).

The Authorization Flow

When a user attempts an operation, SPOG follows this flow:

  1. Token Validation: The auth service validates the JWT token
  2. Claims Extraction: User attributes are extracted from the token
  3. Context Assembly: User claims are combined with cluster labels to form the policy input
  4. Policy Evaluation: The REGO engine evaluates policies against the assembled input
  5. Decision: Access is granted or denied based on policy results
Text Only
1
2
3
User Request → JWT Token → Claims Extraction → REGO Evaluation → Allow/Deny
                              ↓                      ↑
                         input.user            input.cluster

Well-Known Permissions

SPOG's policy engine evaluates these well-known permissions defined in pdns_permissions.rego:

Permission Purpose Example Use
connect Basic connection (always true for authenticated users) Establish session
read View cluster in UI See cluster in dashboard
read_logs View detailed information Read container logs
clear_cache Clear DNS cache Invalidate cached records
restart_instance_set Restart pods Rolling restart of instances
delete_pod Delete individual pods Remove specific container
dns_check Execute DNS queries Test DNS resolution

These permissions use helper rules from user.rego (like can_see_cluster, can_observe_cluster) to make decisions. The helpers are not permissions themselves - they're reusable building blocks.

Write Without Read

Write permissions (clear_cache, restart_instance_set) require cluster visibility but not read access. A user can have permission to clear cache without having permission to view logs.


Authentication Methods

SPOG supports multiple authentication methods to accommodate different deployment scenarios.

Static Users (Development and POC)

Static users provide a simple authentication mechanism for development, testing, and proof-of-concept deployments. Users are defined directly in the Helm configuration without requiring external identity infrastructure.

Configuration Structure:

YAML
policy:
  staticUsers:
    admin:
      id: "admin"
      sub: "admin"
      name: "Administrator"
      roles:
        - "admin"
        - "global"
        - "production"
        - "development"
      groups:
        - "dns-admins"

    us-operator:
      id: "us-operator"
      sub: "us-operator"
      name: "US Regional Operator"
      roles:
        - "americas"
        - "production"
        - "authoritative"
        - "observer"
        - "operator-non-prod"
      groups:
        - "dns-operators"

Static User Fields:

Static user fields are arbitrary - whatever you define in the YAML becomes available as claims in REGO. The only special field is password, which is used for authentication and then removed from the claims. Common conventions include:

Field Convention Available in REGO
password Authentication credential Not available (removed after auth)
id Unique identifier for the user input.user.id
sub Subject claim (typically matches id) input.user.sub
name Display name shown in the UI input.user.name
roles Array of role strings for authorization input.user.roles
groups Array of group memberships input.user.groups
any field Custom claims you define input.user.<field>

You can add any custom fields your policies need:

YAML
1
2
3
4
5
6
7
8
9
policy:
  staticUsers:
    region-lead:
      password: "secret"
      name: "Region Lead"
      roles: ["operator"]
      department: "infrastructure"      # Custom field
      cost_center: "CC-12345"          # Custom field
      allowed_clusters: ["prod-*"]      # Custom field - your policies decide meaning

Static Users Are Not for Production

Static users store credentials in Kubernetes ConfigMaps. Use static users only for development, testing, or demos. For production deployments, configure OIDC integration with your enterprise identity provider.

OIDC Integration (Production)

For production environments, SPOG integrates with OIDC (OpenID Connect) identity providers such as Keycloak, Okta, Azure AD, or Auth0. OIDC provides enterprise-grade authentication with features like single sign-on, multi-factor authentication, and centralized user management.

Configuration Overview:

YAML
1
2
3
4
policy:
  oidc:
    enabled: true
    issuerUrl: "https://auth.example.com/realms/spog"

When OIDC is enabled, SPOG:

  1. Redirects unauthenticated users to the identity provider
  2. Receives an ID token after successful authentication
  3. Extracts claims from the token for authorization decisions
  4. Maps OIDC claims to REGO input fields

Common OIDC Claims Mapping:

OIDC Claim REGO Path Description
sub input.user.sub Unique user identifier
name input.user.name Display name
email input.user.email Email address
groups input.user.groups Group memberships
roles input.user.roles Role assignments
Custom claims input.user.<claim> Any custom claims

Claims Are Case-Normalized

SPOG normalizes all claim keys to lowercase before making them available in REGO policies. If your OIDC provider sends Groups or ROLES, they will be accessible as input.user.groups and input.user.roles.

Playlist Tokens (NOC Displays)

Playlist tokens provide bearer token authentication for NOC displays, kiosk screens, and dashboards that need to show playlists without interactive user login. Unlike user tokens (OIDC or static), playlist tokens are simple bearer strings with claims defined in the Helm configuration.

Key Characteristics:

  • Prefix: PLT_ (e.g., PLT_abc123def456)
  • Token type: playlist (routed to input.playlist in REGO)
  • Read-only access: Can only view playlists, not modify data
  • No interactive login required

Configuration Structure:

YAML
playlistTokens:
  noc-main:
    token: "PLT_your-secure-token-here"
    claims:
      groups:
        - "noc-viewers"
      playlists:
        - "noc-24x7"
        - "global-overview"
      environment: "production"

  auto-generated:
    generate:
      secretName: "glass-noc-token"
      key: "token"
      length: 32
    claims:
      groups:
        - "generated-viewers"

Token Source Modes:

Mode Configuration Use Case
Direct token: "PLT_..." Simple setup, token visible in config
From Secret fromSecret: {name, key} Reference pre-existing K8s secret
Generate generate: {secretName, key, length} Auto-generate secure token

Claims Available in REGO:

Playlist token claims are available at input.playlist.<claim>:

Claim Example REGO Path
groups ["noc-viewers"] input.playlist.groups
playlists ["noc-24x7"] input.playlist.playlists
Custom claims Any value you define input.playlist.<claim>

Using Playlist Tokens:

Access playlists by including the token in the URL:

Text Only
https://console.spog.local/playlist/noc-24x7?token=PLT_your-token

REGO Policy for Playlist Access:

Rego
package pdns_global_flags



# Allow playlist access if token has the playlist in its claims
playlist_noc_24x7 if {
    "noc-24x7" in input.playlist.playlists
}

# Group-based playlist access
playlist_noc_24x7 if {
    "noc-viewers" in input.playlist.groups
}

# Admin bypass (user tokens can also access)
playlist_noc_24x7 if "admin" in input.user.roles

Retrieving Auto-Generated Tokens:

Bash
kubectl get secret glass-noc-token -n center -o jsonpath='{.data.token}' | base64 -d

Playlist Tokens Are Read-Only

Playlist tokens only grant access to view playlists and dashboard data. They cannot perform write operations, clear caches, restart instances, or access cluster management features.


Understanding JWT Tokens

JWT (JSON Web Token) tokens are the carrier of identity information in SPOG. Understanding their structure helps you write effective authorization policies.

Token Structure

A JWT consists of three parts separated by dots: header.payload.signature

The payload contains claims - key-value pairs that describe the token subject and metadata:

JSON
{
  "sub": "us-operator",
  "name": "US Regional Operator",
  "email": "operator@example.com",
  "roles": ["americas", "production", "observer"],
  "groups": ["dns-operators"],
  "token_type": "user",
  "iat": 1699900000,
  "exp": 1699986400
}

Standard vs Custom Claims

Standard Claims (defined by JWT specification):

Claim Description
sub Subject - unique identifier for the user
iat Issued At - timestamp when token was created
exp Expiration - timestamp when token expires
iss Issuer - who created the token

Custom Claims (added by SPOG or your OIDC provider):

Claim Description
roles Array of role assignments
groups Array of group memberships
token_type Type of token (typically user)
name Human-readable display name

How Claims Become REGO Input

The policy service transforms JWT claims into the input object available in REGO policies:

Text Only
JWT Token                         REGO Input
─────────────                     ──────────
{                                 {
  "sub": "us-operator",    →        "user": {
  "roles": ["americas"],              "sub": "us-operator",
  "token_type": "user"                "roles": ["americas"],
}                                     "type": "user"
                                    },
+ Cluster Context            →      "cluster": {
                                      "cluster_id": "pdns-us-east",
                                      "labels": {
                                        "region": ["us-east"],
                                        "environment": ["production"]
                                      }
                                    }
                                  }

View Your Claims in the Debug Interface

Navigate to /debug/user to see your actual JWT claims as they appear to the policy service. This is invaluable when writing policies that reference specific claim fields.


REGO Authorization System

REGO is a declarative policy language created by the Open Policy Agent (OPA) project. SPOG uses REGO because it provides powerful, flexible authorization logic that can express complex access control requirements.

What is REGO?

REGO policies are collections of rules that evaluate to true or false. Unlike imperative code that describes how to compute something, REGO describes what conditions must be true for a result.

Key characteristics of REGO:

  • Declarative: Describe conditions, not procedures
  • Data-driven: Policies operate on structured input data
  • Composable: Build complex rules from simpler rules
  • Safe: No side effects, guaranteed termination

Policy Structure in SPOG

SPOG organizes REGO policies into packages with specific purposes:

Package Purpose File
user Helper rules for user attribute matching user.rego
pdns_permissions Well-known permissions for cluster operations pdns_permissions.rego
pdns_global_flags Custom flags for navigation and dashboard access pdns_global_flags.rego

Required Policy File Structure

Every REGO policy file must begin with a package declaration:

Rego
1
2
3
package user          # Package declaration - REQUIRED

# Your policy rules go here...

Package Declaration (package name)

The package statement declares which namespace this file's rules belong to. This is mandatory - OPA will reject files without a package declaration. The package name determines how other policies reference these rules:

Package Declaration How to Reference Rules
package user user.can_see_cluster, user.admin
package pdns_permissions data.pdns_permissions.read
package pdns_global_flags data.pdns_global_flags.dashboard_noc

When importing and using another package's rules within a policy file, use the import statement:

Rego
1
2
3
4
5
6
7
package pdns_permissions

import data.user    # Import the user package

# Now you can reference rules from the user package
read if user.can_see_cluster    # Calls the can_see_cluster rule from user.rego
read if user.admin              # Calls the admin rule from user.rego

Always Include Package Declaration

Every policy file must start with package <name>. Missing this will cause policy evaluation errors.

pdns_permissions vs pdns_global_flags

Understanding the difference between these two packages is essential for writing effective policies:

pdns_permissions defines well-known permission names that SPOG's frontend and backend explicitly check. These permission names (like read, clear_cache, restart_instance_set) are hard-coded into the system - the UI checks them to show/hide buttons, and the backend checks them to allow/deny operations. You cannot invent new permission names here; you can only define the rules that grant the existing permissions.

Rego
1
2
3
4
5
package pdns_permissions

# You're defining WHEN "read" is granted, not creating a new permission
read if user.can_see_cluster
read if user.can_observe_cluster

pdns_global_flags defines arbitrary flag names that you invent for use with requires: in navigation menus and dashboards. The flag names are entirely up to you - SPOG doesn't know or care what they're called. You create a flag, then reference it in your navigation or dashboard configuration.

Rego
1
2
3
4
5
6
package pdns_global_flags

# You're inventing these flag names - they can be anything
dashboard_americas_operations if "americas" in input.user.roles
navigation_admin_tools if "admin" in input.user.roles
my_custom_feature_flag if "beta-tester" in input.user.roles

These flags are then referenced in navigation and dashboard configuration:

YAML
globalConfig:
  navigation:
    menus:
      - name: "Operations"
        sections:
          - name: "Regional"
            items:
              - name: "Americas Dashboard"
                url: "/dashboards/regional-ops"
                requires:
                  - "dashboard_americas_operations"  # References your flag
Aspect pdns_permissions pdns_global_flags
Names Well-known, hard-coded in SPOG Arbitrary, you invent them
Purpose Cluster operations (read, write, execute) UI visibility (navigation, dashboards)
Checked by Frontend + Backend Frontend only (via requires:)
Creating new Cannot add new permission names Create any flag names you need

Naming Convention

While pdns_global_flags names are arbitrary, using descriptive prefixes helps organization:

  • dashboard_* for dashboard access flags
  • playlist_* for playlist access flags
  • navigation_* for navigation item visibility
  • feature_* for feature flags

Policy File Locations:

  • Default Policies: Built into the policy service image at /etc/defaultPolicies/
  • User Policies: Mounted from ConfigMaps at /etc/policies/

User policies override default policies when they define the same package. This allows you to customize authorization without modifying the base image.

REGO Syntax Essentials

Understanding three fundamental REGO patterns will enable you to write most authorization policies. These patterns are demonstrated throughout the complete examples in the Writing REGO Policies section below.

OR Logic: Multiple Rules with the Same Name

When you define multiple rules with the same name, REGO treats them as alternatives - if any rule evaluates to true, the result is true. This implements OR logic.

Example: Regional Access

Rego
package user



roles := input.user.roles

# Rule 1: Americas users can access US regions
has_matching_region if {
    input.cluster.labels.region in ["us-east", "us-west"]
    "americas" in roles
}

# Rule 2: Europe users can access EU regions
has_matching_region if {
    input.cluster.labels.region in ["eu-east", "eu-west"]
    "europe" in roles
}

# Rule 3: APAC users can access Asia-Pacific regions
has_matching_region if {
    input.cluster.labels.region in ["ap-north", "ap-south"]
    "apac" in roles
}

# Rule 4: Global users can access ANY region
has_matching_region if {
    "global" in roles
}

This defines four separate rules named has_matching_region. A user matches if:

  • They have the americas role AND the cluster is in us-east or us-west, OR
  • They have the europe role AND the cluster is in eu-east or eu-west, OR
  • They have the apac role AND the cluster is in ap-north or ap-south, OR
  • They have the global role (regardless of cluster region)

Another Example: Dashboard Access

Rego
package pdns_global_flags



# Executive dashboard - accessible via multiple paths
dashboard_executive_overview if "executive" in input.user.roles
dashboard_executive_overview if "admin" in input.user.roles

# Regional dashboard - requires regional role
dashboard_americas_operations if "americas" in input.user.roles
dashboard_americas_operations if {
    "regional-director" in input.user.roles
    "americas" in input.user.roles
}
dashboard_americas_operations if "admin" in input.user.roles

OR Logic Summary

Multiple rules with the same name implement OR logic. The rule is true if any of the definitions evaluates to true.

AND Logic: Multiple Conditions in a Rule

Within a single rule definition, multiple conditions on separate lines implement AND logic - all conditions must be true for the rule to be true.

Example: Cluster Visibility

Rego
package user



# User can see cluster only if ALL conditions match
can_see_cluster if {
    has_matching_region        # Condition 1: Region access
    has_matching_cluster_role  # Condition 2: Role type access
    has_matching_environment   # Condition 3: Environment access
}

This rule requires all three helper rules to be true. A user with americas role (matching region) but missing the production role (environment mismatch) cannot see production clusters.

Example: Non-Production Operator

Rego
1
2
3
4
5
6
# Can manage DNS content in non-production environments
can_manage_dns_content if {
    can_see_cluster                                          # Must see the cluster
    "content-manager-non-prod" in roles                      # Must have this role
    not "production" in input.cluster.labels.environment     # Must NOT be production
}

All three conditions must be true: 1. User can see the cluster (via region/role/environment matching) 2. User has the content-manager-non-prod role 3. The cluster is NOT in the production environment

Combining OR and AND:

Rego
# Full permission rule with OR (multiple definitions) and AND (multiple conditions)
can_manage_dns_content if {
    can_see_cluster
    "content-manager" in roles
}

can_manage_dns_content if {
    can_see_cluster
    "content-manager-non-prod" in roles
    not "production" in input.cluster.labels.environment
}

can_manage_dns_content if {
    admin  # Admin override - single condition
}

This defines three ways to get DNS content management permission: 1. Have the content-manager role (full access), OR 2. Have content-manager-non-prod AND be accessing a non-production cluster, OR 3. Be an admin

AND Logic Summary

Multiple lines within a single rule block implement AND logic. All conditions must be true for that rule definition to be true.

Checking Labels and Values

REGO provides powerful operators for checking membership and matching values. Understanding how in works is essential for SPOG policies.

Using in for Membership:

The in operator checks if a value is a member of a collection:

Rego
1
2
3
4
5
6
7
8
9
# Check if user has a specific role (string in array)
"admin" in input.user.roles

# Check if cluster region is one of the allowed values
# This checks: is the string "us-east" a member of the array ["us-east", "us-west"]?
input.cluster.labels.region in ["us-east", "us-west"]

# Check if a role string is in the user's role list
"americas" in roles

Labels Are Always Arrays

Cluster labels are always arrays, even for single values (e.g., region: ["us-east"]). This means:

  • Never use == for label comparisons: input.cluster.labels.region == "us-east" will fail
  • Use in to check if a value exists in a label: "us-east" in input.cluster.labels.region
  • Use some to iterate when matching against user attributes

Using some for Iteration:

The some keyword introduces a local variable that iterates over a collection:

Rego
1
2
3
4
5
# Check if ANY user role matches the cluster's role label
has_matching_cluster_role if {
    some role in roles
    role in input.cluster.labels.role
}

This iterates through all user roles and checks if any of them is in the cluster's role label. Use some when you need to match ANY element from one collection against another.

Checking Label Existence:

Rego
1
2
3
4
5
6
7
8
9
# Check if cluster has a specific label key (evaluates to true if key exists and has value)
has_region_label if {
    input.cluster.labels.region
}

# Check if environment label contains a specific value
is_production if {
    "production" in input.cluster.labels.environment
}

Combining Iteration for Multi-Valued Labels:

When clusters can have multiple values for a label (e.g., multiple teams managing one cluster), use some to check both sides:

Rego
1
2
3
4
5
6
7
# User can access cluster if they have ANY role that matches
# ANY of the cluster's team labels
has_team_access if {
    some user_role in input.user.roles
    some cluster_team in input.cluster.labels.team
    user_role == cluster_team
}

Single vs Multi-Valued Labels

Most labels are single-valued strings in the input (e.g., region: "us-east"). However, some labels can be arrays for shared ownership (e.g., team: ["platform", "security"]). The in operator works for both - it checks membership naturally whether the value is a string or an element in an array.


Building Helper Modules

Helper modules provide reusable abstractions that simplify permission policies. The user.rego file demonstrates this pattern.

The user.rego Pattern

The user package defines convenience rules that other policies import and use:

Rego
package user



# Extract roles for convenient access
roles := input.user.roles

# Role convenience flags - single-line rules
admin if "admin" in roles
executive if "executive" in roles
security_officer if "security-officer" in roles
regional_director if "regional-director" in roles
noc if "noc" in roles
operator if "operator" in roles
observer if "observer" in roles

# Complex matching rules
has_matching_region if {
    input.cluster.labels.region in ["us-east", "us-west"]
    "americas" in roles
}
# ... additional region rules ...

has_matching_cluster_role if {
    some role in roles
    role in input.cluster.labels.role
}

has_matching_environment if {
    some env in input.cluster.labels.environment
    env in roles
}

# Compound rules building on simpler rules
can_see_cluster if {
    has_matching_region
    has_matching_cluster_role
    has_matching_environment
}

can_observe_cluster if {
    can_see_cluster
    "observer" in roles
}

can_manage_dns_content if {
    can_see_cluster
    "content-manager" in roles
}

Importing Helper Modules

Other policy packages import the user module to leverage its rules:

Rego
package pdns_permissions


import data.user    # Import the user helper module

# Use helper rules in permission definitions
read if user.can_see_cluster
read if user.admin
read if user.regional_director

read_logs if user.can_observe_cluster

clear_cache if user.can_manage_dns_content
clear_cache if user.admin

restart_instance_set if user.can_manage_instances
restart_instance_set if user.admin

Benefits of Helper Modules

  1. Abstraction: Permission rules read naturally - user.can_see_cluster is clearer than inline conditions
  2. Reusability: Define matching logic once, use it in multiple permission rules
  3. Maintainability: Update region mappings in one place when adding new regions
  4. Layered Security: Build complex permissions from well-tested building blocks

Creating Custom Helper Modules

You can create additional helper modules for your organization's needs:

Rego
package customer



# Customer-specific helper rules
customer_id := input.user.customer_id

is_premium_customer if {
    customer_id
    "premium" in input.cluster.labels.tier
    customer_id in input.cluster.labels.customer
}

is_standard_customer if {
    customer_id
    "standard" in input.cluster.labels.tier
    customer_id in input.cluster.labels.customer
}

can_access_customer_cluster if {
    is_premium_customer
}

can_access_customer_cluster if {
    is_standard_customer
}

Then import and use in permissions:

Rego
1
2
3
4
5
6
7
package pdns_permissions


import data.user
import data.customer

read if customer.can_access_customer_cluster

Access Control Patterns

SPOG supports multiple access control patterns that can be combined to match your organizational requirements.

Role-Based Access Control (RBAC)

RBAC assigns permissions based on roles. Users receive roles, and roles determine what actions are permitted.

Role Hierarchy Example:

Rego
package user



roles := input.user.roles

# Define role hierarchy using convenience flags
admin if "admin" in roles

# Managers inherit from operators
manager if "manager" in roles
manager if admin

# Operators inherit from observers
operator if "operator" in roles
operator if manager

# Observers are the base level
observer if "observer" in roles
observer if operator

Using Role Hierarchy in Permissions:

Rego
package pdns_permissions


import data.user

# Observers can read
read if user.observer

# Operators can clear cache (and read, since they're also observers)
clear_cache if user.operator

# Managers can restart instances
restart_instance_set if user.manager

# Admins can delete pods
delete_pod if user.admin

Pattern 1: Role-Based Dashboard Access

Rego
package pdns_global_flags



# NOC dashboard requires NOC or global role
dashboard_noc_monitoring if "noc" in input.user.roles
dashboard_noc_monitoring if "global" in input.user.roles
dashboard_noc_monitoring if "admin" in input.user.roles

# Executive dashboard requires executive or admin role
dashboard_executive_overview if "executive" in input.user.roles
dashboard_executive_overview if "admin" in input.user.roles

# Service dashboards require service-specific roles
dashboard_authoritative_dns if "authoritative" in input.user.roles
dashboard_authoritative_dns if {
    "service-lead" in input.user.roles
    "authoritative" in input.user.roles
}
dashboard_authoritative_dns if "admin" in input.user.roles

Attribute-Based Access Control (ABAC)

ABAC makes decisions based on arbitrary attributes of the user, resource, and environment - not just role membership. This enables fine-grained control using any claim from your identity provider or static user configuration.

Recall that static user fields are arbitrary - you can define any attributes your policies need. ABAC leverages this flexibility.

Pattern 2: Department and Cost Center Access

Rego
package user



# Access based on department attribute (not a role!)
# Labels are arrays, so use `some` to iterate
has_department_access if {
    some dept in input.cluster.labels.department
    input.user.department == dept
}

# Cross-department access for shared infrastructure
has_department_access if {
    "shared-services" in input.cluster.labels.department
}

# Cost center based access - user can only see clusters they pay for
has_cost_center_access if {
    some cc in input.cluster.labels.cost_center
    input.user.cost_center == cc
}

Static user configuration for department-based access:

YAML
1
2
3
4
5
6
7
8
policy:
  staticUsers:
    infra-engineer:
      password: "secret"
      name: "Infrastructure Engineer"
      department: "infrastructure"    # Arbitrary attribute
      cost_center: "CC-INFRA-001"     # Arbitrary attribute
      roles: ["operator"]

Pattern 3: Allowed Regions List

Instead of using roles for regions, store allowed regions directly as a user attribute:

Rego
package user



# User has a list of specific regions they can access
has_region_access if {
    some allowed_region in input.user.allowed_regions
    some cluster_region in input.cluster.labels.region
    allowed_region == cluster_region
}

# Wildcard support for "all regions"
has_region_access if {
    "*" in input.user.allowed_regions
}

Static user with explicit region list:

YAML
policy:
  staticUsers:
    us-specialist:
      password: "secret"
      name: "US Regional Specialist"
      allowed_regions: ["us-east", "us-west", "us-central"]  # Explicit list
      roles: ["observer"]

    global-admin:
      password: "secret"
      name: "Global Administrator"
      allowed_regions: ["*"]  # Wildcard for all regions
      roles: ["admin"]

Pattern 4: Clearance Level Hierarchy

Rego
package user



# Define clearance hierarchy (higher number = more access)
clearance_levels := {
    "public": 1,
    "internal": 2,
    "confidential": 3,
    "restricted": 4
}

# User can access clusters at or below their clearance level
# Labels are arrays, so iterate to find the classification
has_clearance if {
    user_level := clearance_levels[input.user.clearance_level]
    some classification in input.cluster.labels.classification
    cluster_level := clearance_levels[classification]
    user_level >= cluster_level
}

Pattern 5: Multi-Tenant Customer Isolation

Rego
package user



# User can only see clusters belonging to their customer
# Labels are arrays, so check if user's customer is in the label array
can_see_customer_cluster if {
    input.user.customer
    input.user.customer in input.cluster.labels.customer
}

# Customer admins can manage their own clusters
can_manage_customer_cluster if {
    can_see_customer_cluster
    input.user.customer_role == "admin"  # Customer-specific role attribute
}

Pattern 6: Team Membership Attribute

Rego
package user



# User has a teams attribute (array of team names)
has_team_access if {
    some user_team in input.user.teams
    some cluster_team in input.cluster.labels.team
    user_team == cluster_team
}

Static user with team membership:

YAML
1
2
3
4
5
6
7
8
policy:
  staticUsers:
    platform-dev:
      password: "secret"
      name: "Platform Developer"
      teams: ["platform", "shared-services"]  # Team membership as attribute
      department: "engineering"
      roles: ["developer"]

ABAC vs RBAC

The key difference: RBAC checks "some-role" in input.user.roles, while ABAC uses arbitrary attributes like input.user.department, input.user.allowed_regions, or input.user.clearance_level. ABAC is more flexible but requires your identity provider to include these attributes in tokens.

Scopes and Permission Boundaries

Scopes define permission boundaries, often used with OIDC tokens to limit what an authenticated session can do.

Pattern 6: Scope-Based Permissions

Rego
package user



scopes := input.user.scope  # OIDC scope claim (may be space-delimited string or array)

# Check for read scope
has_read_scope if {
    "read:clusters" in scopes
}

has_read_scope if {
    contains(scopes, "read:clusters")  # If scope is a space-delimited string
}

# Check for write scope
has_write_scope if {
    "write:clusters" in scopes
}

# Check for admin scope
has_admin_scope if {
    "admin:all" in scopes
}

Using Scopes in Permissions:

Rego
package pdns_permissions


import data.user

# Read requires read scope (or admin scope)
read if user.has_read_scope
read if user.has_admin_scope

# Write operations require write scope
clear_cache if {
    user.can_manage_dns_content
    user.has_write_scope
}

clear_cache if user.has_admin_scope

Dynamic Scopes

Dynamic scopes encode variable information in the scope string itself, allowing policies to extract and match against cluster attributes.

Pattern 7: Region-Scoped Access

Rego
package user



# Scopes like "region:us-east", "region:eu-west"
# Extract region from scope and match against cluster label array
has_region_scope if {
    some scope in input.user.scopes
    startswith(scope, "region:")
    region := trim_prefix(scope, "region:")  # Extract after "region:"
    region in input.cluster.labels.region
}

Pattern 8: Customer-Scoped Access

Rego
package user



# Scopes like "customer:acme-corp", "customer:widgets-inc"
has_customer_scope if {
    some scope in input.user.scopes
    startswith(scope, "customer:")
    customer := trim_prefix(scope, "customer:")  # Extract after "customer:"
    customer in input.cluster.labels.customer
}

# Combined scope check
can_access_customer_region if {
    has_region_scope
    has_customer_scope
}

Pattern 9: Environment-Scoped Write Access

Rego
package user



# Scopes like "write:development", "write:staging", "write:production"
can_write_to_environment if {
    some scope in input.user.scopes
    startswith(scope, "write:")
    allowed_env := trim_prefix(scope, "write:")
    allowed_env in input.cluster.labels.environment
}

# User with scope "write:staging" can only write to staging clusters

Dynamic Permission Arguments

Dynamic permission arguments allow dashboard and navigation configurations to pass runtime values to REGO policies. This enables truly dynamic access control without per-resource REGO rules.

How It Works:

  1. Dashboard configuration specifies requires with arguments
  2. UI extracts route parameters and passes them to the policy engine
  3. REGO receives values in input.arguments object
  4. Policy evaluates using these dynamic values

Configuration Format:

YAML
# Dashboard configuration with dynamic arguments
team-dashboard:
  url: /team/:team
  requires:
    - permission: "see_team_dashboard"
      arguments:
        - name: team
          value: ":team"    # Route parameter
        - name: tier
          value: "production"  # Static value

Argument Value Syntax:

Syntax Description Example
:param Extract from URL route parameter :team from /team/:team
{{ param }} Template expression {{ team }}_suffix
"value" Static string "production"

REGO Policy Pattern:

Rego
package pdns_global_flags



# Dynamic team dashboard access
# Uses input.arguments.team passed from the UI
see_team_dashboard if {
  input.arguments.team in input.user.roles
}
see_team_dashboard if "admin" in input.user.roles

# Multiple arguments
see_team_tier_dashboard if {
  input.arguments.team in input.user.roles
  input.arguments.tier in input.user.roles
}
see_team_tier_dashboard if "admin" in input.user.roles

Input Structure:

When arguments are provided, the policy engine receives:

JSON
{
  "user": {
    "roles": ["devops", "observer", "production"],
    "sub": "devops-user",
    "name": "DevOps Test User"
  },
  "arguments": {
    "team": "devops",
    "tier": "production"
  }
}

Benefits:

  • One rule for all teams: see_team_dashboard works for any team value
  • No per-resource REGO: Add new teams without policy changes
  • Runtime flexibility: Access control adapts to URL parameters
  • Reduced maintenance: Fewer rules to manage

When to Use Dynamic Arguments

Use dynamic arguments when you have parameterized routes (like /team/:team) and want access control to follow the parameter. For static routes, use simple string format in requires.


Writing REGO Policies

This section provides complete, ready-to-use policy examples covering common authorization scenarios. Each pattern is self-contained and can be adapted for your organization.

Pattern 10: Complete Regional Operations Policy

Rego
package user



roles := input.user.roles

# Role convenience flags
admin if "admin" in roles
executive if "executive" in roles
regional_director if "regional-director" in roles
noc if "noc" in roles
operator if "operator" in roles
observer if "observer" in roles

# Regional access - OR logic across regions
has_matching_region if {
    input.cluster.labels.region in ["us-east", "us-west"]
    "americas" in roles
}

has_matching_region if {
    input.cluster.labels.region in ["eu-east", "eu-west"]
    "europe" in roles
}

has_matching_region if {
    input.cluster.labels.region in ["ap-north", "ap-south"]
    "apac" in roles
}

has_matching_region if {
    "global" in roles
}

# Service type access
has_matching_cluster_role if {
    some role in roles
    role in input.cluster.labels.role
}

# Environment access
has_matching_environment if {
    some env in input.cluster.labels.environment
    env in roles
}

# Compound visibility rule - AND logic
can_see_cluster if {
    has_matching_region
    has_matching_cluster_role
    has_matching_environment
}

# Admin override for visibility
can_see_cluster if {
    admin
    has_matching_region
    has_matching_cluster_role
}

# Read-only observation
can_observe_cluster if {
    can_see_cluster
    observer
}

can_observe_cluster if {
    admin
}

# DNS content management
can_manage_dns_content if {
    can_see_cluster
    "content-manager" in roles
}

can_manage_dns_content if {
    can_see_cluster
    "content-manager-non-prod" in roles
    not "production" in input.cluster.labels.environment
}

can_manage_dns_content if {
    admin
}

# Instance management
can_manage_instances if {
    can_see_cluster
    operator
}

can_manage_instances if {
    can_see_cluster
    "operator-non-prod" in roles
    not "production" in input.cluster.labels.environment
}

can_manage_instances if {
    admin
}

Pattern 11: Tiered Service Access

Permission Inheritance Design

This pattern implements implicit permission inheritance - users with higher-tier access automatically get lower-tier access. Design these hierarchies carefully to ensure this behavior matches your security requirements. Always document which permissions imply which other permissions.

Rego
package user



roles := input.user.roles

# Tier-based access for support scenarios
can_access_critical_tier if {
    "tier-1-support" in roles
    "critical" in input.cluster.labels.tier
}

can_access_critical_tier if {
    "senior-engineer" in roles
}

can_access_critical_tier if {
    "admin" in roles
}

# Critical access implies standard access (inheritance)
can_access_standard_tier if {
    can_access_critical_tier
}

can_access_standard_tier if {
    "tier-2-support" in roles
    "standard" in input.cluster.labels.tier
}

# Standard access implies experimental access (inheritance)
can_access_experimental_tier if {
    can_access_standard_tier
}

can_access_experimental_tier if {
    "developer" in roles
    "experimental" in input.cluster.labels.tier
}

# Unified tier check - OR logic across all tiers
can_access_tier if can_access_critical_tier
can_access_tier if can_access_standard_tier
can_access_tier if can_access_experimental_tier

Testing & Debugging Policies

SPOG provides two debug interfaces for developing and testing authorization policies.

Using the User Debug Interface

The User Debug interface at /debug/user displays your JWT claims and generates REGO code snippets based on your actual token structure.

Navigating to User Debug:

  1. Log into the Glass UI
  2. Navigate to /debug/user (or access via Dev Mode menu)

Understanding the Interface:

The page displays three sections:

1. User Profile Section

Shows basic identity information: - Name: Display name from claims - Email: Email address (if available) - Type: Token type (User Token or Machine Token)

2. JWT Claims Section

Displays the complete claims object as an expandable JSON tree. This is exactly what your REGO policies see via input.user. Common fields include:

Text Only
1
2
3
4
5
sub: "us-operator"
name: "US Regional Operator"
roles: ["americas", "production", "observer"]
groups: ["dns-operators"]
token_type: "user"

3. REGO Policy Examples Section

The interface analyzes your claims structure and generates relevant REGO code snippets. These snippets demonstrate proper syntax using your actual field names and values.

Understanding the Generated Code Snippets:

The User Debug page generates six types of examples based on your claims:

Snippet 1: Check if array contains a value

Rego
# Check if roles contains a specific value
"americas" in input.user.roles

Use this pattern when checking if a user has a specific role, group, or other array membership.

Snippet 2: Iterate over array with some

Rego
1
2
3
# Check if any roles item matches a cluster label
some item in input.user.roles
item in input.cluster.labels.role

Use this pattern when you need to find any match between user attributes and cluster labels.

Snippet 3: Match string exactly

Rego
# Check exact sub match
input.user.sub == "us-operator"

Use this pattern for exact string comparisons on scalar claims.

Snippet 4: Check value in allowed list

Rego
# Check if token_type is in an allowed list
input.user.token_type in ["user", "service"]

Use this pattern to verify a claim value is within an acceptable set.

Snippet 5: Combine multiple conditions

Rego
1
2
3
4
5
# Allow if user meets multiple criteria
can_access if {
    "americas" in input.user.roles
    input.user.sub == "us-operator"
}

Use this pattern to create rules with AND logic - all conditions must be true.

Snippet 6: Define reusable rules

Rego
1
2
3
4
5
6
7
8
9
# Create convenience rules from roles
americas if "americas" in input.user.roles
production if "production" in input.user.roles

# Use in policy decisions
can_manage if {
    americas
    not "production" in input.cluster.labels.environment
}

Use this pattern to create helper rules in user.rego that simplify permission definitions.

Copy and Adapt

Click the "Copy" button on any example to copy it to your clipboard. These snippets use your actual claim field names, so they're ready to adapt for your policies.

Using the Policy Debug Interface

The Policy Debug interface at /debug/policy provides an interactive environment for testing REGO policies against simulated inputs.

Navigating to Policy Debug:

  1. Log into the Glass UI
  2. Navigate to /debug/policy (or access via Dev Mode menu)

Interface Components:

1. File Selector (Top Bar)

A dropdown showing available policy files organized into two groups: - User Policies: Custom policies mounted from ConfigMaps - Default Policies: Built-in policies from the service image

Select a file to view and edit its contents. Modified files show a "modified" badge.

2. Query Input

Enter REGO query expressions to evaluate:

Text Only
1
2
3
data.pdns_permissions.read
data.user.can_see_cluster
data.pdns_global_flags.dashboard_executive_overview

Press Enter or click "Evaluate" to run the query.

3. Input Data Editor

A JSON/YAML editor for defining the policy input. This simulates the context that would be assembled for a real authorization check:

JSON
{
  "user": {
    "sub": "test-user",
    "roles": ["americas", "production", "observer"],
    "groups": ["dns-team"],
    "token_type": "user"
  },
  "cluster": {
    "cluster_id": "pdns-us-east-prod",
    "labels": {
      "region": ["us-east"],
      "environment": ["production"],
      "role": ["authoritative"],
      "team": ["platform"]
    },
    "connected": true
  }
}

4. Policy File Editor

View and modify policy file contents. Changes are temporary (session-only) and don't affect the actual deployed policies. This allows safe experimentation.

5. Results Panel

Displays: - Result: The evaluation result (true, false, or a value) - Trace: Step-by-step evaluation trace for debugging

Step-by-Step Debugging Workflow:

  1. Select a policy file to understand its structure
  2. Set up input data that simulates your test scenario
  3. Enter a query for the rule you want to test
  4. Click Evaluate to see the result
  5. Review the trace if the result is unexpected
  6. Modify policies or input and re-evaluate to iterate

Example Debugging Session:

Testing why a user cannot see a cluster:

Text Only
Query: data.user.can_see_cluster

Input:
{
  "user": {
    "roles": ["americas", "production"]
  },
  "cluster": {
    "labels": {
      "region": ["us-east"],
      "environment": ["production"],
      "role": ["authoritative"]
    }
  }
}

Result: false

The result is false. Test the component rules:

Text Only
1
2
3
4
5
6
7
8
Query: data.user.has_matching_region
Result: true  ✓

Query: data.user.has_matching_environment
Result: true  ✓

Query: data.user.has_matching_cluster_role
Result: false  ✗

The user lacks the authoritative role. Add it to the input and re-evaluate:

JSON
1
2
3
4
5
6
7
8
9
{
  "user": {
    "roles": ["americas", "production", "authoritative"]
  },
  ...
}

Query: data.user.can_see_cluster
Result: true  

Changes Are Session-Only

Policy modifications in the debug interface are temporary. To deploy policy changes, update the Helm configuration and redeploy.


Best Practices

Follow these practices to create secure, maintainable authorization policies.

Security Best Practices

Principle of Least Privilege

Grant the minimum permissions required for each role. Start with deny-all and explicitly allow specific actions:

Rego
1
2
3
# Default deny - only explicitly allowed operations succeed
read if user.can_see_cluster
# Without this rule, read is implicitly denied

Explicit Admin Checks

Always verify admin status explicitly rather than assuming admin access:

Rego
1
2
3
4
5
# Good: Explicit admin check
delete_pod if user.admin

# Avoid: Implicit admin assumption through role hierarchy
delete_pod if user.operator  # Dangerous if operators inherit admin

Separate Production Permissions

Require explicit production access rather than granting it implicitly:

Rego
1
2
3
4
5
6
7
8
9
# Good: Explicit production check
can_modify_production if {
    can_see_cluster
    "production-operator" in roles
    "production" in input.cluster.labels.environment
}

# Avoid: Implicit production access
can_modify if can_see_cluster  # Would allow prod modification

Audit-Friendly Rules

Write rules that produce clear audit trails:

Rego
1
2
3
4
5
6
7
8
9
# Include identifying information in rule names
can_restart_production_instances if {
    user.admin
}

can_restart_nonprod_instances if {
    "operator-non-prod" in roles
    not "production" in input.cluster.labels.environment
}

Maintainability Best Practices

Organize Helper Rules in user.rego

Keep all user-related helper logic in a single file:

Rego
package user

# All role flags
admin if "admin" in roles
operator if "operator" in roles

# All matching rules
has_matching_region if ...
has_matching_environment if ...

# All compound rules
can_see_cluster if ...
can_manage_cluster if ...

Use Descriptive Rule Names

Choose names that clearly indicate what the rule checks:

Rego
1
2
3
4
5
6
7
8
9
# Good: Clear purpose
can_access_production_clusters
has_regional_admin_role
is_customer_tenant_match

# Avoid: Vague names
check_access
validate
is_ok

Document Complex Logic

Add comments explaining non-obvious policy decisions:

Rego
1
2
3
4
5
6
7
8
9
# Regional directors can see all clusters in their region
# even without environment-specific roles, because they need
# visibility for escalation handling
can_see_cluster if {
    regional_director
    has_matching_region
    has_matching_cluster_role
    # Note: environment check intentionally omitted for directors
}

Version Your Policies

Include version comments and change history:

Rego
1
2
3
4
5
6
7
package user

# Version: 2.1.0
# Last Modified: 2024-01-15
# Changes:
#   - Added APAC region support
#   - Added customer-admin role

Quick Reference

REGO Syntax Summary

Pattern Syntax Meaning
OR (multiple rules) rule if {...} rule if {...} Any rule true = result true
AND (multiple conditions) rule if { cond1; cond2 } All conditions must be true
Array membership "value" in array Check if value is in array
Iteration some x in array Iterate over array elements
String prefix startswith(s, "prefix:") Check string starts with prefix
String extraction trim_prefix(s, "prefix:") Remove prefix from string
Negation not condition True if condition is false
Assignment x := expression Assign value to variable
Comparison ==, !=, <, > Compare values

Input Structure Reference

JSON
{
  "user": {
    "sub": "string",
    "name": "string",
    "email": "string",
    "roles": ["array", "of", "strings"],
    "groups": ["array", "of", "strings"],
    "token_type": "user",
    "scopes": ["optional", "oidc", "scopes"]
  },
  "cluster": {
    "cluster_id": "string",
    "labels": {
      "key": ["array", "of", "values"]
    },
    "connected": true
  },
  "args": {}
}

Well-Known Permissions

These are the permissions defined in pdns_permissions.rego that SPOG evaluates:

Permission Helper Rule Used Use Case
connect Always true Basic connection
read user.can_see_cluster View cluster in UI
read_logs user.can_observe_cluster View container logs
clear_cache user.can_manage_dns_content Clear DNS cache
restart_instance_set user.can_manage_instances Restart pods
delete_pod user.can_manage_instances Delete individual pods
dns_check user.can_observe_cluster Execute DNS queries

The helper rules (can_see_cluster, can_observe_cluster, etc.) are defined in user.rego and can be customized.


What's Next?

Now that you understand SPOG's authorization system, explore these related topics:

Integration Guides

External Resources


Summary

SPOG's authentication and authorization system provides flexible, policy-driven access control through:

  1. Multiple Authentication Methods: Static users for development, OIDC for production
  2. JWT-Based Identity: Tokens carry user attributes that become policy input
  3. REGO Policy Engine: Declarative policies express complex authorization logic
  4. Helper Module Pattern: Reusable rules in user.rego simplify permission definitions
  5. Multiple Access Patterns: RBAC, ABAC, and scope-based access control
  6. Debug Interfaces: Interactive tools for policy development and testing

The key REGO concepts to remember:

  • Multiple rules with the same name implement OR logic - any rule being true makes the result true
  • Multiple conditions within a rule implement AND logic - all conditions must be true
  • Use in to check array membership and some to iterate over arrays
  • Build helper modules to create reusable abstractions that simplify permission rules

Use the debug interfaces at /debug/user and /debug/policy to develop and test policies before deployment.