Datastore

The goal of the datastore service is to allow users to store common parameters and their values within StackStorm for reuse in the definition of sensors, actions, and rules. The datastore service stores the data as a key-value pair. They can be get/set using the StackStorm CLI or the StackStorm Python client.

From the sensor and action plugins, since they are implemented in Python, the key-value pairs are accessed from the StackStorm Python client. For rule definitions in YAML/JSON, the key-value pairs are referenced with a specific string substitution syntax and the references are resolved on rule evaluation.

Key-Value pairs can also have a TTL associated with them, for automatic expiry.

Storing and Retrieving Key-Value Pairs via CLI

Set the value of a key-value pair:

st2 key set os_keystone_endpoint http://localhost:5000/v2.0
st2 key set aws_cfn_endpoint https://cloudformation.us-west-1.amazonaws.com

Get individual key-value pair or list all:

# To list first 50 key-value pairs (default)
st2 key list
# To list all the key-value pairs in the datastore
st2 key list -n -1
# To list all the system and user scoped datstore items (default behavior)
st2 key list --scope=all
# To list all the system scoped key-value pairs
st2 key list --scope=system
# To list all the key-value pairs scoped to the current user
st2 key list --scope=user
# To list all the key-value pairs scoped to a particular user
# NOTE: When RBAC is enabled, only admins can list key-value pairs scoped to
# a different user. Regular users can only list key-value pairs scoped to
# themselves.
st2 key list --scope=user --user=john

# Get value for key "os_keystone_endpoint"
st2 key get os_keystone_endpoint
# Get value for key "os_keystone_endpoint" in json format
st2 key get os_keystone_endpoint -j

Update an existing key-value pair:

st2 key set os_keystone_endpoint http://localhost:5000/v3

Delete an existing key-value pair:

st2 key delete os_keystone_endpoint

Storing and Retrieving Numbers, Objects and Arrays via CLI

Currently, all datastore values are stored as string. If you want to store a non-string value, you can store a JSON-serialized version, and then de-serialize it in your action/sensor code (see Referencing Key-Value Pairs in Action Definitions for more info on using Key-Values in Actions).

Storing an number / integer:

st2 key set retention_days 7

Storing an object using a JSON-serialized string representation:

st2 key set complex_data '{"name": "Dave Smith", "age": 7, "is_parent": True}'

Storing an array using a JSON-serialized string representation:

st2 key set number_list '[1, 2, 3, 4]'
st2 key set object_list '[{"name": "Eric Jones"}, {"name": "Bob Seger"}]'

Loading Key-Value Pairs from a File

Load a list of key-value pairs from a JSON file. The following is a JSON example using the same keys from the examples above:

[
    {
        "name": "os_keystone_endpoint",
        "value": "http://localhost:5000/v2.0"
    },
    {
        "name": "aws_cfn_endpoint",
        "value": "https://cloudformation.us-west-1.amazonaws.com"
    }
]

Load this file using this command:

st2 key load mydata.json

The load command can also accept a YAML file. The following example is YAML for the same key-value pairs as the JSON file above:

---
- name: os_keystone_endpoint
  value: http://localhost:5000/v2.0
- name: aws_cfn_endpoint
  value: https://cloudformation.us-west-1.amazonaws.com

Load this file using this command:

st2 key load mydata.yaml

The load command also allows you to directly load the output of the st2 key list -j command. If you have more than 50 key-value pairs, use st2 key list -n -1 -j to export all keys. This is useful if you want to migrate datastore items from a different cluster or if you want to version control the datastore items and load them from version controlled files:

# JSON
st2 key list -n -1 -j > mydata.json
st2 key load mydata.json

# YAML
st2 key list -n -1 -y > mydata.yaml
st2 key load mydata.yaml

By default, all values for keys in the file must be strings. However, it is also possible to set the value to any arbitrary data type supported by JSON/YAML (hash, array, int, boolean, etc) in the file and have StackStorm convert it to JSON before loading it into the datastore. To accomplish this, you need to explicity pass the -c/--convert flag: st2 key load -c mydata.json

Loading non-string content via JSON:

[
    {
        "name": "managed_hosts",
        "value": [
            {
                "ip_address": "192.168.1.1",
                "fqdn": "myhost.domain.tld"
            },
            {
                "ip_address": "192.168.1.2",
                "fqdn": "myotherhost.domain.tld"
            }
        ]
    },
    {
        "name": "primary_vlan",
        "value": {
            "tag": 123,
            "note": "General purpose traffic"
        }
    }
]

Load this file using this command (values will be converted into JSON strings):

$ st2 key load -c mydata.json
+---------------+-----------------------+--------+--------+------+-----+
| name          | value                 | secret | scope  | user | ttl |
+---------------+-----------------------+--------+--------+------+-----+
| managed_hosts | [{"ip_address":       |        | system |      |     |
|               | "192.168.1.1",        |        |        |      |     |
|               | "fqdn":               |        |        |      |     |
|               | "myhost.domain.tld"}, |        |        |      |     |
|               | {"ip_address":        |        |        |      |     |
|               | "192.168.1.2",        |        |        |      |     |
|               | "fqdn": "myotherhost. |        |        |      |     |
|               | domain.tld"}]         |        |        |      |     |
| primary_vlan  | {"note": "General     |        | system |      |     |
|               | purpose traffic",     |        |        |      |     |
|               | "tag": 123}           |        |        |      |     |
+---------------+-----------------------+--------+--------+------+-----+

Loading non-string content via YAML:

---
- name: managed_hosts
  value:
      - ip_address: 192.168.1.1
        fqdn: myhost.domain.tld
      - ip_address: 192.168.1.2
        fqdn: myotherhost.domain.tld
- name: primary_vlan
  value:
      tag: 123
      note: General purpose traffic

Load this file using this command (values will be converted into JSON strings):

$ st2 key load -c mydata.yaml
+---------------+-----------------------+--------+--------+------+-----+
| name          | value                 | secret | scope  | user | ttl |
+---------------+-----------------------+--------+--------+------+-----+
| managed_hosts | [{"ip_address":       |        | system |      |     |
|               | "192.168.1.1",        |        |        |      |     |
|               | "fqdn":               |        |        |      |     |
|               | "myhost.domain.tld"}, |        |        |      |     |
|               | {"ip_address":        |        |        |      |     |
|               | "192.168.1.2",        |        |        |      |     |
|               | "fqdn": "myotherhost. |        |        |      |     |
|               | domain.tld"}]         |        |        |      |     |
| primary_vlan  | {"note": "General     |        | system |      |     |
|               | purpose traffic",     |        |        |      |     |
|               | "tag": 123}           |        |        |      |     |
+---------------+-----------------------+--------+--------+------+-----+

Scoping Datastore Items

By default, all items in the key-value store are stored in the st2kv.system scope. This means every user has access to these variables. Use the Jinja expression {{st2kv.system.key_name}} to refer to these variables in actions or workflows. Prior to v2.0.1, the scope was called system and therefore the Jinja expression was {{system.key_name}}. As of v2.2, this is no longer supported.

Variables can be scoped to a specific user. With authentication enabled, you can now control who can read or write into those variables. For example, to set the variable date_cmd for the currently authenticated user, use:

st2 key set date_cmd "date -u" --scope=user

The name of the user is determined by the X-Auth-Token or St2-Api-Key header passed with the API call. From the API call authentication credentials, StackStorm will determine the user, and assign this variable to that particular user.

To retrieve the key, use:

st2 key get date_cmd --scope=user

If you want a variable date_cmd as a system variable, you can use:

st2 key set date_cmd "date +%s" --scope=system

or simply:

st2 key set date_cmd "date +%s"

This variable won’t clash with user variables with the same name. Also, you can refer to user variables in actions or workflows. The Jinja syntax to do so is {{st2kv.user.date_cmd}}.

Note that the notion of st2kv.user is available only when actions or workflows are run manually. The notion of st2kv.user is non-existent when actions or workflows are kicked off via rules. So the use of user scoped variables is limited to manual execution of actions or workflows.

Scope can be set in a JSON/YAML key file by adding the scope property:

JSON

[
    {
        "name": "date_cmd",
        "value": "date -u",
        "scope": "user"
    }
]

YAML

---
- name: date_cmd
  value: date -u
  scope: user

Setting a Key-Value Pair TTL

By default, items do not have any TTL (Time To Live). They will remain in the datastore until manually deleted. You can set a TTL with key-value pairs, so they will be automatically deleted on expiry of the TTL.

The TTL is set in seconds. To set a key-value pair for the next hour, use this:

st2 key set date_cmd "date +%s" --ttl=3600

Use-cases for setting a TTL include limiting auto-remediation workflows from running too frequently. For example, you could set a value with a TTL when a workflow is triggered. If the workflow is triggered again, it could check if the value is still set, and if so, bypass running the remediation action.

Some users keep a count of executions in the key-value store to set a maximum number of executions in a time period.

TTL can be set in a JSON/YAML key file by adding the ttl property with an integer value:

JSON

[
    {
        "name": "date_cmd",
        "value": "date -u",
        "ttl": 3600
    }
]

YAML

---
- name: date_cmd
  value: date -u
  ttl: 3600

Storing and Retrieving via Python Client

Create new key-value pairs. The StackStorm API endpoint is set either via the Client init (base_url) or from the environment variable (ST2_BASE_URL). The default ports for the API servers are assumed:

>>> from st2client.client import Client
>>> from st2client.models import KeyValuePair
>>> client = Client(base_url='http://localhost')
>>> client.keys.update(KeyValuePair(name='os_keystone_endpoint', value='http://localhost:5000/v2.0'))

Get individual key-value pair or list all:

>>> keys = client.keys.get_all()
>>> os_keystone_endpoint = client.keys.get_by_name(name='os_keystone_endpoint')
>>> os_keystone_endpoint.value
u'http://localhost:5000/v2.0'

Update an existing key-value pair:

>>> os_keystone_endpoint = client.keys.get_by_name(name='os_keystone_endpoint')
>>> os_keystone_endpoint.value = 'http://localhost:5000/v3'
>>> client.keys.update(os_keystone_endpoint)

Delete an existing key-value pair:

>>> os_keystone_endpoint = client.keys.get_by_name(name='os_keystone_endpoint')
>>> client.keys.delete(os_keystone_endpoint)

Create an encrypted key-value pair:

>>> client.keys.update(KeyValuePair(name='os_keystone_password', value='$uper$ecret!', secret=True))

Get and decrypt an encrypted key-value pair:

>>> os_keystone_password = client.keys.get_by_name(name='os_keystone_password', decrypt=True)
>>> os_keystone_password.value
u'$uper$ecret!'

Get all key-value pairs and decrypt any that are encrypted:

>>> keys = client.keys.get_all(params={'decrypt': True})
>>> # or
>>> keys = client.keys.query(decrypt=True)

Update an existing encrypted key-value pair:

>>> os_keystone_password = client.keys.get_by_name(name='os_keystone_password')
>>> os_keystone_password.value = 'New$ecret!'
>>> print os_keystone_password.secret
True
>>> client.keys.update(os_keystone_password)
>>> client.keys.get_by_name(name='os_keystone_password', decrypt=True)
<KeyValuePair name=os_keystone_password,value=New$ecret!>

Set the TTL when creating a key-value pair:

>>> from st2client.client import Client
>>> from st2client.models import KeyValuePair
>>> client = Client(base_url='http://localhost')
>>> client.keys.update(KeyValuePair(name='os_keystone_endpoint', value='http://localhost:5000/v2.0', ttl=600))

Referencing Key-Value Pairs in Action Definitions

Key-value pairs are referenced via specific string substitution syntax in rules. In general, the variable for substitution is enclosed with double brackets (i.e. {{var1}}). To refer to a key-value pair, prefix the name with “st2kv.system”, e.g. {{st2kv.system.os_keystone_endpoint}}.

A simple action example:

st2 key set error_message "Remediation failure"
---
description: Remediates a host.
enabled: true
runner_type: orquesta
entry_point: workflows/remediate.yaml
name: remediate
pack: default
parameters:
  host:
    required: true
    type: string
  error_message:
    type: string
    default: "{{ st2kv.system.error_message }}"

There is also support for retrieving integer, number, object and array key-value pairs from the datastore. If the values are stored as JSON-serialized strings, then the data will be automatically parsed into the datatype defined in the parameter definition:

st2 key set username "stanley"
st2 key set -e password "$ecret1!"
st2 key set num_network_adapters 1
st2 key set vlan_config '{"vlan_100_general_use": {"tag": 100, "subnet": "10.1.1.0/24"}, "vlan_200_dmz": {"tag": 200, "subnet": "10.99.1.0/24"}}'
st2 key set dns_servers '["10.0.0.10", "10.0.0.11"]'
---
description: Provisions a VM
enabled: true
runner_type: orquesta
entry_point: workflows/vm_provision.yaml
name: vm_provision
pack: default
parameters:
  fqdn:
    type: string
    required: true
  username:
    type: string
    default: "{{ st2kv.system.username }}"
  password:
    type: string
    default: "{{ st2kv.system.password | decrypt_kv }}"
    secret: true
  num_network_adapters:
    type: integer
    default: "{{ st2kv.system.num_network_adapters }}"
  vlan:
    type: string
    required: true
  vlan_config:
    type: array
    default: "{{ st2kv.system.vlan_config }}"
  dns_servers:
    type: object
    default: "{{ st2kv.system.dns_servers }}"

Referencing Key-Value Pairs in Rule Definitions

Similar to Action Definitions above, one can refer to a key-value pair by prefixing the name with st2kv.system, e.g. {{ st2kv.system.os_keystone_endpoint }}.

An example rule is provided below. Please refer to the Rules documentation for rule-related syntax.

{
    "name": "daily_clean_up_rule",
    "trigger": {
        "name": "st2.timer.daily"
    },
    "enabled": true,
    "action": {
        "name": "daily_clean_up_action",
        "parameters": {
            "os_keystone_endpoint": "{{ st2kv.system.os_keystone_endpoint }}"
        }
    }
}

Securing Secrets (admin only)

The key-value store allows users to store encrypted values (secrets). Symmetric encryption using AES-256 is used to encrypt secrets. The StackStorm administrator is responsible for generating the symmetric key used for encryption/decryption. Note that the StackStorm operator and administrator (or anyone else who has access to the key) can decrypt the encrypted values.

To generate a symmetric crypto key, please run:

sudo mkdir -p /etc/st2/keys/
sudo st2-generate-symmetric-crypto-key --key-path /etc/st2/keys/datastore_key.json

We recommend that the key is placed in a private location such as /etc/st2/keys/ and permissions are set such that only the StackStorm API process owner (usually st2) can read the file, and only root can write to it.

To make sure only st2 and root can access the file on the box, run:

sudo usermod -a -G st2 st2                              # Add user ``st2`` to ``st2`` group
sudo mkdir -p /etc/st2/keys/
sudo chown -R st2:st2 /etc/st2/keys/                    # Give user and group ``st2`` ownership for key
sudo chmod o-r /etc/st2/keys/                           # Revoke read access for others
sudo chmod o-r /etc/st2/keys/datastore_key.json         # Revoke read access for others

Once the key is generated, StackStorm needs to be made aware of the key. To do this, edit the st2 configuration file (/etc/st2/st2.conf) and add the following lines:

[keyvalue]
encryption_key_path = /etc/st2/keys/datastore_key.json

Once the config file changes are made, restart StackStorm:

sudo st2ctl restart

Validate you are able to set an encrypted key-value in the datastore:

st2 key set test_key test_value --encrypt

If you see errors like "MESSAGE: Crypto key not found", something has gone wrong with setting up the keys.

Storing Secrets

Note

Please note that if an admin has not setup an encryption key, you will not be allowed to save secrets in the key-value store. Contact your StackStorm admin to setup encryption keys as per the section above.

To save a secret in the key-value store:

st2 key set api_token SECRET_TOKEN --encrypt

By default, getting a key tagged as secret (via --encrypt) will always return encrypted values only. To get plain text, please run the command with the --decrypt flag:

st2 key get api_token --decrypt

Note

Keep in mind that --decrypt flag can either be used by an administrator (administrator is able to decrypt every value) and by the user who set that value in case of the user-scoped datastore items (i.e. if --scope=user flag was passed when originally setting the value).

If you are using system scoped (st2kv.system) or user scoped (st2kv.user) datastore items to store secrets, you can decrypt them and use as parameter values in rules or actions. This is supported via Jinja filter decrypt_kv (read more about Jinja filters). For example, to pass a decrypted password as a rule parameter, use:

aws_key: "{{st2kv.system.aws_key | decrypt_kv}}"

Note

When using decrypt_kv Jinja filter on a default value of an action parameter you should also mark that parameter as secret (secret: true). If you don’t do that, every user who has permission to run (execution) that action will be able to view raw unencryted value of that datastore item when executing an action.

Secret keys can be loaded from a JSON/YAML key file by adding the secret property with a boolean value. The secret property only controls how the value is stored in the datastore, not how it is saved in or read from the JSON/YAML key file. The value(s) specified in the JSON/YAML key file should be the cleartext values.

JSON

[
    {
        "name": "api_token",
        "value": "SECRET_TOKEN",
        "secret": true
    }
]

Note

For the above example, "encrypted": false is the default so it is not explicitly specified, and SECRET_TOKEN is the cleartext value.

YAML

---
- name: api_token
  value: SECRET_TOKEN  # cleartext
  secret: true  # will be stored encrypted
  # encrypted: false (default)

If you would like to save encrypted values in the JSON/YAML key file, see the next section.

Storing Pre-Encrypted Secrets

In more advanced deployments using the Infrastructure as Code philosophy with Configuration Management tools like Puppet, Chef, Ansible, etc or Kubernetes it can be useful to import keys into the datastore that are already encrypted. This technique allows for storing secrets encrypted at rest, in something like Git or a Vault, then import them into StackStorm without having to decrypt.

Keep in mind that the values need to be encrypted with the same private key which is used by the StackStorm instance in question.

To save an already encrypted secret in the key-value store, you need to tell StackStorm that the value you are passing in is already encrypted and should be used as-is. This can be done using --pre-encrypted CLI flag.

st2 key set api_token XYZ12fsAz310D --pre-encrypted

Pre-encrypted secret keys can be loaded from a JSON/YAML key file out of the box. All of the values which contain encrypted: true and secret: true are considered as pre-encrypted and treated as such.

JSON

[
    {
        "name": "api_token",
        "value": "XYZ12fsAz310D",
        "secret": true,
        "encrypted": true
    }
]
``XYZ12fsAz310D`` is the encrypted value

YAML

---
- name: api_token
  value: XYZ12fsAz310D  # encrypted value
  secret: true  # store in encrypted format
  encrypted: true  # denotes that the value is already encrypted

Security notes

We wish to discuss security details and be transparent about the implementation and limitations of the security practices to attract more eyes to it and therefore build better quality into security implementations. For the key-value store, we have settled on AES-256 symmetric encryption for simplicity. For StackStorm v2.7 and earlier, we used the Python library keyczar for doing this. Since StackStorm v2.8, we use the Python cryptography library to implement symmetric encryption.

We have made a trade-off that the StackStorm admin is allowed to decrypt the secrets in the key-value store. This made our implementation simpler. We are looking into how to let users pass their own keys for encryption every time they want to consume a secret from the key-value store. This requires more UX thought and also moves the responsibility of storing keys to the users. Your ideas are welcome here.

Please note that the global encryption key means that users with direct access to the database will only see encrypted secrets in the database. Still, the onus is on the StackStorm admin to restrict access to the database via network daemons only and not allow physical access to the box (or run databases on different boxes to st2). Note that several layers of security need to be in place, beyond the scope of this document. While we can help people with deployment questions on the StackStorm Slack community, please follow your own best security practices guide.