Policies

Policies, written as Open Policy Agent (OPA) Policies in Rego, define if someone should get access.

There are two types of Policies:

  1. Access Policies define if someone should get access.

  2. Workflow Policies define if a Workflow Step should be skipped.

Polices Spec

Access Policies

policies = [
    # Access Policies
    {
        bundle = "github://organization/repo/path/to/policies[.tar.gz]"
        # or,
        # query = <<-EOT
        #     package main
        #
        #     ...
        # EOT
    },
    ...   
]

Workflow Policies

workflow = {
    steps = [
        {
            ...
            # Workflow Policies
            skip_if = [
                {
                    # bundle = "..."
                    # query = "..."
                }
            ]
        }
    ]
}

Configuring Policies For Your Grant Kit

Access Policies and Workflow Policies are configured in identical ways.

  • For Access Policies, put your policies in the policies attribute.

  • For Workflow Policies, use the skip_if attribute.

These attributes represent a list of OPA Policies as either a bundle or a query.

Example Policy Configurations
// Grant Kit with an Access Policy containing a single bundle.
policies = [
    { bundle = "..." }
]

// Grant Kit with an Access Policy containing two bundles.
policies = [
    { bundle = "..." },
    { bundle = "..." },
]

// Grant Kit with an Access Policy containing two bundles and a single policy.
policies = [
    { bundle = "..." },
    { bundle = "..." },
    { query = "..." },
]

// Grant Kit with a single step configured with a Workflow Policy that
// contains a single bundle.
steps = [
    {
        skip_if = [
            { bundle = "..." }
        ]
    }
]

// Grant Kit with two steps, one of which is configured with
// a Workflow Policy that contains a single bundle and the
// other with a Workflow Policy that contain a bundle and a query.
steps = [
    {
        skip_if = [
            { bundle = "..." }
        ]
    },
    {
        skip_if = [
            { bundle = "..." },
            { query = "..." }
        ]
    }
]

Bundles

A Bundle is a native OPA Bundle that represents a group of OPA Policies. Bundles are typically either a directory or a gzipped tarball.

Bundles are configured using an RFC 3986 URI string for the bundle attribute. This string must point to the location of your bundle in your code repository.

Bundle Spec

github://{organization}/{repository}/path/to/my/bundle[.tar.gz]
Example Bundle Configurations
// Organization: abbeylabs
// Repository: starter-kit-quickstart
// Path to policy bundle: /policies/soc2.tar.gz
//
// This example assumes the soc2.tar.gz was built ahead of time
// using `opa build` and is committed directly to the
// `starter-kit-quickstart` repository.
github://abbeylabs/starter-kit-quickstart/policies/soc2.tar.gz

// Organization: jeffchao (personal, not a GitHub org)
// Repository: my-starter-kit
// Path to policy bundle: /policies/access-policies/hipaa
//
// This example assumes `opa build` was not used and instead prefers
// Abbey to build the bundle instead. Abbey will inspect the `hipaa`
// directory and recursively add all OPA Policies defined with
// the `.rego` extension to the bundle.
github://jeffchao/my-starter-kit/policies/access-policies/hipaa

Currently Abbey supports the github:// scheme. Future schemes such as file://, s3://, and https:// coming soon.

Organizing Bundles

Bundles are great for distributing and reusing policies.

Before building bundles, you should first organize your policies into a directory structure that represents the functionality of the bundle, for example:

❯ tree -a
.
└── policies
    ├── soc2
    │   ├── .manifest
    │   └── soc2-type2
    │       └── example.rego
    └── privacy
        ├── .manifest
        └── privacy-gdpr
            └── example.rego

5 directories, 4 files

The above setup has a policies directory that contains 2 bundles, each with 1 package in them. The bundles contain a .manifest file to define the packages it knows about and is used to avoid package conflicts.

Example .manifest file
{
    "roots": [
        "soc2-type2",
        "privacy-gdpr"
    ]
}

.manifest files are required if you're using OPA bundles. They help avoid package conflicts. As we add more packages, we add their paths to the .manifest file.

Building Bundles

Once you have your directory set up, you can build your bundles using the OPA CLI, for example:

# Build the `soc2-type2` bundle from the `soc2` directory and
# Output the bundle to the `policies/soc2` directory.
opa build -b policies/soc2/soc2-type2 -o policies/soc2/soc2-type2.tar.gz

