Policy as Code

A Simple Introduction

Author

Patrick Lefler

Published

April 30, 2026

Abstract
Policy as Code is the practice of expressing organizational rules as executable, version-controlled logic rather than as prose. This document introduces the concept through the most simple example possible - setting password characteristics that any team can read, test, and extend.

Setup

Display code
# ── Libraries ──────────────────────────────────────────────────────────────
library(tidyverse)
library(scales)
library(knitr)
library(kableExtra)
library(plotly)
library(sessioninfo)

# ── Brand Colors ───────────────────────────────────────────────────────────
brand_primary   <- "#222222"
brand_secondary <- "#6E6E73"
brand_accent    <- "#0166CC"
brand_highlight <- "#1D9E75"
brand_surface   <- "#F5F5F5"
brand_text      <- "#222222"

brand_palette <- c(
  primary   = brand_primary,
  secondary = brand_secondary,
  accent    = brand_accent,
  highlight = brand_highlight
)

# ── Brand ggplot2 theme ────────────────────────────────────────────────────
theme_brand <- function(base_size = 12) {
  theme_minimal(base_size = base_size) %+replace%
    theme(
      plot.background  = element_rect(fill = "#FEFEFA", color = NA),
      panel.background = element_rect(fill = "#FEFEFA", color = NA),
      panel.grid.major = element_line(color = "#E5E5E5", linewidth = 0.4),
      panel.grid.minor = element_blank(),
      plot.title       = element_text(
                           size = base_size + 2, face = "bold",
                           color = brand_primary, margin = margin(b = 6)
                         ),
      plot.subtitle    = element_text(
                           size = base_size - 1, color = brand_secondary,
                           margin = margin(b = 10)
                         ),
      plot.caption     = element_text(
                           size = base_size - 2, color = brand_secondary,
                           hjust = 1, margin = margin(t = 8)
                         ),
      axis.title       = element_text(size = base_size - 1, color = brand_primary),
      axis.text        = element_text(size = base_size - 2, color = brand_secondary),
      legend.title     = element_text(size = base_size - 1, face = "bold",
                                      color = brand_primary),
      legend.text      = element_text(size = base_size - 2, color = brand_primary),
      legend.position  = "bottom",
      strip.text       = element_text(size = base_size - 1, face = "bold",
                                      color = brand_primary),
      strip.background = element_rect(fill = "#EAE5DD", color = NA),
      plot.margin      = margin(12, 16, 12, 12)
    )
}

theme_set(theme_brand())

What is Policy as Code?

A policy is a rule: who may access a system, how long data may be kept, what a valid password looks like. Traditionally, policies live in documents — PDFs, wikis, handbooks — and rely on humans to apply them consistently. That model has a known failure mode. Humans make exceptions, miss edge cases, and interpret ambiguous language differently on different days.

Policy as Code moves the rule from the document into executable logic. The policy is still a rule, but now it is a function: it takes inputs, evaluates conditions, and returns a deterministic verdict. It can be tested against known cases, version-controlled alongside the systems it governs, and run automatically in a pipeline rather than reviewed annually by a compliance officer.

The concept is not new — access control lists and firewall rules are policies expressed as code — but the term has gained currency as teams apply the pattern more broadly: to infrastructure configuration, data governance, software supply chains, and business approval workflows.

NoteThe importance of Policy as Code within Governance, Risk & Compliance (GRC)

Most compliance programs still run on PDFs. Someone writes a policy in a Word document, routes it through legal, gets it signed, and files it in a SharePoint folder where it quietly rots. When an auditor arrives eighteen months later, a small team rushes to show that the company’s actions match what the document states. This is the status quo, and it is expensive, slow, and fundamentally misrepresents how modern technology organizations operate. Policy as Code swaps the PDF for executable logic. It expresses compliance requirements in a programming language that machines can read. This allows for testing and automatic enforcement. The policy doesn’t describe what should happen. It makes it happen.

The practical payoff is speed and verifiability. When a policy is in code, it can be version-controlled like software. You can test it against live infrastructure quickly. Then, you can deploy it across thousands of systems without needing a person to copy requirements into a spreadsheet. A rule that says “all customer data must be encrypted at rest” stops being a matter of interpretation and starts being a check that runs continuously. Drift gets caught in hours, not quarters. For GRC teams, this collapses the gap between what the organization promises regulators and what it actually does, which is where nearly all compliance risk lives. Boards should care about this because that gap is what triggers eight-figure fines and front-page stories.

