Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple policies compose in a surprising way #5988

Closed
chadparry opened this issue Dec 21, 2018 · 4 comments
Closed

Multiple policies compose in a surprising way #5988

chadparry opened this issue Dec 21, 2018 · 4 comments

Comments

@chadparry
Copy link

chadparry commented Dec 21, 2018

Describe the bug
When a token is associated with multiple policies, it would be expected that those policies each add more permissions, not take them away. However, the actual behavior is that additional policies might be able to remove permissions.

Suppose that a policy grants the capability to access the path /foo/* but denies access to /bar/*. A separate policy grants access to /bar/* but denies access to /foo/*. Combining these policies leads to a token that has fewer permissions than a token that doesn't have those policies.

When policies are combined, it's expected that the permissions are cumulative. That's because that's the security model that seems to be supported. When a parent token creates a child token, it is allowed to selectively pass policies down to the child. If some policies were meant for denying permissions, then the parent would be able to circumvent those restrictions when creating a child token.

Or alternatively, the security model should clarify that it is supported to use policies to remove permissions. To go that route, it would make sense for parent tokens to only be allowed to create child tokens that had exactly their same policies. It would be unsafe to allow parents to specify a subset of policies if policies are indeed supposed to be able to restrict permissions.

To Reproduce
This issue affects a wide variety of paths throughout Vault. This particular example shows the difficulty in creating a policy that allows creating an AppRole and getting its secret. These are the configuration steps to create the token:

# Input the Vault root token
read -p "Vault root token: " -s VAULT_TOKEN
# Identify the Vault server
export VAULT_ADDR="http://127.0.0.1:8200"

# Create a policy to be used by the new AppRoles
curl -s --header "X-Vault-Token: $VAULT_TOKEN" --request PUT --data "$(jq -n --arg POLICY_BODY "$(<<'END' cat
# Some capabilities could be added here
END
)" '{ "policy": $POLICY_BODY }')" "$VAULT_ADDR/v1/sys/policy/restricted-approle"

# Create a policy that can create AppRoles
curl -s --header "X-Vault-Token: $VAULT_TOKEN" --request PUT --data "$(jq -n --arg POLICY_BODY "$(<<'END' cat
path "auth/approle/role/*" {
  capabilities = ["create", "update"]
  required_parameters = ["policies"]
  allowed_parameters = {
    "policies" = [["restricted-approle"]]
    "secret_id_ttl" = ["5m"]
    "token_max_ttl" = ["5m"]
  }
}
END
)" '{ "policy": $POLICY_BODY }')" "$VAULT_ADDR/v1/sys/policy/approle-creator"

# Create a policy that can read AppRole IDs
curl -s --header "X-Vault-Token: $VAULT_TOKEN" --request PUT --data "$(jq -n --arg POLICY_BODY "$(<<'END' cat
# The intention is to restrict the path to "auth/approle/role/*/role-id", but wildcards are only supported at the end of a path.
path "auth/approle/role/*" {
  capabilities = ["read"]
  denied_parameters = {
    "*" = []
  }
}
END
)" '{ "policy": $POLICY_BODY }')" "$VAULT_ADDR/v1/sys/policy/approle-reader"

# Create a policy that can read AppRole secrets
curl -s --header "X-Vault-Token: $VAULT_TOKEN" --request PUT --data "$(jq -n --arg POLICY_BODY "$(<<'END' cat
# The intention is to restrict the path to "auth/approle/role/*/secret-id", but wildcards are only supported at the end of a path.
path "auth/approle/role/*" {
  capabilities = ["create", "update"]
  denied_parameters = {
    // This policy is not allowed to attach other policies to an AppRole
    "*" = []
  }
}
END
)" '{ "policy": $POLICY_BODY }')" "$VAULT_ADDR/v1/sys/policy/approle-secrets"

# Create a parent policy for the rest of the AppRole tasks
curl -s --header "X-Vault-Token: $VAULT_TOKEN" --request PUT --data "$(jq -n --arg POLICY_BODY "$(<<END cat
path "auth/token/create" {
  capabilities = ["create", "update"]
  required_parameters = ["policies", "num_uses", "ttl"]
  allowed_parameters = {
    "policies" = [["approle-creator"], ["approle-reader"], ["approle-secrets"]]
    "num_uses" = ["1"]
    "ttl" = ["5m"]
  }
}
END
)" '{ "policy": $POLICY_BODY }')" "$VAULT_ADDR/v1/sys/policy/parent"

# Create a token for the parent
PARENT_TOKEN="$(curl -s --header "X-Vault-Token: $VAULT_TOKEN" --data '{ "policies": ["parent", "approle-creator", "approle-reader", "approle-secrets"] }' "$VAULT_ADDR/v1/auth/token/create" | jq -r .auth.client_token)"