# Build the `privacy-gdpr` bundle from the `privacy` directory and
# Output the bundle to the `policies/privacy` directory.
opa build -b policies/privacy/privacy-gdpr -o policies/privacy/privacy-gdpr.tar.gz

Queries

A Query represents a single OPA Policy. Queries are typically used for simple one-off rules that don't require the hierarchical rule organization that Bundles provide and aren't intended to be distributed.

Queries are configured by writing native Rego code directly as a string, typically a multiline Heredoc string for better visibility.

Queries must be defined using the main namespace for Abbey to evaluate it.

Example Query Configurations
// Policies defined using `query` must use `package main`.

// This example shows the use of a multiline Heredoc string to define a simple
// Policy that always denies access.
query = <<-EOT
    package main
    
    deny[msg] {
        true
        msg := "always deny access because we're always returning true"
    }
EOT

// This example shows the same policy above, but using a single line string.
// The result is the same but the configuration is harder to read.
query = "package main\n\ndeny[msg] {\n\ttrue\n\tmsg := \"always deny access because we're always returning true\"\n}"

Writing Policies For Your Grant Kit

Policies are implemented using the Abbey OPA Constraint Framework. This Framework is simplified subset of the OPA Constraint Framework that makes working with OPA and Rego easier.

Writing Policies consists of three steps:

Define Your Logic Using Rego

Access Policies and Workflow Policies are implemented using identical schemas.

Writing Access Policies

Access Policies are written using the following schema:

allow[msg] {
    // Policy logic goes here.
    msg := "return an explanation for why the policy passes"
}
Example Access Policies
// Access Policy that always grants access because the rule will always
// return `true`.
allow[msg] {
    true
    msg := "granting access for non-sensitive resource"
}

// Access Policy that always grants access, but expires after 24 hours.
// Assumes you added `import data.abbey.functions` in your policy.
allow[msg] {
    true; functions.expire_after("24h")
    msg := "always approves access but will eventually expire"
}