The harder question is organizational, not technical. Writing policy as code demands that legal, security, and engineering teams share a language, or at least build a reliable translation layer between their worlds. This transition requires compliance pros to review pull requests. Engineers must also see governance as a key design element, not just a checkbox to tick later. Companies that get this right will find that compliance becomes cheaper and faster over time, compounding like a good investment.

The example below is deliberately simple. It represents a problem that Policy as Code solves cleanly.

The Policy: Password Complexity

Here is a simple six-step password validation process where password is valid only if it satisfies these independent conditions: a minimum length of twelve characters, maximum length of 25 characters, at least one uppercase letter, at least one lowercase letter, at least one numeric digit, and at least one special character from this list @$!%*?&? Each condition is checked in sequence. The first failure terminates evaluation and returns a specific rejection reason.

The specificity of the failure message is a meaningful design choice. A prose policy that says “passwords must be complex” leaves interpretation to the user. A coded policy that returns FAIL: no special character tells the user exactly which requirement was not met — and tells the system exactly which condition to re-evaluate after the user makes a correction.

The Workflow

Display code
%%{init: {
  "theme": "base",
  "themeVariables": {
    "primaryColor": "#E6F1FB",
    "primaryTextColor": "#0C447C",
    "primaryBorderColor": "#185FA5",
    "lineColor": "#6E6E73",
    "secondaryColor": "#EAE5DD",
    "tertiaryColor": "#F5F5F5",
    "edgeLabelBackground": "#FEFEFA",
    "fontSize": "15px"
  }
}}%%
flowchart TD
    A([Input: password string]):::input --> B{"Length ≥ 12?"}

    B -->|No| F1([FAIL: too short]):::fail
    B -->|Yes| C{"Length ≤ 25?"}

    C -->|No| F2([FAIL: too long]):::fail
    C -->|Yes| D{"Contains at least 1 uppercase letter"}
    
    D -->|No| F3([FAIL: no uppercase letter]):::fail
    D -->|Yes| E{"Contains at least 1 lowercase letter?"}
    
    
    E -->|No| F$([FAIL: no lowercase letter]):::fail
    E -->|Yes| F{"Contains numeric digit?"}

    F -->|No| F5([FAIL: no numeric digit]):::fail
    F -->|Yes| G{"Contains special character?"}

    G -->|No| F6([FAIL: no special character]):::fail
    G -->|Yes| P([PASS: password accepted]):::pass

    classDef input   fill:#E6F1FB, stroke:#185FA5, stroke-width:1.5px, color:#0C447C
    classDef pass    fill:#E1F5EE, stroke:#0F6E56, stroke-width:1.5px, color:#085041
    classDef fail    fill:#FCEBEB, stroke:#A32D2D, stroke-width:1.5px, color:#791F1F

%%{init: {
  "theme": "base",
  "themeVariables": {
    "primaryColor": "#E6F1FB",
    "primaryTextColor": "#0C447C",
    "primaryBorderColor": "#185FA5",
    "lineColor": "#6E6E73",
    "secondaryColor": "#EAE5DD",
    "tertiaryColor": "#F5F5F5",
    "edgeLabelBackground": "#FEFEFA",
    "fontSize": "15px"
  }
}}%%
flowchart TD
    A([Input: password string]):::input --> B{"Length ≥ 12?"}

    B -->|No| F1([FAIL: too short]):::fail
    B -->|Yes| C{"Length ≤ 25?"}

    C -->|No| F2([FAIL: too long]):::fail
    C -->|Yes| D{"Contains at least 1 uppercase letter"}
    
    D -->|No| F3([FAIL: no uppercase letter]):::fail
    D -->|Yes| E{"Contains at least 1 lowercase letter?"}
    
    
    E -->|No| F$([FAIL: no lowercase letter]):::fail
    E -->|Yes| F{"Contains numeric digit?"}

    F -->|No| F5([FAIL: no numeric digit]):::fail
    F -->|Yes| G{"Contains special character?"}

    G -->|No| F6([FAIL: no special character]):::fail
    G -->|Yes| P([PASS: password accepted]):::pass

    classDef input   fill:#E6F1FB, stroke:#185FA5, stroke-width:1.5px, color:#0C447C
    classDef pass    fill:#E1F5EE, stroke:#0F6E56, stroke-width:1.5px, color:#085041
    classDef fail    fill:#FCEBEB, stroke:#A32D2D, stroke-width:1.5px, color:#791F1F

The Code

Here’s what the actual code looks like using regular expressions (Regex) to validate passwords in C#.

Step 1: Is the length ≥ 12 characters?
string basicRegex = "^.{8,}$";

Step 2: Is the length ≤ 25 characters?
string basicRegex = "^.{,25}$";

Step 3: Does it contain at least one uppercase letter?
string basicRegex = (?=.*[A-Z])

Step 4: Does it contain at least one lowercase letter?
string basicRegex = (?=.*[a-z])

Step 5: Does it contain a nummeric digit?
string basicRegex = (?=.*\d)

Step 6: Does it contain a special character?
string basicRegex = (?=.*[@$!%*?&])

Putting it all together becomes a single line of basicRegex:
string advancedRegex = "^{12,25}(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])$";

Why this is Policy as Code

The six conditions above are not hidden inside an application — they are explicit, readable, and auditable. A security team can review the rules directly. A developer can write unit tests against each branch. If the organization decides to raise the minimum length to sixteen characters, that is a one-line change with a clear diff in version history. If the organization want to add more rules to the policy (i.e. password can not match employee’s birthday, company ID, etc.), these steps can be added to both the workflow and accompanying code. Compare that to updating a PDF policy document, notifying users, and hoping the application enforces the new standard correctly.

Insights & Conclusion

The password example is trivial on purpose. Nobody needs a Quarto document to validate a twelve-character string. But the pattern it demonstrates — stating a rule as a function, testing that function against known inputs, and returning a specific, deterministic verdict — scales to problems that are not trivial at all. Infrastructure compliance, data retention, access provisioning, third-party risk scoring: each of these is a policy problem currently managed through prose documents that nobody reads with the same precision a machine would bring to the task.

What makes the pattern worth adopting is the failure message, not the pass. A PDF policy that says “passwords must be complex” generates ambiguity. The coded version that returns "Rejected: no uppercase letter found" eliminates it. That shift from vague guidance to specific, actionable output is the real product of expressing policy as code. It changes what a compliance team delivers from opinions about rules to evidence of enforcement.

The organizational cost is worth acknowledging. Writing policy as executable logic requires that compliance professionals learn to read code, or at least review pull requests with enough fluency to confirm the logic matches the intent. Engineers, in turn, need to treat governance constraints as first-class design inputs rather than afterthoughts bolted on before an audit. Neither adjustment is free, and both demand sustained sponsorship from leadership. The companies that defer this investment will continue to discover compliance gaps the same way they always have: during an incident, when the cost of remediation is highest.

Version control deserves a final word. When a policy lives in a Git repository, every change carries a timestamp, an author, and a diff. An auditor can reconstruct the entire history of a rule — when it was introduced, who modified it, what the previous version said — in minutes rather than weeks. That audit trail is not a feature of Policy as Code. It is perhaps the strongest argument for it.

Session Information

#> ─ Session info ───────────────────────────────────────────────────────────────
#>  setting  value
#>  version  R version 4.5.2 (2025-10-31)
#>  os       macOS Tahoe 26.2
#>  system   aarch64, darwin20
#>  ui       X11
#>  language (EN)
#>  collate  en_US.UTF-8
#>  ctype    en_US.UTF-8
#>  tz       America/New_York
#>  date     2026-04-29
#>  pandoc   3.8.3 @ /Applications/Positron.app/Contents/Resources/app/quarto/bin/tools/aarch64/ (via rmarkdown)
#>  quarto   1.8.26 @ /Applications/quarto/bin/quarto
#> 
#> ─ Packages ───────────────────────────────────────────────────────────────────
#>  package      * version date (UTC) lib source
#>  cli            3.6.5   2025-04-23 [1] CRAN (R 4.5.0)
#>  data.table     1.17.8  2025-07-10 [1] CRAN (R 4.5.0)
#>  digest         0.6.39  2025-11-19 [1] CRAN (R 4.5.2)
#>  dplyr        * 1.1.4   2023-11-17 [1] CRAN (R 4.5.0)
#>  evaluate       1.0.5   2025-08-27 [1] CRAN (R 4.5.0)
#>  farver         2.1.2   2024-05-13 [1] CRAN (R 4.5.0)
#>  fastmap        1.2.0   2024-05-15 [1] CRAN (R 4.5.0)
#>  forcats      * 1.0.1   2025-09-25 [1] CRAN (R 4.5.0)
#>  generics       0.1.4   2025-05-09 [1] CRAN (R 4.5.0)
#>  ggplot2      * 4.0.2   2026-02-03 [1] CRAN (R 4.5.2)
#>  glue           1.8.0   2024-09-30 [1] CRAN (R 4.5.0)
#>  gtable         0.3.6   2024-10-25 [1] CRAN (R 4.5.0)
#>  hms            1.1.4   2025-10-17 [1] CRAN (R 4.5.0)
#>  htmltools      0.5.8.1 2024-04-04 [1] CRAN (R 4.5.0)
#>  htmlwidgets    1.6.4   2023-12-06 [1] CRAN (R 4.5.0)
#>  httr           1.4.7   2023-08-15 [1] CRAN (R 4.5.0)
#>  jsonlite       2.0.0   2025-03-27 [1] CRAN (R 4.5.0)
#>  kableExtra   * 1.4.0   2024-01-24 [1] CRAN (R 4.5.0)
#>  knitr        * 1.50    2025-03-16 [1] CRAN (R 4.5.0)
#>  lazyeval       0.2.2   2019-03-15 [1] CRAN (R 4.5.0)
#>  lifecycle      1.0.5   2026-01-08 [1] CRAN (R 4.5.2)
#>  lubridate    * 1.9.4   2024-12-08 [1] CRAN (R 4.5.0)
#>  magrittr       2.0.4   2025-09-12 [1] CRAN (R 4.5.0)
#>  pillar         1.11.1  2025-09-17 [1] CRAN (R 4.5.0)
#>  pkgconfig      2.0.3   2019-09-22 [1] CRAN (R 4.5.0)
#>  plotly       * 4.11.0  2025-06-19 [1] CRAN (R 4.5.0)
#>  purrr        * 1.2.0   2025-11-04 [1] CRAN (R 4.5.0)
#>  R6             2.6.1   2025-02-15 [1] CRAN (R 4.5.0)
#>  RColorBrewer   1.1-3   2022-04-03 [1] CRAN (R 4.5.0)
#>  readr        * 2.1.5   2024-01-10 [1] CRAN (R 4.5.0)
#>  rlang          1.1.7   2026-01-09 [1] CRAN (R 4.5.2)
#>  rmarkdown      2.30    2025-09-28 [1] CRAN (R 4.5.0)
#>  rstudioapi     0.17.1  2024-10-22 [1] CRAN (R 4.5.0)
#>  S7             0.2.1   2025-11-14 [1] CRAN (R 4.5.2)
#>  scales       * 1.4.0   2025-04-24 [1] CRAN (R 4.5.0)
#>  sessioninfo  * 1.2.3   2025-02-05 [1] CRAN (R 4.5.0)
#>  stringi        1.8.7   2025-03-27 [1] CRAN (R 4.5.0)
#>  stringr      * 1.6.0   2025-11-04 [1] CRAN (R 4.5.0)
#>  svglite        2.2.2   2025-10-21 [1] CRAN (R 4.5.0)
#>  systemfonts    1.3.1   2025-10-01 [1] CRAN (R 4.5.0)
#>  textshaping    1.0.4   2025-10-10 [1] CRAN (R 4.5.0)
#>  tibble       * 3.3.0   2025-06-08 [1] CRAN (R 4.5.0)
#>  tidyr        * 1.3.1   2024-01-24 [1] CRAN (R 4.5.0)
#>  tidyselect     1.2.1   2024-03-11 [1] CRAN (R 4.5.0)
#>  tidyverse    * 2.0.0   2023-02-22 [1] CRAN (R 4.5.0)
#>  timechange     0.3.0   2024-01-18 [1] CRAN (R 4.5.0)
#>  tzdb           0.5.0   2025-03-15 [1] CRAN (R 4.5.0)
#>  vctrs          0.7.1   2026-01-23 [1] CRAN (R 4.5.2)
#>  viridisLite    0.4.3   2026-02-04 [1] CRAN (R 4.5.2)
#>  withr          3.0.2   2024-10-28 [1] CRAN (R 4.5.0)
#>  xfun           0.54    2025-10-30 [1] CRAN (R 4.5.0)
#>  xml2           1.4.1   2025-10-27 [1] CRAN (R 4.5.0)
#>  yaml           2.3.10  2024-07-26 [1] CRAN (R 4.5.0)
#> 
#>  [1] /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library
#>  * ── Packages attached to the search path.
#> 
#> ──────────────────────────────────────────────────────────────────────────────

Rendered with Quarto + Mermaid | Packages: dplyr kableExtra knitr scales sessioninfo tidyverse