AppRole issuance¶
This guide describes how to set up the Salt master to orchestrate minion authentication by issuing AppRoles. For an overview and help with deciding on an authentication flavor, see the Quickstart guide.
Prerequisites¶
Important
The following shows basic examples of how to create the necessary resources to get you rolling quickly. It does not necessarily represent recommended practices, specifically regarding token/SecretID validity.
A Vault server (cluster) is assumed to be available.
Minion AppRole mount¶
Issued AppRoles should be managed on a separate (unused) mount of the AppRole
auth backend, called salt-minions by default:
vault auth enable -path=salt-minions approle
# You will need the mount accessor to replace the placeholder
# in the policy below, so look it up now:
vault read -format json sys/auth/salt-minions | jq '.data.accessor'
Master policy¶
The Salt master needs access to the AppRole and entity management endpoints:
# This is the required Salt master policy for issuing AppRoles.
# Note that credentials should be issued from a distinct mount,
# not the one the Salt master AppRole is configured at.
# This separate mount is called `salt-minions` by default.
# List existing AppRoles
path "auth/salt-minions/role" {
capabilities = ["list"]
}
# Manage AppRoles
# This enables the Salt Master to create roles with arbitrary policies.
# If you need to restrict the assignable policies, issue tokens instead.
path "auth/salt-minions/role/*" {
capabilities = ["read", "create", "update", "delete"]
}
# Lookup mount accessor
path "sys/auth/salt-minions" {
capabilities = ["read", "sudo"]
}
# Lookup entities by alias name (role-id) and alias mount accessor
path "identity/lookup/entity" {
capabilities = ["create", "update"]
allowed_parameters = {
"alias_name" = []
# Replace `auth_approle_0a1b2c3d` with the output of the previous step
"alias_mount_accessor" = ["auth_approle_0a1b2c3d"]
}
}
# Manage entities with name prefix salt_minion_
path "identity/entity/name/salt_minion_*" {
capabilities = ["read", "create", "update", "delete"]
}
# Create entity aliases – you can restrict the mount_accessor.
# This might allow privilege escalation in case the Salt master
# is compromised and the attacker knows the entity ID of an
# entity with relevant policies attached - although you might
# have other problems at that point.
path "identity/entity-alias" {
capabilities = ["create", "update"]
allowed_parameters = {
"id" = []
"canonical_id" = []
# Replace `auth_approle_0a1b2c3d` with the output of the previous step
"mount_accessor" = ["auth_approle_0a1b2c3d"]
"name" = []
}
}
You can write it to a file (e.g. salt-master.hcl) and create the policy like this:
vault policy write salt-master salt-master.hcl
Master credentials¶
The Salt master itself needs statically configured authentication credentials,
which should be associated with the salt-master policy created above.
Create them as described in the Vault-side setup section
of the local configuration guide.
Minion policies¶
Create policies for minions as needed. Examples are shown in the secrets setup section below.
Salt master configuration¶
Authentication¶
Configure the master’s own authentication, server connection and a persistent cache as described in the Node configuration section of the local configuration guide.
Credential orchestration¶
To allow minions to pull configuration and credentials from the Salt master, add this segment to the master configuration:
peer_run:
.*:
- vault.get_config
- vault.generate_secret_id
Credential issuance¶
Reference the AppRole mount created during the prerequisites:
vault:
issue:
type: approle
approle:
mount: salt-minions # <-- mount the Salt master manages
Credential validity¶
The validity defaults for issued credentials are sane for light use; depending
on how heavily you use Vault and whether you create dynamic leases such as database credentials,
they might have to be customized.
See issue:approle:params for details.
Policies¶
Authenticated clients need associated authorizations to be useful. Policies describe the operations a client is allowed to perform.
By default, minions receive the following named policies:
saltstack/minionssaltstack/<minion_id>
Important
You need to create these policies yourself. Missing policies do not cause errors, but minions are left with the default permissions only if none of the assigned policies exist.
You can customize which policies are assigned to minions. They can be templated.
vault:
policies:
assign:
- salt_minion
- salt_minion_{minion}
- salt_role_{pillar[roles]}
# While it's theoretically possible to use {grains[roles]} here
# for backwards-compatibility reasons, it's HIGHLY discouraged.
# The minion reports grains itself, so a compromised minion would
# be able to assign arbitrary roles to itself.
Entity metadata¶
You can customize the metadata that is written to Vault
when creating Entities. Templating is supported. This metadata can then
be used in a templated Vault policy, reducing the need for boilerplate policies a lot:
vault:
metadata:
entity:
minion-id: '{minion}'
roles: '{pillar[roles]}'
List values are expanded into several indexed keys (e.g. roles__0); see
entity metadata templating for details.
This allows you to create a single policy like:
path "salt/data/minions/{{identity.entity.metadata.minion-id}}" {
capabilities = ["create", "read", "update", "delete", "patch"]
}
path "salt/data/roles/{{identity.entity.metadata.roles__0}}" {
capabilities = ["read"]
}
Note
AppRole policies and entity metadata are generally not updated automatically. After a change, you need to synchronize them by running vault.sync_approles or vault.sync_entities respectively.
Complete example¶
vault:
auth:
method: approle
approle_mount: approle # <-- mount the Salt master authenticates at
role_id: e5a7b66e-5d08-da9c-7075-71984634b882
secret_id: 841771dc-11c9-bbc7-bcac-6a3945a69cd9
cache:
backend: disk
issue:
type: approle
approle:
mount: salt-minions # <-- mount the Salt master manages
metadata:
entity:
minion-id: '{minion}'
roles: '{pillar[roles]}'
policies:
assign:
- salt_minion
server:
url: https://vault.example.com:8200
Secrets setup¶
Decide how you want to map minions to authorizations. A common pattern is to create policies based on minion IDs and minion roles, as shown in the complete example above. This example setup is continued here.
Mount the KV backend¶
Mount the Key/Value v2 backend to a path, e.g. salt:
vault secrets enable -path=salt -version=2 kv
Create secrets¶
Write a secret that is accessible to all minions:
vault kv put -mount=salt general/accessible_for_all_minions all_foo=bar
Write a secret that is accessible to any minion that has the db role:
vault kv put -mount=salt roles/db db_foo=baz
Write a secret that is accessible to a specific minion named elliott:
vault kv put -mount=salt minions/elliott minion_foo=quux
Create policies¶
Create the policies that map necessary authorizations.
Warning
If a secret path is used as a minion pillar, the minion must not have write access, otherwise a core security assumption in Salt is violated.
Important
Even if you only intend to use the secrets for minion pillars, you need to create minion policies. The master uses these policies to decide whether a minion should receive a specific pillar. The master token should not have access to secret paths itself. For details, see Pillar impersonation.
When issuing AppRoles, you can take advantage of minion metadata for templated Vault policies. This means a single policy should cover most minions and roles:
vault policy write salt_minion - <<'EOF'
path "salt/data/general/*" {
capabilities = ["read"]
}
path "salt/data/minions/{{identity.entity.metadata.minion-id}}" {
capabilities = ["read"]
}
path "salt/data/roles/{{identity.entity.metadata.roles__0}}" {
capabilities = ["read"]
}
path "salt/data/roles/{{identity.entity.metadata.roles__1}}" {
capabilities = ["read"]
}
path "salt/data/roles/{{identity.entity.metadata.roles__2}}" {
capabilities = ["read"]
}
path "salt/data/roles/{{identity.entity.metadata.roles__3}}" {
capabilities = ["read"]
}
EOF
Hint
See entity metadata templating for details, especially
to understand why the roles mapping is repeated multiple times.
Test access¶
Now you can test that the minion is able to read all secrets:
[root@master ~]# salt elliott vault_secret.read salt/general/accessible_for_all_minions
elliott:
----------
all_foo: bar
[root@master ~]# salt elliott vault_secret.read salt/roles/db
elliott:
----------
db_foo: baz
[root@master ~]# salt elliott vault_secret.read salt/minions/elliott
elliott:
----------
minion_foo: quux
Also verify that minions without authorization to access these secrets can’t.
If reading fails, clear the minion’s cached Vault data, which forces new credentials to be requested, and try again:
salt elliott vault.clear_cache
If it still fails, manually sync AppRoles and entities, clear the minion’s cached data and try again:
salt-run vault.sync_approles
salt-run vault.sync_entities
salt elliott vault.clear_cache