Then, you can verify that the parent token is denied access to the AppRole endpoints:

curl -s --header "X-Vault-Token: $PARENT_TOKEN" --data '{ "policies": ["restricted-approle"], "secret_id_ttl": "5m", "token_max_ttl": "5m" }' "$VAULT_ADDR/v1/auth/approle/role/test-approle"

The command reports permission denied.

Strangely, the parent token is allowed to create child tokens that are given access to the AppRole endpoints:

CREATOR_TOKEN="$(curl -s --header "X-Vault-Token: $PARENT_TOKEN" --data '{ "policies": ["approle-creator"], "num_uses": "1", "ttl": "5m" }' "$VAULT_ADDR/v1/auth/token/create" | jq -r .auth.client_token)"
READER_TOKEN="$(curl -s --header "X-Vault-Token: $PARENT_TOKEN" --data '{ "policies": ["approle-reader"], "num_uses": "1", "ttl": "5m" }' "$VAULT_ADDR/v1/auth/token/create" | jq -r .auth.client_token)"
SECRETS_TOKEN="$(curl -s --header "X-Vault-Token: $PARENT_TOKEN" --data '{ "policies": ["approle-secrets"], "num_uses": "1", "ttl": "5m" }' "$VAULT_ADDR/v1/auth/token/create" | jq -r .auth.client_token)"

curl -s --header "X-Vault-Token: $CREATOR_TOKEN" --data '{ "policies": ["restricted-approle"], "secret_id_ttl": "5m", "token_max_ttl": "5m" }' "$VAULT_ADDR/v1/auth/approle/role/test-approle"
APPROLE_ID="$(curl -s --header "X-Vault-Token: $READER_TOKEN" "$VAULT_ADDR/v1/auth/approle/role/test-approle/role-id" | jq -r .data.role_id)"
APPROLE_SECRET="$(curl -s --header "X-Vault-Token: $SECRETS_TOKEN" --request POST "$VAULT_ADDR/v1/auth/approle/role/test-approle/secret-id" | jq -r .data.secret_id)"
echo $APPROLE_SECRET

Expected behavior
The parent token should not get a permission denied error for operations that its child tokens are allowed to perform.

Environment:

  • Vault Server Version: 0.11.5
  • Vault CLI Version: v0.11.5
  • Server Operating System/Architecture: Ubuntu 16.04.5 LTS

Additional context
This is similar to #3892, except that ticket is referring to longest-path matching, while this ticket also covers paths of the same length.

@jefferai
Copy link
Member

Hi there,

Vault is default-deny and uses a capability based model. Capabilities are additive. One such capability that can be added is deny, and the way our system works, deny always takes precedence. So if you are explicitly denying access to a path and explicitly granting it in another, instead of e.g. create, update being your set of capabilities, it would be create, update, deny -- and deny would take precedence over all others.

We recommend explicitly denying only in very specific circumstances, because it's quite easy to make a mistake and give priveleges you didn't intend to. The example you gave about creating a child token with a subset of policies, one of which does not contain a deny statement, is a perfect example of this. Generally deny is only used to exclude specific paths from a prefix path rule -- and then, it's best to keep the deny statements in the same policy. Overall though it's much safer to only grant specific access than to try to grant broad access and then restrict it.

Your example is also exactly why creating child tokens is not in our default policy, and why generally you should only allow it if it's needed and in correct circumstances. It adds a complexity factor that can be hard to remember/reason about.

@chadparry
Copy link
Author

That is a good argument, and I agree with the principle. I still have a concern about this specific part of the permissions model, however. You'll notice that the keyword "deny" never appears in my example. The behavior of "deny" isn't related to this. Instead, look at the two policies for the path auth/approle/role/*. One offers ["create", "update"] capabilities and the other offers ["read"]. Then a token is created that composes both those policies. You would expect those capabilities to be cumulative. As you said, "Capabilities are additive." The surprising behavior is that those capabilities are not cumulative. Instead of the token gaining ["create", "update", "read"] capabilities, it ends up with only ["create", "update"].

@jefferai
Copy link
Member

allowed and denied parameters compose too. By puting all of those statements in policies affecting the parent token, the parent token is inhering denied parameters of *. This overrides the allowed parameters. When you are creating the child token that only has the creator policy, this denied parameters flag value doesn't exist.

Even though it's the parameters, not capabilities, that you're denying, it's the same concept, and why using policies to deny instead of to grant is not generally recommended.

It may be helpful for you to check out the command vault read -format=json sys/internal/ui/resultant-acl when working with these.

@jefferai
Copy link
Member

jefferai commented Feb 1, 2019

Closing for now as this seems like it's not a bug.

@jefferai jefferai closed this as completed Feb 1, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants