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:

/etc/salt/master.d/peer_run.conf
peer_run:
  .*:
    - vault.get_config
    - vault.generate_secret_id

Credential issuance

Reference the AppRole mount created during the prerequisites:

/etc/salt/master.d/vault.conf
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/minions

  • saltstack/<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

/etc/salt/master.d/vault.conf
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