Policies
There are two types of Policies:
- 1.Access Policies define if someone should get access.
- 2.
policies = [
# Access Policies
{
bundle = "github://organization/repo/path/to/policies[.tar.gz]"
# or,
# query = <<-EOT
# package main
#
# ...
# EOT
},
...
]
workflow = {
steps = [
{
...
# Workflow Policies
skip_if = [
{
# bundle = "..."
# query = "..."
}
]
}
]
}
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.
1
// Grant Kit with an Access Policy containing a single bundle.
2
policies = [
3
{ bundle = "..." }
4
]
5
6
// Grant Kit with an Access Policy containing two bundles.
7
policies = [
8
{ bundle = "..." },
9
{ bundle = "..." },
10
]
11
12
// Grant Kit with an Access Policy containing two bundles and a single policy.
13
policies = [
14
{ bundle = "..." },
15
{ bundle = "..." },
16
{ query = "..." },
17
]
18
19
// Grant Kit with a single step configured with a Workflow Policy that
20
// contains a single bundle.
21
steps = [
22
{
23
skip_if = [
24
{ bundle = "..." }
25
]
26
}
27
]
28
29
// Grant Kit with two steps, one of which is configured with
30
// a Workflow Policy that contains a single bundle and the
31
// other with a Workflow Policy that contain a bundle and a query.
32
steps = [
33
{
34
skip_if = [
35
{ bundle = "..." }
36
]
37
},
38
{
39
skip_if = [
40
{ bundle = "..." },
41
{ query = "..." }
42
]
43
}
44
]
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.github://{organization}/{repository}/path/to/my/bundle[.tar.gz]
// 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.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..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.# 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
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.// 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}"
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:
Access Policies and Workflow Policies are implemented using identical schemas.
Access Policies are written using the following schema:
1
allow[msg] {
2
// Policy logic goes here.
3
msg := "return an explanation for why the policy passes"
4
}
1
// Access Policy that always grants access because the rule will always
2
// return `true`.
3
allow[msg] {
4
true
5
msg := "granting access for non-sensitive resource"
6
}
7
8
// Access Policy that always grants access, but expires after 24 hours.
9
// Assumes you added `import data.abbey.functions` in your policy.
10
allow[msg] {
11
true; functions.expire_after("24h")
12
msg := "always approves access but will eventually expire"
13
}
14
15
// Access Policy that always grants access, but expires at a specific date.
16
// Assumes you added `import data.abbey.functions` in your policy.
17
allow[msg] {
18
true; functions.expire_at("2023-06-16T07:15:58+00:00"
19
msg := "always approves but will expire at a specific time"
20
}
21
22
// Access Policy that grants access to sensitive infrastructure if the
23
// requester is currently on call.
24
// `input` comes from infrastructure changes, generally `tfplan.json` via
25
// your CI's `terraform plan` output.
26
// `data` is automatically enriched for you by Abbey.
27
allow[msg] {
28
input.resource_changes[_].change.after.database_name == "pii_customers"
29
data.system.abbey.pagerduty.isoncall == true
30
msg := "for the pii_customers database, grant access only to on-calls. everyone else is denied by default"
31
}
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.Workflow Policies are written using the following schema:
1
skip[msg] {
2
// Policy logic goes here.
3
msg := "return an explanation for why this step was skipped"
4
}
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:
- 1.
- 2.
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.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": [
]
},
"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": [
]
},
"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": "[email protected]",
"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": [
]
}
}
]
}
}
},
"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 represents properties from external systems 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.
Usage
- To use Enriched Data, use the
data
object in your code with thesystem.abbey
namespace.
For example, given the following
data
:{
"system": {
"abbey": {
"identities": {
"snowflake": {
},
"pagerduty": {
"isoncall": true
}
}
}
}
}
You can define an Access Policy that automatically denies access to someone if they're not on-call:
deny[msg] {
data.system.abbey.identities.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.