// Access Policy that always grants access, but expires at a specific date.
// Assumes you added `import data.abbey.functions` in your policy.
allow[msg] {
    true; functions.expire_at("2023-06-16T07:15:58+00:00"
    msg := "always approves but will expire at a specific time"
}

// Access Policy that grants access to sensitive infrastructure if the
// requester is currently on call.
// `input` comes from infrastructure changes, generally `tfplan.json` via
// your CI's `terraform plan` output.
// `data` is automatically enriched for you by Abbey.
allow[msg] {
    input.resource_changes[_].change.after.database_name == "pii_customers"
    data.system.abbey.pagerduty.isoncall == true
    msg := "for the pii_customers database, grant access only to on-calls. everyone else is denied by default"
}

Automatic Revocation

Once access is granted, Abbey will continuously monitor your policies and revoke access in realtime if they evaluate to false. This may happen if you set an expiration through functions.expire_at or functions.expire_after, a user's attribute has changed (e.g., they went off-call), or infrastructure has changed (e.g., a database was now marked as sensitive). You get this functionality out-of-the-box without having to configure anything extra.

Writing Workflow Policies

Workflow Policies are written using the following schema:

skip[msg] {
    // Policy logic goes here.
    msg := "return an explanation for why this step was skipped"
}
Example Workflow Policies
// Workflow Policy that always skips its step because the `skip` rule
// always evaluates to `true`.
skip[msg] {
    true
    msg := "always skip this step"
}

Use Policy Inputs and Enriched Data In Your Logic

In order to write meaningful policies, you need to be able to compare properties across different systems.

For example, you might want to create a policy that denies access to a sensitive database table by default, unless they're an engineer and active on an on-call rotation. "Sensitive database table", "engineer", and "active on an on-call rotation" are properties from systems such as Terraform Plan, GitHub Teams, and PagerDuty Schedules.

Abbey provides two categories of external data for you when writing your policies:

Policy Inputs

Policy Inputs represent the Terraform Plan output that contains the preview of changes that Terraform plans to make to your infrastructure. This allows you to write policies based on what your infrastructure looks like.

Usage

  • To use Policy Inputs, use the input object in your Rego code.

For example, given the following Terraform Plan output as your input:

{
    "resource_changes": [
        {
            "change": {
                "after": {
                    "role_name": "PII_READONLY"
                }
            }
        }
    ]
}

You can define an Access Policy that automatically denies access to anyone attempting to get access to the PII_READONLY role:

deny[msg] {
    input.resource_changes.change.after.role_name == "PII_READONLY"
    msg := "auto deny anyone attempting to get access to the PII_READONLY role"
}

The input schema is native to Terraform and has many other attributes available for you to use.

Full Example of input
input.json
{
  "format_version": "1.1",
  "terraform_version": "1.4.2",
  "variables": {
    "account": {
      "value": "***"
    },
    "password": "***",
    "username": {
      "value": "***"
    }
  },
  "planned_values": {
    "root_module": {
      "resources": [
        {
          "address": "abbey_grant_kit.role__pii_readonly",
          "mode": "managed",
          "type": "abbey_grant_kit",
          "name": "role__pii_readonly",
          "provider_name": "registry.terraform.io/abbeylabs/abbey",
          "schema_version": 0,
          "values": {
            "description": "Grants access to the PII READONLY Snowflake Role Grant.\n",
            "name": "Abbey Alpha Demo: PII READONLY role grant",
            "output": {
              "append": "resource \"snowflake_role_grants\" \"pii_readonly__{{ .data.system.abbey.secondary_identities.snowflake.username }}\" {\n  role_name = \"PII_READONLY\"\n  users     = [\"{{ .data.system.abbey.secondary_identities.snowflake.username }}\"]\n}\n",
              "location": "github://organization/repo/access.tf",
              "overwrite": null
            },
            "policies": [
              {
                "bundle": null,
                "query": "package main\n\nwarn[msg] {\n  input.resource_changes[_].change.after.database_name == \"DATABASE\"\n  msg := \"be careful granting access to sensitive data\"\n}\n"
              }
            ],
            "workflow": {
              "steps": [
                {
                  "reviewers": {
                    "all_of": null,
                    "one_of": [
                      "replace-me@abbey.so"
                    ]
                  },
                  "skip_if": null
                }
              ]
            }
          },
          "sensitive_values": {
            "output": {},
            "policies": [],
            "workflow": {
              "steps": [
                {
                  "reviewers": {
                    "one_of": [
                      false
                    ]
                  }
                }
              ]
            }
          }
        },
        {
          "address": "snowflake_role_grants.pii_readonly__REPLACE_ME",
          "mode": "managed",
          "type": "snowflake_role_grants",
          "name": "pii_readonly__REPLACE_ME",
          "provider_name": "registry.terraform.io/snowflake-labs/snowflake",
          "schema_version": 0,
          "values": {
            "enable_multiple_grants": false,
            "role_name": "PII_READONLY",
            "roles": null,
            "users": [
              "REPLACE_ME"
            ]
          },
          "sensitive_values": {
            "users": [
              false
            ]
          }
        },
        {
          "address": "snowflake_table_grant.pii_readonly__can_read__pii__table",
          "mode": "managed",
          "type": "snowflake_table_grant",
          "name": "pii_readonly__can_read__pii__table",
          "provider_name": "registry.terraform.io/snowflake-labs/snowflake",
          "schema_version": 0,
          "values": {
            "database_name": "DATABASE",
            "enable_multiple_grants": false,
            "on_future": false,
            "privilege": "SELECT",
            "roles": [
              "PII_READONLY"
            ],
            "schema_name": "SCHEMA",
            "shares": null,
            "table_name": "TABLE",
            "with_grant_option": false
          },
          "sensitive_values": {
            "roles": [
              false
            ]
          }
        }
      ]
    }
  },
  "resource_changes": [
    {
      "address": "abbey_grant_kit.role__pii_readonly",
      "mode": "managed",
      "type": "abbey_grant_kit",
      "name": "role__pii_readonly",
      "provider_name": "registry.terraform.io/abbeylabs/abbey",
      "change": {
        "actions": [
          "create"
        ],
        "before": null,
        "after": {
          "description": "Grants access to the PII READONLY Snowflake Role Grant.\n",
          "name": "Name",
          "output": {
            "append": "resource \"snowflake_role_grants\" \"pii_readonly__{{ .data.system.abbey.secondary_identities.snowflake.username }}\" {\n  role_name = \"PII_READONLY\"\n  users     = [\"{{ .data.system.abbey.secondary_identities.snowflake.username }}\"]\n}\n",
            "location": "github://organization/repo/access.tf",
            "overwrite": null
          },
          "policies": [
            {
              "bundle": null,
              "query": "package main\n\nwarn[msg] {\n  input.resource_changes[_].change.after.database_name == \"DATABASE\"\n  msg := \"be careful granting access to sensitive data\"\n}\n"
            }
          ],
          "workflow": {
            "steps": [
              {
                "reviewers": {
                  "all_of": null,
                  "one_of": [
                    "replace-me@example.com"
                  ]
                },
                "skip_if": null
              }
            ]
          }
        },
        "after_unknown": {
          "id": true,
          "output": {},
          "policies": [],
          "workflow": {
            "steps": [
              {
                "reviewers": {
                  "one_of": [
                    false
                  ]
                }
              }
            ]
          }
        },
        "before_sensitive": false,
        "after_sensitive": {
          "output": {},
          "policies": ,
          "workflow": {
            "steps": [
              {
                "reviewers": {
                  "one_of": [
                    false
                  ]
                }
              }
            ]
          }
        }
      }
    },
    {
      "address": "snowflake_role_grants.pii_readonly__REPLACE_ME",
      "mode": "managed",
      "type": "snowflake_role_grants",
      "name": "pii_readonly__REPLACE_ME",
      "provider_name": "registry.terraform.io/snowflake-labs/snowflake",
      "change": {
        "actions": [
          "create"
        ],
        "before": null,
        "after": {
          "enable_multiple_grants": false,
          "role_name": "PII_READONLY",
          "roles": null,
          "users": [
            "***"
          ]
        },
        "after_unknown": {
          "id": true,
          "users": [
            false
          ]
        },
        "before_sensitive": false,
        "after_sensitive": {
          "users": [
            false
          ]
        }
      }
    },
    {
      "address": "snowflake_table_grant.pii_readonly__can_read__pii__table",
      "mode": "managed",
      "type": "snowflake_table_grant",
      "name": "pii_readonly__can_read__pii__table",
      "provider_name": "registry.terraform.io/snowflake-labs/snowflake",
      "change": {
        "actions": [
          "create"
        ],
        "before": null,
        "after": {
          "database_name": "DATABASE",
          "enable_multiple_grants": false,
          "on_future": false,
          "privilege": "SELECT",
          "roles": [
            "PII_READONLY"
          ],
          "schema_name": "SCHEMA",
          "shares": null,
          "table_name": "TABLE",
          "with_grant_option": false
        },
        "after_unknown": {
          "id": true,
          "roles": [
            false
          ]
        },
        "before_sensitive": false,
        "after_sensitive": {
          "roles": [
            false
          ]
        }
      }
    }
  ],
  "prior_state": {
    "format_version": "1.0",
    "terraform_version": "1.4.2",
    "values": {
      "root_module": {
        "resources": [
          {
            "address": "data.snowflake_database.pii_database",
            "mode": "data",
            "type": "snowflake_database",
            "name": "pii_database",
            "provider_name": "registry.terraform.io/snowflake-labs/snowflake",
            "schema_version": 0,
            "values": {
              "comment": "",
              "created_on": "DATE",
              "id": "DATABASE",
              "is_current": false,
              "is_default": false,
              "name": "DATABASE",
              "options": "",
              "origin": "",
              "owner": "ACCOUNTADMIN",
              "retention_time": 1
            },
            "sensitive_values": {}
          },
          {
            "address": "data.snowflake_role.pii_readonly_role",
            "mode": "data",
            "type": "snowflake_role",
            "name": "pii_readonly_role",
            "provider_name": "registry.terraform.io/snowflake-labs/snowflake",
            "schema_version": 0,
            "values": {
              "comment": "This role allows identities to read tables which contain PII",
              "id": "PII_READONLY",
              "name": "PII_READONLY"
            },
            "sensitive_values": {}
          },
          {
            "address": "data.snowflake_users.my_snowflake_user",
            "mode": "data",
            "type": "snowflake_users",
            "name": "my_snowflake_user",
            "provider_name": "registry.terraform.io/snowflake-labs/snowflake",
            "schema_version": 0,
            "values": {
              "id": "VMB31206.AWS_US_WEST_2",
              "pattern": "REPLACE_ME",
              "users": [
                {
                  "comment": "",
                  "default_namespace": "",
                  "default_role": "ROLE",
                  "default_secondary_roles": [
                    ""
                  ],
                  "default_warehouse": "",
                  "disabled": false,
                  "display_name": "REPLACE_ME",
                  "email": "replace-me-snowflake@abbey.so",
                  "first_name": "Firstname",
                  "has_rsa_public_key": false,
                  "last_name": "Lastname",
                  "login_name": "REPLACE_ME",
                  "name": "REPLACE_ME"
                }
              ]
            },
            "sensitive_values": {
              "users": [
                {
                  "default_secondary_roles": [
                    false
                  ]
                }
              ]
            }
          }
        ]
      }
    }
  },
  "configuration": {
    "provider_config": {
      "abbey": {
        "name": "abbey",
        "full_name": "registry.terraform.io/abbeylabs/abbey",
        "version_constraint": "0.1.2"
      },
      "snowflake": {
        "name": "snowflake",
        "full_name": "registry.terraform.io/snowflake-labs/snowflake",
        "version_constraint": "0.56.5",
        "expressions": {
          "account": {
            "references": [
              "var.account"
            ]
          },
          "password": {
            "references": [
              "var.password"
            ]
          },
          "username": {
            "references": [
              "var.username"
            ]
          }
        }
      }
    },
    "root_module": {
      "resources": [
        {
          "address": "abbey_grant_kit.role__pii_readonly",
          "mode": "managed",
          "type": "abbey_grant_kit",
          "name": "role__pii_readonly",
          "provider_config_key": "abbey",
          "expressions": {
            "description": {
              "constant_value": "Grants access to the PII READONLY Snowflake Role Grant.\n"
            },
            "name": {
              "constant_value": "Name"
            },
            "output": {
              "references": [
                "data.snowflake_role.pii_readonly_role.name",
                "data.snowflake_role.pii_readonly_role"
              ]
            },
            "policies": {
              "references": [
                "data.snowflake_database.pii_database.name",
                "data.snowflake_database.pii_database"
              ]
            },
            "workflow": {
              "constant_value": {
                "steps": [
                  {
                    "reviewers": {
                      "one_of": [
                        "replace-me@abbey.so"
                      ]
                    }
                  }
                ]
              }
            }
          },
          "schema_version": 0
        },
        {
          "address": "snowflake_role_grants.pii_readonly__REPLACE_ME",
          "mode": "managed",
          "type": "snowflake_role_grants",
          "name": "pii_readonly__REPLACE_ME",
          "provider_config_key": "snowflake",
          "expressions": {
            "role_name": {
              "constant_value": "PII_READONLY"
            },
            "users": {
              "constant_value": [
                "REPLACE_ME"
              ]
            }
          },
          "schema_version": 0
        },
        {
          "address": "snowflake_table_grant.pii_readonly__can_read__pii__table",
          "mode": "managed",
          "type": "snowflake_table_grant",
          "name": "pii_readonly__can_read__pii__table",
          "provider_config_key": "snowflake",
          "expressions": {
            "database_name": {
              "references": [
                "data.snowflake_database.pii_database.name",
                "data.snowflake_database.pii_database"
              ]
            },
            "privilege": {
              "constant_value": "SELECT"
            },
            "roles": {
              "references": [
                "data.snowflake_role.pii_readonly_role.name",
                "data.snowflake_role.pii_readonly_role"
              ]
            },
            "schema_name": {
              "constant_value": "SCHEMA"
            },
            "table_name": {
              "constant_value": "TABLE"
            },
            "with_grant_option": {
              "constant_value": false
            }
          },
          "schema_version": 0
        },
        {
          "address": "data.snowflake_database.pii_database",
          "mode": "data",
          "type": "snowflake_database",
          "name": "pii_database",
          "provider_config_key": "snowflake",
          "expressions": {
            "name": {
              "constant_value": "DATABASE"
            }
          },
          "schema_version": 0
        },
        {
          "address": "data.snowflake_role.pii_readonly_role",
          "mode": "data",
          "type": "snowflake_role",
          "name": "pii_readonly_role",
          "provider_config_key": "snowflake",
          "expressions": {
            "name": {
              "constant_value": "PII_READONLY"
            }
          },
          "schema_version": 0
        },
        {
          "address": "data.snowflake_users.my_snowflake_user",
          "mode": "data",
          "type": "snowflake_users",
          "name": "my_snowflake_user",
          "provider_config_key": "snowflake",
          "expressions": {
            "pattern": {
              "constant_value": "REPLACE_ME"
            }
          },
          "schema_version": 0
        }
      ],
      "variables": {
        "account": {
          "description": "Snowflake account",
          "sensitive": true
        },
        "password": {
          "description": "Snowflake password",
          "sensitive": true
        },
        "username": {
          "description": "Snowflake username",
          "sensitive": true
        }
      }
    }
  },
  "relevant_attributes": [
    {
      "resource": "data.snowflake_role.pii_readonly_role",
      "attribute": [
        "name"
      ]
    },
    {
      "resource": "data.snowflake_database.pii_database",
      "attribute": [
        "name"
      ]
    }
  ]
}

input is accessible for both Access Policies and Workflow Policies.

Enriched Data

Enriched Data represents properties from external applications that generally contain information about who someone is. This allows you to write policies based on roles, relationships, or attributes of a person. Abbey automatically enriches data for you, based on identities you or your admin have imported into Abbey. For more information about importing application data, go toLink Identities.

Usage

  • To use Enriched Data, use the data object in your code with the user namespace.

For example, given the following data:

{
    "user": {
        "abbey": {
            "email": "alice@example.com"
        },
        "pagerduty": {
            "isoncall": true
        },
        "github": {
            "username": "Alice"
        }
    }
}

You can define an Access Policy that automatically denies access to someone if they're not on-call:

deny[msg] {
    data.user.pagerduty.isoncall == false
}

The data schema is a component of the Abbey OPA Constraint Framework. As you connect more systems to the Abbey Platform, Abbey will automatically pick them up and enrich the data for you to use in your policies.

Schema of data
data.json
{
    "user": {
        "abbey": {
            "abbey":{
                "email":...
            }
            "snowflake": {
                ...
            },
            "pagerduty": {
                ...
            },
            "aws_iam": {
                ...
            },
            "gcp": {
                ...
            },
            "github": {
                ...
            },
            "okta": {
                ...
            },
            "other-identity": {
                ...
            }
        },
        "system": {
            "abbey": {
                "target": {
                    "grant": {
                        "granted_at": "RFC 3339",
                        "revoked_at": "RFC 3339"
                    }
                }            
            }
        },
    }
}

data is accessible in all Policy types: Access, Revocation, and Workflow.

Policy Evaluation

Abbey evaluates your policies using Abbey's distributed Policy Evaluation Engine. This engine will evaluate your Access, Revocation, and Workflow Policies.

Policy Evaluation has four stages:

  1. Get Policy Input.

    • This is typically the output of your tfplan.json from your terraform plan command.

  2. Fetch and enrich Data.

    • Abbey will automatically fetch and enrich data for any system you have connected.

  3. Evaluate your OPA Policies.

    • Pass in your Policy Input and Enriched Data as parameters.

  4. Output evaluation results and warning details.

    • Output a single boolean value along with a key-value object of evaluation and warning details.

Guardrail Policies

Abbey supports reusable policies that can be distributed using Bundles. You can leverage this feature to provide Guardrail Policies. These are policies that can be imported into other policies and cannot be overridden.

There are two primary use cases:

  1. You're a member of a security or IT team that wants to instill set guardrails around access for the rest of your company. For example, set some sort of non-overridable policy around PII data.

  2. You don't want to write your own policies. Instead, you prefer to leverage existing, battle-tested policies. For example, you can pass in an OPA bundle in your Grant Kit that already has all the necessary Rego rules for compliance for a specific target system.

To create a Guardrail Policy, create a policy or bundle as you would normally and import it into any other policy you want to use in your Grant Kits.

Troubleshooting

A common deployment failure for grant kits is a misconfigured bundle field in the policies block

  • Double check it starts with github://

  • Double check repository and username or org name is correct

    • These fields are case-sensitive so double check any upper/lowercase letters

  • Double check for any extra : or /'s

  • Double check the path to your policies exists

Last updated