From 3473d9a8176b6829610913cbce0addda37b052f2 Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 4 Sep 2023 07:44:38 +0200 Subject: [PATCH 1/3] Update pack v2.0 with documentation, new actions and profile support. --- .github/CODEOWNERS | 1 + .github/workflows/build_and_test.yaml | 20 +- .gitignore | 3 + CHANGES.md | 13 +- README.jinja | 141 ++++++++++++++ README.md | 254 +++++++++++++++++++++++--- actions/create_token.py | 68 ++++--- actions/create_token.yaml | 116 +++++++----- actions/delete.py | 8 +- actions/delete.yaml | 14 +- actions/delete_policy.py | 5 +- actions/delete_policy.yaml | 14 +- actions/generate_secret.yaml | 50 +++++ actions/get_policy.py | 5 +- actions/get_policy.yaml | 14 +- actions/is_initialized.py | 5 +- actions/is_initialized.yaml | 5 + actions/lib/action.py | 112 ++++++++---- actions/list_policies.py | 5 +- actions/list_policies.yaml | 5 + actions/read.py | 9 +- actions/read.yaml | 14 +- actions/read_kv.py | 16 +- actions/read_kv.yaml | 45 +++-- actions/revoke_token.py | 20 ++ actions/revoke_token.yaml | 22 +++ actions/set_policy.py | 5 +- actions/set_policy.yaml | 24 ++- actions/write.py | 5 +- actions/write.yaml | 24 ++- actions/write_secret.py | 114 ++++++++++++ actions/write_secret.yaml | 45 +++++ config.schema.yaml | 118 ++++++------ pack.yaml | 5 +- requirements-tests.txt | 1 - requirements.txt | 2 +- tests/test_action_create_token.py | 2 +- tests/test_action_delete.py | 2 +- tests/test_action_delete_policy.py | 2 +- tests/test_action_get_policy.py | 4 +- tests/test_action_is_initialized.py | 6 +- tests/test_action_list_policies.py | 10 +- tests/test_action_read.py | 4 +- tests/test_action_read_kv.py | 8 +- tests/test_action_set_policy.py | 4 +- tests/test_action_write.py | 2 +- tests/vault_action_tests_base.py | 43 +++-- vault.yaml.example | 25 ++- 48 files changed, 1098 insertions(+), 341 deletions(-) create mode 100644 README.jinja create mode 100644 actions/generate_secret.yaml create mode 100644 actions/revoke_token.py create mode 100644 actions/revoke_token.yaml create mode 100644 actions/write_secret.py create mode 100644 actions/write_secret.yaml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2ae6e19..bdc7033 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -9,6 +9,7 @@ # This is base configuration. These owners could review the # changes in all files in this repository. * @cognifloyd +* @nzlosh # CI configuration files should be reviewed by specific owners # who are more responsible for ensuring the quality of this pack diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index 82ee126..d3170b0 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -12,14 +12,15 @@ jobs: # StackStorm-Exchange/ci/.github/workflows/pack-build_and_test.yaml@master build_and_test: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 name: 'Build and Test / Python ${{ matrix.python-version-short }}' strategy: matrix: include: - - python-version-short: "3.6" - python-version: 3.6.13 - vault-version: 1.5.9 + - python-version-short: "3.8" + python-version: 3.8.17 + vault-version: "1.12.5-1" + hvac-gh-tag: "v1.1.1" steps: - name: Checkout Pack Repo and CI Repos uses: StackStorm-Exchange/ci/.github/actions/checkout@master @@ -30,7 +31,7 @@ jobs: path: hvac repository: hvac/hvac # main = the release branch; devel = the active development branch - ref: main + ref: ${{ matrix.hvac-gh-tag }} fetch-depth: 0 - name: Install APT Dependencies @@ -50,7 +51,7 @@ jobs: working-directory: pack shell: bash run: | - curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - + wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/hashicorp.gpg echo "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" \ | sudo tee /etc/apt/sources.list.d/hashicorp.list @@ -59,10 +60,7 @@ jobs: -o APT::Get::List-Cleanup="0" \ -o Dir::Etc::sourcelist="sources.list.d/hashicorp.list" - sudo apt install \ - consul \ - vault=${{ matrix.vault-version }} \ - ; + sudo apt install consul vault=${{ matrix.vault-version }} # We disble cap_ipc_lock here as its generally incompatabile with GitHub # Actions' runtime environments. @@ -99,7 +97,7 @@ jobs: services: mongo: - image: mongo:3.4 + image: mongo:4.4 ports: - 27017:27017 rabbitmq: diff --git a/.gitignore b/.gitignore index f2ca7e5..d183a1b 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,6 @@ ENV/ # Rope project settings .ropeproject + +# pack2md backup file +README.md.bak diff --git a/CHANGES.md b/CHANGES.md index e04305a..664e101 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,8 +1,19 @@ # Change Log +## 2.0.0 + +- Add action to generate secrets. +- Add profile support to pack to define multiple Vault end-points. +- Updated README with full list of available actions. +- Fixes TLS support for server and client certificates. +- Updated HVAC python module dependency v1.1.0 +- Added token revoke action. +- Updated all actions to use profile name. +- Moved from Python 3.6 to 3.8 to support newer version of Vault. + ## 1.0.0 -* Drop Python 2.7 support +- Drop Python 2.7 support ## 0.6.0 diff --git a/README.jinja b/README.jinja new file mode 100644 index 0000000..a0737c5 --- /dev/null +++ b/README.jinja @@ -0,0 +1,141 @@ +# {{ pack["pack.yaml"].name | capitalize }} Integration Pack +_{{ pack["pack.yaml"].description }}_ + +*Author:* {{ pack["pack.yaml"].author }} <{{ pack["pack.yaml"].email }}> + +## Maintainers +Active pack maintainers with review & write repository access and expertise with vault: +* Jacob Floyd ([@cognifloyd](https://github.com/cognifloyd)) Copart +* Carlos ([@nzlosh](https://github.com/nzlosh)) + +### Contributors +{% for contributor in pack["pack.yaml"].contributors -%} + - {{ contributor }} +{% endfor %} + +{% if pack and pack["config.schema.yaml"] -%} +## Configuration + +The following options are required to be configured for the pack to work correctly. + +| Option | Type | Required | Secret | Description | +|---|---|---|---|---| +{% for key, value in pack["config.schema.yaml"].items() -%} +| `{{ key }}` | {{ value.type }} | {{ value.required }} | {{ value.secret | default("default") }} | _{{ value.description | default("Unavailable") }}_ | +{% if "array" == value.type -%} +{% if "object" == value.get("items").type -%} +{% for ik, iv in value.get("items").properties.items() -%} +| - `{{ ik }}` | {{ iv.type }} | {{ iv.required }} | {{ iv.secret | default("default") }} | _{{ iv.description | default("Unavailable") }}_ | +{% endfor -%} +{% else -%} +| | {{ value.get("items").type }} | | | list of items | +{% endif -%} +{% endif -%} +{% endfor -%} + +{% endif %} + +## Actions + +{% if actions | length > 0 %} +The pack provides the following actions: + +{% for key, value in actions.items() -%} +### {{ value.name }} +_{{ value.description }}_ +{% if "parameters" in value -%} +| Parameter | Type | Required | Secret | Description | +|---|---|---|---|---| +{% for p_key, p_value in value.parameters.items() -%} +{% if "array" == p_value.type -%} +{% if p_value.get("items").type == "object" -%} +{% for a_k, a_v in p_value.get("items").properties.items() -%} +| - `{{ a_k }}` | {{ a_v.type | default("n/a") }} | {{ a_v.required | default("default") }} | {{ a_v.secret | default("default") }} | _{{ a_v.description | default("Unavailable") }}_ | +{% endfor %} +{% else -%} +| Items are of type | {{ p_value.items.type }} |||| +{% endif -%} +{% endif -%} +| `{{ p_key }}` | {{ p_value.type | default("n/a") }} | {{ p_value.required | default("default") }} | {{ p_value.secret | default("default") }} | _{{ p_value.description | default("Unavailable") }}_ | +{% endfor -%} +{% endif %} + +{% endfor %} +{% else %} +There are no actions available for this pack. +{% endif %} + +### generate secret + +This action is written to pre-populate keys with a random secret. + +The following string sets are available + + - ascii_letters + ```abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ``` + - ascii_lowercase + ```abcdefghijklmnopqrstuvwxyz``` + - ascii_uppercase + ```ABCDEFGHIJKLMNOPQRSTUVWXYZ``` + - digits + ```0123456789``` + - punctuation + ```!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~``` + - printable + ```0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c``` + - alphanumeric + ```abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789``` + +### Update tactic + +The update tactic controls how the action will update existing secrets. It's intended to ensure idempotence on multiple runs of the secret generation action. The currently supported tactics are: + - `overwrite`: Overwrite an existing secret. + - `refrain`: Do not overwrite an existing secret. + +## Sensors +{% if sensors | length > 0 %} +The following sensors and triggers are provided: +{% for key, value in sensors.items() %} +### Class {{ value.class_name }} +_{{ value.description }}_ + +{% for trigger in value.trigger_types -%} +| Trigger Name | Description | +|---|---| +| `{{ trigger.name }}` | _{{ trigger.description | default("Unavailable") }}_ | +{% endfor %} + + +{% endfor %} +{% else %} +There are no sensors available for this pack. +{% endif %} + +## Authentication methods + +Authentication methods are defined per profile and are mutally exclusive. Only configure the +method that should be used. + +### Supported + - approle + - token + +### Unsupported + - app-id + - ali-cloud + - aws-iam # aka aws + - aws-ec2 + - azure + - cert # aka tls + - gcp + - github + - jwt + - kubernetes + - ldap + - mfa + - oidc + - okta + - radius + - userpass + +Documentation generated using [pack2md](https://github.com/nzlosh/pack2md) diff --git a/README.md b/README.md index 2fa9c27..2e12bfc 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,238 @@ # Vault Integration Pack +_StackStorm pack integration with HashiCorp Vault_ + +*Author:* steve.neuharth + +## Maintainers +Active pack maintainers with review & write repository access and expertise with vault: +* Jacob Floyd ([@cognifloyd](https://github.com/cognifloyd)) Copart +* Carlos ([@nzlosh](https://github.com/nzlosh)) + +### Contributors +- Andy Moore +- Jacob Floyd +- Carlos -This pack is for Hashicorp Vault integrations ## Configuration -Copy the example configuration in [vault.yaml.example](./vault.yaml.example) -to `/opt/stackstorm/configs/vault.yaml` and edit as required. +The following options are required to be configured for the pack to work correctly. -It should contain: +| Option | Type | Required | Secret | Description | +|---|---|---|---|---| +| `default_profile` | string | True | default | _The default profile to use in an action when none is given._ | +| `profiles` | array | True | default | _Profiles definitions_ | +| - `name` | string | True | default | _Name of the profile._ | +| - `url` | string | True | False | _URL for the Vault server_ | +| - `verify` | boolean | False | default | _Verify the TLS certificate for HTTPS requests. Default false (this option is ignored if ca_cert_path is supplied)._ | +| - `ca_cert_path` | string | False | default | _CA Certificate path. Defaults to empty string. When path is provided, TLS certificates are verified._ | +| - `client_cert_path` | string | False | default | _Client side certificates for HTTPS request_ | +| - `client_key_path` | string | False | default | _Client private key for HTTPS request_ | +| - `auth_method` | string | False | default | _Authentication method_ | +| - `token` | string | False | True | _Authentication token (method=token)_ | +| - `role_id` | string | False | True | _Authentication approle role-id (method=approle)_ | +| - `secret_id` | string | False | True | _Authentication approle secret-id (method=approle)_ | -* `url` - URL for the Vault server -* `cert` - Path to client-side certificate -* `verify` - Whether to verify the SSL certificate or not -* `auth_method` - Which authentication method to use. - Only `token` (the default) and `approle` are implemented so far. -Also include the relevant auth_method-specific config: +## Actions -* `token` - Authentication token for `auth_method=token`. If not specified, - also tries using the `VAULT_TOKEN` env var or the `~/.vault-token` file. -* `role_id` - Authentication role_id for `auth_method=approle`. -* `secret_id` - Authentication secret_id for `auth_method=approle`. -You can also use dynamic values from the datastore. See the -[docs](https://docs.stackstorm.com/reference/pack_configs.html) for more info. +The pack provides the following actions: -**Note** : When modifying the configuration in `/opt/stackstorm/configs/` please - remember to tell StackStorm to load these new values by running - `st2ctl reload --register-configs` +### delete +_Delete value from Vault server_ +| Parameter | Type | Required | Secret | Description | +|---|---|---|---|---| +| `profile_name` | string | False | default | _The profile to use to run this action._ | +| `path` | string | True | default | _Path to delete from Vault_ | -## Actions -* `delete` - Delete value from Vault server -* `get_policy` - Read policy from Vault server -* `is_initialized` - Read initialization status from Vault server -* `list_policies` - List policies from Vault server -* `read` - Read value from Vault server -* `write` - Write key/value to Vault server -* `read_kv` - Read key-value secrets from Vault server +### generate_secret +_Generate a secret and write it to vault._ +| Parameter | Type | Required | Secret | Description | +|---|---|---|---|---| +| `profile_name` | string | False | default | _The profile to use to run this action._ | +| `mount_point` | string | False | default | _Vault moint point in the URL_ | +| `path` | string | True | default | _Path to the secrets_ | +| `key_name` | string | True | default | _Name of the key to write the secret._ | +| `update_tactic` | string | False | default | _The logic to use when writing secret to Vault. See readme for details._ | +| `string_set` | string | default | default | _Unavailable_ | +| `secret_length` | integer | default | default | _The number of characters to use in the secret._ | -## Maintainers -Active pack maintainers with review & write repository access and expertise with vault: -* Jacob Floyd ([@cognifloyd](https://github.com/cognifloyd)) Copart + +### read +_Read value from Vault server_ +| Parameter | Type | Required | Secret | Description | +|---|---|---|---|---| +| `profile_name` | string | False | default | _The profile to use to run this action._ | +| `path` | string | True | default | _Key to read from Vault_ | + + +### create_token +_Create a new Token_ +| Parameter | Type | Required | Secret | Description | +|---|---|---|---|---| +| `profile_name` | string | False | default | _The profile to use to run this action._ | +| `token_id` | string | False | default | _The ID of the client token. By default, this is an auto-generated value._ | +| `role_name` | string | False | default | _The name of the token role._ | +| Items are of type | |||| +| `policies` | array | False | default | _List of policy names to associate with this token._ | +| `meta` | string | False | default | _Metadata to associate with the token. This metadata will show in the audit log when the token is used._ | +| `no_parent` | boolean | False | default | _This argument only has effect if used by a root or sudo caller._ | +| `no_default_policy` | boolean | False | default | _Detach the 'default' policy from the policy set for this token._ | +| `renewable` | boolean | False | default | _True: Permit the token to be renewable up to the system/mount maximum TTL. False: Token can't be renewed past its initial TTL._ | +| `ttl` | string | False | default | _Initial TTL to associate with the token, provided as '1h', where hour is the largest suffix. (default unit: seconds)_ | +| `token_type` | string | False | default | _The token type. Can be 'batch' or 'service'. Defaults to the type specified by the role configuration named by role_name._ | +| `explicit_max_ttl` | string | False | default | _If set, the token will never be able to be renewed or used past the value set at issue time._ | +| `display_name` | string | False | default | _Name to associate with this token. This is a non-sensitive value that can be used to help identify created secrets (e.g. prefixes)._ | +| `num_uses` | string | False | default | _Number of times this token can be used. After the last use, the token is automatically revoked._ | +| `period` | string | False | default | _If specified, the token will be periodic; it will have no maximum TTL (unless an 'explicit-max-ttl' is also set) but every renewal will use the given period. Requires a root token or one with the sudo capability._ | +| `entity_alias` | string | default | default | _Name of the entity alias to associate with during token creation._ | +| `wrap_ttl` | string | False | default | _Specifies response wrapping token creation with duration. IE: '15s', '20m', '25h'._ | +| `mount_point` | string | False | default | _The 'path' the method/backend was mounted on._ | + + +### write +_Write a key/value to Vault_ +| Parameter | Type | Required | Secret | Description | +|---|---|---|---|---| +| `profile_name` | string | False | default | _The profile to use to run this action._ | +| `path` | string | True | default | _Path to the Vault secrets_ | +| `values` | string | True | default | _Keys and values to write in Vault ({"key":"value", "key2": "value2"}_ | + + +### get_policy +_Read policy from Vault server_ +| Parameter | Type | Required | Secret | Description | +|---|---|---|---|---| +| `profile_name` | string | False | default | _The profile to use to run this action._ | +| `name` | string | True | default | _Policy to read from Vault_ | + + +### delete_policy +_Delete policy from Vault server_ +| Parameter | Type | Required | Secret | Description | +|---|---|---|---|---| +| `profile_name` | string | False | default | _The profile to use to run this action._ | +| `name` | string | True | default | _Policy to delete from Vault_ | + + +### read_kv +_Read a kv value from Vault server_ +| Parameter | Type | Required | Secret | Description | +|---|---|---|---|---| +| `profile_name` | string | False | default | _The profile to use to run this action._ | +| `path` | string | True | default | _Key to read from Vault_ | +| `kv_version` | number | True | default | _The version of the KV store in vault. Use 1 for legacy kv stores, 2 for newer kv stores_ | +| `mount_point` | string | True | default | _The mount point of the kv store_ | +| `version` | string | True | default | _The version of the kv *data*_ | + + +### set_policy +_Create a new Vault policy_ +| Parameter | Type | Required | Secret | Description | +|---|---|---|---|---| +| `profile_name` | string | False | default | _The profile to use to run this action._ | +| `name` | string | True | default | _Name of new Vault Policy_ | +| `rules` | string | True | default | _Policy rules_ | + + +### list_policies +_List Policies from Vault server_ +| Parameter | Type | Required | Secret | Description | +|---|---|---|---|---| +| `profile_name` | string | False | default | _The profile to use to run this action._ | + + +### write_secret +_Write a secret to Vault._ +| Parameter | Type | Required | Secret | Description | +|---|---|---|---|---| +| `profile_name` | string | False | default | _The profile to use to run this action._ | +| `mount_point` | string | False | default | _Vault moint point in the URL_ | +| `path` | string | True | default | _Path to the secrets_ | +| `key_name` | string | True | default | _Name of the key to write the secret._ | +| `secret` | string | True | True | _Secret contents to be written._ | +| `decode_json` | boolean | False | default | _Secret is formatted as a json and should be decode to be sent to Vault_ | +| `update_tactic` | string | False | default | _The logic to use when writing secret to Vault. See readme for details._ | + + +### revoke_token +_Revoke a token and all its child tokens._ +| Parameter | Type | Required | Secret | Description | +|---|---|---|---|---| +| `profile_name` | string | False | default | _The profile to use to run this action._ | +| `token` | string | True | default | _Token to revoke._ | +| `mount_point` | string | False | default | _The 'path' the method/backend was mounted on._ | + + +### is_initialized +_Read initialization status from Vault server_ +| Parameter | Type | Required | Secret | Description | +|---|---|---|---|---| +| `profile_name` | string | False | default | _The profile to use to run this action._ | + + + + + +### generate secret + +This action is written to pre-populate keys with a random secret. + +The following string sets are available + + - ascii_letters + ```abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ``` + - ascii_lowercase + ```abcdefghijklmnopqrstuvwxyz``` + - ascii_uppercase + ```ABCDEFGHIJKLMNOPQRSTUVWXYZ``` + - digits + ```0123456789``` + - punctuation + ```!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~``` + - printable + ```0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c``` + - alphanumeric + ```abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789``` + +### Update tactic + +The update tactic controls how the action will update existing secrets. It's intended to ensure idempotence on multiple runs of the secret generation action. The currently supported tactics are: + - `overwrite`: Overwrite an existing secret. + - `refrain`: Do not overwrite an existing secret. + +## Sensors + +There are no sensors available for this pack. + + +## Authentication methods + +Authentication methods are defined per profile and are mutally exclusive. Only configure the +method that should be used. + +### Supported + - approle + - token + +### Unsupported + - app-id + - ali-cloud + - aws-iam # aka aws + - aws-ec2 + - azure + - cert # aka tls + - gcp + - github + - jwt + - kubernetes + - ldap + - mfa + - oidc + - okta + - radius + - userpass + +Documentation generated using [pack2md](https://github.com/nzlosh/pack2md) \ No newline at end of file diff --git a/actions/create_token.py b/actions/create_token.py index cd2a9a4..21b4ec6 100644 --- a/actions/create_token.py +++ b/actions/create_token.py @@ -2,24 +2,50 @@ class VaultCreateTokenAction(action.VaultBaseAction): - def run(self, - token_id=None, - policies=None, - meta=None, - no_parent=False, - display_name=None, - num_uses=None, - no_default_policy=False, - ttl=None, - orphan=False, - wrap_ttl=None): - return self.vault.create_token(token_id=token_id, - policies=policies, - meta=meta, - no_parent=no_parent, - display_name=display_name, - num_uses=num_uses, - no_default_policy=no_default_policy, - ttl=ttl, - orphan=orphan, - wrap_ttl=wrap_ttl) + """ + Request a child token to be created. Useful for one time use + or fixed time. + """ + + def run( + self, + display_name=None, + entity_alias=None, + explicit_max_ttl="1h", + meta=None, + mount_point="token", + no_default_policy=False, + no_parent=False, + num_uses=None, + period=None, + policies=None, + profile_name=None, + renewable=True, + role_name=None, + token_id=None, + token_type=None, + ttl=None, + wrap_ttl=None, + ): + super().run(profile_name=profile_name) + return ( + True, + self.vault.auth.token.create( + display_name=display_name, + entity_alias=entity_alias, + explicit_max_ttl=explicit_max_ttl, + id=token_id, + meta=meta, + mount_point=mount_point, + no_default_policy=no_default_policy, + no_parent=no_parent, + num_uses=num_uses, + period=period, + policies=policies, + renewable=renewable, + role_name=role_name, + ttl=ttl, + type=token_type, + wrap_ttl=wrap_ttl, + ), + ) diff --git a/actions/create_token.yaml b/actions/create_token.yaml index e6bc1b9..565b184 100644 --- a/actions/create_token.yaml +++ b/actions/create_token.yaml @@ -6,46 +6,76 @@ description: "Create a new Token" enabled: true entry_point: "create_token.py" parameters: - token_id: - type: string - description: "" - required: false - policies: - type: array - description: "" - required: false - meta: - type: string - description: "" - required: false - no_parent: - type: boolean - description: "" - required: false - default: false - display_name: - type: string - description: "" - required: false - num_uses: - type: string - description: "" - required: false - no_default_policy: - type: boolean - description: "" - required: false - default: false - ttl: - type: integer - description: "" - required: false - orphan: - type: boolean - description: "" - required: false - default: false - wrap_ttl: - type: string - description: "" - required: false + profile_name: + description: "The profile to use to run this action." + type: "string" + required: false + token_id: + type: string + description: "The ID of the client token. By default, this is an auto-generated value." + required: false + role_name: + type: string + description: "The name of the token role." + required: false + policies: + type: array + description: "List of policy names to associate with this token." + required: false + items: + type: string + required: true + meta: + type: string + description: "Metadata to associate with the token. This metadata will show in the audit log when the token is used." + required: false + no_parent: + description: "This argument only has effect if used by a root or sudo caller." + type: boolean + required: false + default: false + no_default_policy: + type: boolean + description: "Detach the 'default' policy from the policy set for this token." + required: false + default: false + renewable: + type: boolean + description: "True: Permit the token to be renewable up to the system/mount maximum TTL. False: Token can't be renewed past its initial TTL." + required: false + ttl: + type: string + description: "Initial TTL to associate with the token, provided as '1h', where hour is the largest suffix. (default unit: seconds)" + required: false + token_type: + type: string + description: "The token type. Can be 'batch' or 'service'. Defaults to the type specified by the role configuration named by role_name." + required: false + explicit_max_ttl: + type: string + description: "If set, the token will never be able to be renewed or used past the value set at issue time." + required: false + display_name: + type: string + description: "Name to associate with this token. This is a non-sensitive value that can be used to help identify created secrets (e.g. prefixes)." + required: false + num_uses: + type: string + description: "Number of times this token can be used. After the last use, the token is automatically revoked." + required: false + period: + type: string + description: "If specified, the token will be periodic; it will have no maximum TTL (unless an 'explicit-max-ttl' is also set) but every renewal will use the given period. Requires a root token or one with the sudo capability." + required: false + entity_alias: + type: string + description: "Name of the entity alias to associate with during token creation." + wrap_ttl: + type: string + description: "Specifies response wrapping token creation with duration. IE: '15s', '20m', '25h'." + required: false + mount_point: + type: string + description: "The 'path' the method/backend was mounted on." + required: false + default: "token" diff --git a/actions/delete.py b/actions/delete.py index ebf2050..5af0077 100644 --- a/actions/delete.py +++ b/actions/delete.py @@ -2,5 +2,9 @@ class VaultDeleteAction(action.VaultBaseAction): - def run(self, path): - return self.vault.delete(path) + """ + Delete a v1 path. + """ + def run(self, path, profile_name=None): + super().run(profile_name=profile_name) + return (True, self.vault.delete(path)) diff --git a/actions/delete.yaml b/actions/delete.yaml index 649da18..c95de82 100644 --- a/actions/delete.yaml +++ b/actions/delete.yaml @@ -5,8 +5,12 @@ description: "Delete value from Vault server" enabled: true entry_point: "delete.py" parameters: - path: - type: string - description: "Path to delete from Vault" - required: true - position: 0 + profile_name: + description: "The profile to use to run this action." + type: "string" + required: false + path: + type: string + description: "Path to delete from Vault" + required: true + position: 0 diff --git a/actions/delete_policy.py b/actions/delete_policy.py index b969a4b..2e78e62 100644 --- a/actions/delete_policy.py +++ b/actions/delete_policy.py @@ -2,5 +2,6 @@ class VaultPolicyDeleteAction(action.VaultBaseAction): - def run(self, name): - return self.vault.sys.delete_policy(name) + def run(self, name, profile_name=None): + super().run(profile_name=profile_name) + return (True, self.vault.sys.delete_policy(name)) diff --git a/actions/delete_policy.yaml b/actions/delete_policy.yaml index d0f3211..ea0b61f 100644 --- a/actions/delete_policy.yaml +++ b/actions/delete_policy.yaml @@ -6,8 +6,12 @@ description: "Delete policy from Vault server" enabled: true entry_point: "delete_policy.py" parameters: - name: - type: string - description: "Policy to delete from Vault" - required: true - position: 0 + profile_name: + description: "The profile to use to run this action." + type: "string" + required: false + name: + type: string + description: "Policy to delete from Vault" + required: true + position: 0 diff --git a/actions/generate_secret.yaml b/actions/generate_secret.yaml new file mode 100644 index 0000000..438963e --- /dev/null +++ b/actions/generate_secret.yaml @@ -0,0 +1,50 @@ +--- +name: generate_secret +runner_type: python-script +description: "Generate a secret and write it to vault." +enabled: true +entry_point: "write_secret.py" +parameters: + profile_name: + description: "The profile to use to run this action." + type: "string" + required: false + mount_point: + type: string + description: Vault moint point in the URL + required: false + default: "/" + position: 0 + path: + type: string + description: "Path to the secrets" + required: true + position: 1 + key_name: + type: string + description: "Name of the key to write the secret." + required: true + position: 2 + update_tactic: + type: string + description: "The logic to use when writing secret to Vault. See readme for details." + required: false + enum: + - "overwrite" + - "refrain" + default: "refrain" + string_set: + type: string + enum: + - ascii_letters + - ascii_lowercase + - ascii_uppercase + - digits + - punctuation + - printable + - alphanumeric + default: alphanumeric + secret_length: + type: integer + description: "The number of characters to use in the secret." + default: 8 diff --git a/actions/get_policy.py b/actions/get_policy.py index 8dbcef6..2d017c6 100644 --- a/actions/get_policy.py +++ b/actions/get_policy.py @@ -2,5 +2,6 @@ class VaultGetPolicyAction(action.VaultBaseAction): - def run(self, name): - return self.vault.get_policy(name) + def run(self, name, profile_name=None): + super().run(profile_name=profile_name) + return (True, self.vault.get_policy(name)) diff --git a/actions/get_policy.yaml b/actions/get_policy.yaml index b515ff4..efa3eb5 100644 --- a/actions/get_policy.yaml +++ b/actions/get_policy.yaml @@ -6,8 +6,12 @@ description: "Read policy from Vault server" enabled: true entry_point: "get_policy.py" parameters: - name: - type: string - description: "Policy to read from Vault" - required: true - position: 0 + profile_name: + description: "The profile to use to run this action." + type: "string" + required: false + name: + type: string + description: "Policy to read from Vault" + required: true + position: 0 diff --git a/actions/is_initialized.py b/actions/is_initialized.py index 25d8f8f..a694f05 100644 --- a/actions/is_initialized.py +++ b/actions/is_initialized.py @@ -2,5 +2,6 @@ class VaultIsInitializedAction(action.VaultBaseAction): - def run(self): - return self.vault.sys.is_initialized() + def run(self, profile_name=None): + super().run(profile_name=profile_name) + return (True, self.vault.sys.is_initialized()) diff --git a/actions/is_initialized.yaml b/actions/is_initialized.yaml index d14ab15..2fb7d50 100644 --- a/actions/is_initialized.yaml +++ b/actions/is_initialized.yaml @@ -4,3 +4,8 @@ runner_type: python-script description: "Read initialization status from Vault server" enabled: true entry_point: "is_initialized.py" +parameters: + profile_name: + description: "The profile to use to run this action." + type: "string" + required: false diff --git a/actions/lib/action.py b/actions/lib/action.py index d3e4e5e..813e77f 100644 --- a/actions/lib/action.py +++ b/actions/lib/action.py @@ -3,48 +3,82 @@ class VaultBaseAction(Action): - + """ + Base Action includes st2 profile and vault client functions + for child classes. + """ def __init__(self, config): - super(VaultBaseAction, self).__init__(config) - self.vault = self._get_client() - - def _get_client(self): - url = self.config['url'] - verify = self._get_verify() - - auth_method = self.config.get("auth_method", "token") - token = self.config["token"] - - # token is passed during client init to allow client to also - # get the token from VAULT_TOKEN env var or ~/.vault-token - client = hvac.Client(url=url, token=token, verify=verify) - - # NB: for auth_methods, we used to be able to login with - # client.auth_*, but most of those have been deprecated - # in favor of: client.auth..login - # So, use client.auth. where implemented - - # token is handled during client init - # other auth methods will override it as needed - if auth_method == "token": - pass - elif auth_method == "approle": - client.auth.approle.login( - role_id=self.config["role_id"], - secret_id=self.config["secret_id"], + super().__init__(config) + self.config = config + self.vault = None + + def run(self, profile_name=None): + """ + The base action selects the profile and initialises the vault client. + """ + if profile_name is None: + profile_name = self.config.get("default_profile") + if profile_name is None: + raise ValueError("No default profile found, check the pack configuration.") + + for profile in self.config.get("profiles", []): + if profile_name == profile["name"]: + self._configure_client(profile) + break + else: + raise ValueError( + f"Profile '{profile_name}' doesn't exist, check the pack configuration." ) + + def _configure_client(self, profile): + """ + Set-up of the Vault client from the pack configuration. + """ + auth_methods = {"token": self._auth_token, "approle": self._auth_approle} + + client_kwargs = {"url": profile["url"]} + + # Server TLS validation + if profile.get("ca_cert_path"): + client_kwargs["verify"] = profile["ca_cert_path"] else: - raise NotImplementedError( - "The {} auth method has a typo or has not been implemented (yet).".format( - auth_method - ) + client_kwargs["verify"] = profile.get("verify", False) + + # Client certificate (HVAC expects a cert/key tuple) + cert = (profile.get("client_cert_path"), profile.get("client_key_path")) + # Both the key and certificate must be provided. + if bool(cert[0]) ^ bool(cert[1]): + raise ValueError( + "Client-side TLS requires the client's certificate and key but one was provided." + ) + # Use the key/cert tuple with HVAC if they are both defined. + if cert[0] and cert[1]: + client_kwargs["cert"] = cert + + self.vault = hvac.Client(**client_kwargs) + auth_method = profile.get("auth_method") + if auth_method not in auth_methods: + raise ValueError(f"Auth method '{auth_method}' is not supported.") + + # Authenticate the client connection. + auth_methods[auth_method](profile) + + if self.vault.token is None: + raise ValueError( + "Failed to set a valid token for the client, check the pack configuration." ) - return client + def _auth_token(self, profile): + """ + A vault token provided directly in the pack configuration + """ + self.vault.token = profile.get("token") - def _get_verify(self): - verify = self.config['verify'] - cert = self.config['cert'] - if verify and cert: - return cert - return verify + def _auth_approle(self, profile): + """ + Authenticate using a vault app role to acquire the vault token. + """ + self.vault.auth.approle.login( + role_id=profile["role_id"], + secret_id=profile["secret_id"], + ) diff --git a/actions/list_policies.py b/actions/list_policies.py index 2e7dab5..864a6eb 100644 --- a/actions/list_policies.py +++ b/actions/list_policies.py @@ -2,5 +2,6 @@ class VaultPolicyListAction(action.VaultBaseAction): - def run(self): - return self.vault.list_policies() + def run(self, profile_name=None): + super().run(profile_name=profile_name) + return (True, {"policies": self.vault.sys.list_policies()}) diff --git a/actions/list_policies.yaml b/actions/list_policies.yaml index 57886cf..6c455a0 100644 --- a/actions/list_policies.yaml +++ b/actions/list_policies.yaml @@ -4,3 +4,8 @@ runner_type: python-script description: "List Policies from Vault server" enabled: true entry_point: "list_policies.py" +parameters: + profile_name: + description: "The profile to use to run this action." + type: "string" + required: false diff --git a/actions/read.py b/actions/read.py index 599365b..34c801b 100644 --- a/actions/read.py +++ b/actions/read.py @@ -2,9 +2,10 @@ class VaultReadAction(action.VaultBaseAction): - def run(self, path): + def run(self, path, profile_name=None): + super().run(profile_name=profile_name) value = self.vault.read(path) if value: - return value['data'] - else: - raise KeyError("Key was not found in Vault") + return (True, value) + + return (False, f"Key was not found in path {path}") diff --git a/actions/read.yaml b/actions/read.yaml index c9a2f80..caedcc7 100644 --- a/actions/read.yaml +++ b/actions/read.yaml @@ -5,8 +5,12 @@ description: "Read value from Vault server" enabled: true entry_point: "read.py" parameters: - path: - type: string - description: "Key to read from Vault" - required: true - position: 0 + profile_name: + description: "The profile to use to run this action." + type: "string" + required: false + path: + type: string + description: "Key to read from Vault" + required: true + position: 0 diff --git a/actions/read_kv.py b/actions/read_kv.py index 9aacf02..2851954 100644 --- a/actions/read_kv.py +++ b/actions/read_kv.py @@ -2,19 +2,21 @@ class VaultReadKVAction(action.VaultBaseAction): - def run(self, path, kv_version, mount_point, version): + def run(self, path, kv_version, mount_point, version, profile_name=None): + super().run(profile_name=profile_name) + value = None if kv_version == 1: - value = self.vault.secrets.kv.v1.read_secret( - path=path, mount_point=mount_point - ) + value = self.vault.secrets.kv.v1.read_secret(path=path, mount_point=mount_point) elif kv_version == 2: value = self.vault.secrets.kv.v2.read_secret_version( path=path, mount_point=mount_point, version=version ) + else: + return (False, f"Unsupported kv version {kv_version}.") if value: - return value['data'] - else: - raise KeyError("Key was not found in Vault") + return (True, value) + + return (False, f"Secret was not found at mount: {mount_point}, path: {path}") diff --git a/actions/read_kv.yaml b/actions/read_kv.yaml index cf3b423..4d98b3a 100644 --- a/actions/read_kv.yaml +++ b/actions/read_kv.yaml @@ -5,22 +5,29 @@ description: "Read a kv value from Vault server" enabled: true entry_point: "read_kv.py" parameters: - path: - type: string - description: "Key to read from Vault" - required: true - position: 0 - kv_version: - type: number - description: "The version of the KV store in vault. Use 1 for legacy kv stores, 2 for newer kv stores" - required: true - mount_point: - type: string - description: "The mount point of the kv store" - required: true - default: "secret" - version: - type: string - description: "The version of the kv *data*" - required: true - default: '1' + profile_name: + description: "The profile to use to run this action." + type: "string" + required: false + path: + type: string + description: "Key to read from Vault" + required: true + position: 0 + kv_version: + type: number + description: "The version of the KV store in vault. Use 1 for legacy kv stores, 2 for newer kv stores" + required: true + position: 1 + mount_point: + type: string + description: "The mount point of the kv store" + required: true + default: "secret" + position: 2 + version: + type: string + description: "The version of the kv *data*" + required: true + default: "1" + position: 3 diff --git a/actions/revoke_token.py b/actions/revoke_token.py new file mode 100644 index 0000000..b19c58f --- /dev/null +++ b/actions/revoke_token.py @@ -0,0 +1,20 @@ +from lib import action + + +class VaultRevokeTokenAction(action.VaultBaseAction): + """ + Revoke a token and all child tokens. + + When the token is revoked, all dynamic secrets generated with it are also revoked. + """ + + def run(self, token, profile_name=None, mount_point="token"): + super().run(profile_name=profile_name) + + return ( + True, + self.vault.auth.token.revoke( + token=token, + mount_point=mount_point, + ), + ) diff --git a/actions/revoke_token.yaml b/actions/revoke_token.yaml new file mode 100644 index 0000000..c23575d --- /dev/null +++ b/actions/revoke_token.yaml @@ -0,0 +1,22 @@ +--- +name: revoke_token +pack: vault +runner_type: python-script +description: "Revoke a token and all its child tokens." +enabled: true +entry_point: "revoke_token.py" +parameters: + profile_name: + description: "The profile to use to run this action." + type: "string" + required: false + token: + description: "Token to revoke." + type: "string" + required: true + position: 0 + mount_point: + description: "The 'path' the method/backend was mounted on." + type: "string" + required: false + default: "token" diff --git a/actions/set_policy.py b/actions/set_policy.py index d570fb5..00b828e 100644 --- a/actions/set_policy.py +++ b/actions/set_policy.py @@ -2,5 +2,6 @@ class VaultPolicySetAction(action.VaultBaseAction): - def run(self, name, rules): - return self.vault.sys.create_or_update_policy(name, rules) + def run(self, name, rules, profile_name=None): + super().run(profile_name=profile_name) + return (True, self.vault.sys.create_or_update_policy(name, rules)) diff --git a/actions/set_policy.yaml b/actions/set_policy.yaml index 115f7d4..4c5e495 100644 --- a/actions/set_policy.yaml +++ b/actions/set_policy.yaml @@ -6,13 +6,17 @@ description: "Create a new Vault policy" enabled: true entry_point: "set_policy.py" parameters: - name: - type: string - description: "Name of new Vault Policy" - required: true - position: 0 - rules: - type: string - description: "Policy rules" - required: true - position: 1 + profile_name: + description: "The profile to use to run this action." + type: "string" + required: false + name: + type: string + description: "Name of new Vault Policy" + required: true + position: 0 + rules: + type: string + description: "Policy rules" + required: true + position: 1 diff --git a/actions/write.py b/actions/write.py index f5dbabd..4610ba3 100644 --- a/actions/write.py +++ b/actions/write.py @@ -3,5 +3,6 @@ class VaultWriteAction(action.VaultBaseAction): - def run(self, path, values): - return self.vault.write(path, **json.loads(values)) + def run(self, path, values, profile_name=None): + super().run(profile_name=profile_name) + return (True, self.vault.write(path, **json.loads(values))) diff --git a/actions/write.yaml b/actions/write.yaml index 7fab7c2..cec0ce6 100644 --- a/actions/write.yaml +++ b/actions/write.yaml @@ -5,13 +5,17 @@ description: "Write a key/value to Vault" enabled: true entry_point: "write.py" parameters: - path: - type: string - description: "Path to the Vault secrets" - required: true - position: 0 - values: - type: string - description: 'Keys and values to write in Vault ({"key":"value", "key2": "value2"}' - required: true - position: 1 + profile_name: + description: "The profile to use to run this action." + type: "string" + required: false + path: + type: string + description: "Path to the Vault secrets" + required: true + position: 0 + values: + type: string + description: 'Keys and values to write in Vault ({"key":"value", "key2": "value2"}' + required: true + position: 1 diff --git a/actions/write_secret.py b/actions/write_secret.py new file mode 100644 index 0000000..0016950 --- /dev/null +++ b/actions/write_secret.py @@ -0,0 +1,114 @@ +import secrets +import string +import logging +import json + +from lib import action + +import hvac + +LOG = logging.getLogger(__name__) + +string.alphanumeric = f"{string.ascii_letters}{string.digits}" + + +def generate_secret(string_set="ascii_letters", length=8): + """ + Return a random string. + """ + if not 0 < length < 256: + raise ValueError("Secret length must be between 1 and 255 characters.") + if string_set not in [ + "ascii_letters", + "ascii_lowercase", + "ascii_uppercase", + "digits", + "punctuation", + "printable", + "alphanumeric", + ]: + raise ValueError(f"'{string_set}' is not a supported string set.") + return "".join([secrets.choice(list(getattr(string, string_set))) for _ in range(length)]) + + +def read_secret(client, mount_point, path): + """ + Read secret from Vault. + """ + try: + tmp = client.secrets.kv.v1.read_secret(path=path, mount_point=mount_point) + res = tmp.get("data", {}) + except hvac.exceptions.InvalidPath: + res = {} + return res + + +def write_secret(client, mount_point, path, payload): + """ + Write the key_name secret to vault. + """ + return client.secrets.kv.v1.create_or_update_secret( + path, secret=payload, mount_point=mount_point + ) + + +class VaultGenerateSecretAction(action.VaultBaseAction): + """ + Generate a secert and write it to a secret path. + + mount_point :string: the mount point for the kv store. + path :string: the path to the secret. + key_name :string: the name of the key to generated + eng_ver :string: the kv store version (only v1 is currently supported) + string_set :string: the character set to use in the secret generation. + secret_length :integer: the number of characters to create in the secret. + update_tactic :string: the logic to use when generating a secret. + + Return :tuple: [0] execution success [1] query result + """ + + def run( + self, + mount_point, + path, + key_name, + update_tactic="refrain", + profile_name=None, + secret=None, + decode_json=False, + string_set=None, + secret_length=None, + ): + super().run(profile_name=profile_name) + + if string_set and secret_length: + secret = generate_secret(string_set, secret_length) + + if secret is None: + raise ValueError( + "No secret to write! Either provide a secret or the string_set and" + " secret_length parameters to generate a secret." + ) + + if update_tactic not in ["refrain", "overwrite"]: + raise ValueError(f"Unknown update tactic '{update_tactic}'") + + current_secret = read_secret(self.vault, mount_point, path) + if update_tactic == "refrain" and key_name in current_secret.keys(): + allow_overwrite = False + msg = f"Refrain from updating existing secret {mount_point}/{path}" + else: + allow_overwrite = True + + if allow_overwrite: + if decode_json: + secret = json.loads(secret) + current_secret.update({key_name: secret}) + write_result = write_secret(self.vault, mount_point, path, current_secret) + if write_result.ok: + msg = f"Wrote secret {key_name} to {mount_point}/{path}" + else: + allow_overwrite = write_result.ok + msg = write_result.reason + + return (True, {"updated": allow_overwrite, "message": msg}) diff --git a/actions/write_secret.yaml b/actions/write_secret.yaml new file mode 100644 index 0000000..317cb21 --- /dev/null +++ b/actions/write_secret.yaml @@ -0,0 +1,45 @@ +--- +name: write_secret +runner_type: python-script +description: "Write a secret to Vault." +enabled: true +entry_point: "write_secret.py" +parameters: + profile_name: + description: "The profile to use to run this action." + type: "string" + required: false + mount_point: + type: string + description: Vault moint point in the URL + required: false + default: "/" + position: 0 + path: + type: string + description: "Path to the secrets" + required: true + position: 1 + key_name: + type: string + description: "Name of the key to write the secret." + required: true + position: 2 + secret: + type: string + description: "Secret contents to be written." + required: true + secret: true + decode_json: + type: boolean + description: "Secret is formatted as a json and should be decode to be sent to Vault" + required: false + default: false + update_tactic: + type: string + description: "The logic to use when writing secret to Vault. See readme for details." + required: false + enum: + - "overwrite" + - "refrain" + default: "refrain" diff --git a/config.schema.yaml b/config.schema.yaml index 0eaa02b..e47abbb 100644 --- a/config.schema.yaml +++ b/config.schema.yaml @@ -1,58 +1,64 @@ --- - url: - description: "URL for the Vault server" - type: "string" - secret: false +default_profile: + description: "The default profile to use in an action when none is given." + type: string + required: true +profiles: + description: "Profiles definitions" + type: "array" + required: true + items: + type: "object" required: true - cert: - description: "Path to client-side certificate" - type: "string" - secret: false - required: false - verify: - description: "Whether to verify the SSL certificate or not" - type: "boolean" - secret: false - default: true - - auth_method: - description: "Authentication method" - type: "string" - default: "token" - enum: - - approle - - token - # Not implemented: - # - app-id - # - ali-cloud - # - aws-iam # aka aws - # - aws-ec2 - # - azure - # - cert # aka tls - # - gcp - # - github - # - jwt - # - kubernetes - # - ldap - # - mfa - # - oidc - # - okta - # - radius - # - userpass - required: false - - token: - description: "Authentication token (method=token)" - type: "string" - secret: true - required: false - role_id: - description: "Authentication approle role-id (method=approle)" - type: "string" - secret: true - required: false - secret_id: - description: "Authentication approle secret-id (method=approle)" - type: "string" - secret: true - required: false + additionalProperties: false + properties: + name: + description: "Name of the profile." + type: "string" + required: true + url: + description: "URL for the Vault server" + type: "string" + secret: false + required: true + verify: + description: "Verify the TLS certificate for HTTPS requests. Default false (this option is ignored if ca_cert_path is supplied)." + type: "boolean" + required: false + default: false + ca_cert_path: + description: "CA Certificate path. Defaults to empty string. When path is provided, TLS certificates are verified." + type: "string" + required: false + default: "" + client_cert_path: + description: "Client side certificates for HTTPS request" + type: "string" + required: false + client_key_path: + description: "Client private key for HTTPS request" + type: "string" + required: false + auth_method: + description: "Authentication method" + type: "string" + default: "token" + enum: + - approle + - token + required: false + token: + description: "Authentication token (method=token)" + type: "string" + secret: true + required: false + role_id: + description: "Authentication approle role-id (method=approle)" + type: "string" + secret: true + required: false + secret_id: + description: "Authentication approle secret-id (method=approle)" + type: "string" + secret: true + required: false diff --git a/pack.yaml b/pack.yaml index 48ddb9c..641f231 100644 --- a/pack.yaml +++ b/pack.yaml @@ -1,8 +1,8 @@ --- ref: vault name: vault -description: HashiCorp Vault -version: 1.0.0 +description: StackStorm pack integration with HashiCorp Vault +version: 2.0.0 python_versions: - "3" author: steve.neuharth @@ -10,3 +10,4 @@ email: steve.neuharth@target.com contributors: - Andy Moore - Jacob Floyd + - Carlos diff --git a/requirements-tests.txt b/requirements-tests.txt index 2d2fb9a..591aeed 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,2 +1 @@ -# do we need parser (pyhcl) for the actions as well or just tests? hvac[parser] diff --git a/requirements.txt b/requirements.txt index c02773e..7846d6c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -hvac>=0.10.6 +hvac>=1.1.0,<2.0.0 diff --git a/tests/test_action_create_token.py b/tests/test_action_create_token.py index ad9e559..aacb9d6 100644 --- a/tests/test_action_create_token.py +++ b/tests/test_action_create_token.py @@ -11,7 +11,7 @@ class CreateTokenActionTestCase(VaultActionTestCase): def test_method(self): action = self.get_action_instance(config=self.dummy_pack_config) - result = action.run( + _, result = action.run( # token_id=None, # policies=None, # meta=None, diff --git a/tests/test_action_delete.py b/tests/test_action_delete.py index 92bc93b..a1e5852 100644 --- a/tests/test_action_delete.py +++ b/tests/test_action_delete.py @@ -11,7 +11,7 @@ def test_delete_existing_key(self): # test action = self.get_action_instance(config=self.dummy_pack_config) - action_result = action.run(path="secret/stanley") + _, action_result = action.run(path="secret/stanley") # hvac does not return anything here self.assertIsNone(action_result) diff --git a/tests/test_action_delete_policy.py b/tests/test_action_delete_policy.py index dfef183..cdecd44 100644 --- a/tests/test_action_delete_policy.py +++ b/tests/test_action_delete_policy.py @@ -13,7 +13,7 @@ def test_method(self): # test action = self.get_action_instance(config=self.dummy_pack_config) - action_result = action.run(name="stanley") + _, action_result = action.run(name="stanley") self.assertIsInstance(action_result, Response) result = self.client.get_policy("stanley") diff --git a/tests/test_action_get_policy.py b/tests/test_action_get_policy.py index 51ac4ac..7228d7c 100644 --- a/tests/test_action_get_policy.py +++ b/tests/test_action_get_policy.py @@ -11,8 +11,8 @@ def test_method(self): # test action = self.get_action_instance(config=self.dummy_pack_config) - action_result = action.run(name="stanley") + _, action_result = action.run(name="stanley") self.assertEqual(action_result, rules_text) # cleanup - self.client.delete_policy("stanley") + self.client.sys.delete_policy("stanley") diff --git a/tests/test_action_is_initialized.py b/tests/test_action_is_initialized.py index ec6ca19..b46764b 100644 --- a/tests/test_action_is_initialized.py +++ b/tests/test_action_is_initialized.py @@ -7,18 +7,18 @@ class IsInitializedActionTestCase(VaultActionTestCase): def test_already_initialized(self): action = self.get_action_instance(config=self.dummy_pack_config) - result = action.run() + _, result = action.run() self.assertTrue(result) def test_before_and_after_initialization(self): self.manager.restart_vault_cluster(perform_init=False) action = self.get_action_instance(config=self.dummy_pack_config) - result = action.run() + _, result = action.run() self.assertFalse(result) self.manager.restart_vault_cluster(perform_init=True) action = self.get_action_instance(config=self.dummy_pack_config) - result = action.run() + _, result = action.run() self.assertTrue(result) diff --git a/tests/test_action_list_policies.py b/tests/test_action_list_policies.py index 6877584..6f42578 100644 --- a/tests/test_action_list_policies.py +++ b/tests/test_action_list_policies.py @@ -11,11 +11,11 @@ def test_method(self): # test action = self.get_action_instance(config=self.dummy_pack_config) - result = action.run() - self.assertIn("stanley", result) + _, result = action.run() + self.assertIn("stanley", result["policies"]["data"]["policies"]) - self.client.delete_policy("stanley") + self.client.sys.delete_policy("stanley") action = self.get_action_instance(config=self.dummy_pack_config) - result = action.run() - self.assertNotIn("stanley", result) + _, result = action.run() + self.assertNotIn("stanley", result["policies"]["data"]["policies"]) diff --git a/tests/test_action_read.py b/tests/test_action_read.py index deb6e48..e760812 100644 --- a/tests/test_action_read.py +++ b/tests/test_action_read.py @@ -11,8 +11,8 @@ def test_read_existing_key(self): # test action = self.get_action_instance(config=self.dummy_pack_config) - result = action.run(path="secret/stanley") - self.assertEqual(result["st2"], "awesome") + _, result = action.run(path="secret/stanley") + self.assertEqual(result["data"]["st2"], "awesome") # cleanup self.client.delete("secret/stanley") diff --git a/tests/test_action_read_kv.py b/tests/test_action_read_kv.py index 2f17a07..7fabeec 100644 --- a/tests/test_action_read_kv.py +++ b/tests/test_action_read_kv.py @@ -20,13 +20,13 @@ def test_read_kv1(self): # test action = self.get_action_instance(config=self.dummy_pack_config) - result = action.run( + _, result = action.run( path="stanley", kv_version=1, mount_point=mount_point, version="1", ) - self.assertEqual(result["st2"], "awesome") + self.assertEqual(result["data"]["st2"], "awesome") # cleanup self.client.kv.v1.delete_secret( @@ -46,14 +46,14 @@ def test_read_kv2(self): # test action = self.get_action_instance(config=self.dummy_pack_config) - result = action.run( + _, result = action.run( path="stanley", kv_version=2, mount_point=mount_point, version="1", ) # v2 puts the secret one level deeper than v1 - self.assertEqual(result["data"]["st2"], "awesome") + self.assertEqual(result["data"]["data"]["st2"], "awesome") # cleanup self.client.kv.v2.delete_metadata_and_all_versions( diff --git a/tests/test_action_set_policy.py b/tests/test_action_set_policy.py index deea098..1b97dbc 100644 --- a/tests/test_action_set_policy.py +++ b/tests/test_action_set_policy.py @@ -16,7 +16,7 @@ def test_method(self): # rules_obj = {"path": {"sys": {"policy": "deny"}, "secret": {"policy": "write"}}} action = self.get_action_instance(config=self.dummy_pack_config) - action_result = action.run( + _, action_result = action.run( name="stanley", rules=rules_text, ) @@ -26,4 +26,4 @@ def test_method(self): self.assertEqual(result, rules_text) # cleanup - self.client.delete_policy("stanley") + self.client.sys.delete_policy("stanley") diff --git a/tests/test_action_write.py b/tests/test_action_write.py index d6e6ea2..58c0952 100644 --- a/tests/test_action_write.py +++ b/tests/test_action_write.py @@ -8,7 +8,7 @@ class WriteActionTestCase(VaultActionTestCase): def test_write_key(self): # test action = self.get_action_instance(config=self.dummy_pack_config) - action_result = action.run( + _, action_result = action.run( path="secret/stanley", values='{"st2": "awesome"}', ) diff --git a/tests/vault_action_tests_base.py b/tests/vault_action_tests_base.py index 0db9454..12b0dc5 100644 --- a/tests/vault_action_tests_base.py +++ b/tests/vault_action_tests_base.py @@ -26,23 +26,23 @@ def setUp(self): mounted_secrets_engines = self.client.sys.list_mounted_secrets_engines()["data"] # based on hvac/tests/integration_tests/v1/test_integration.py if "secret/" not in mounted_secrets_engines: - self.client.enable_secret_backend( + self.client.sys.enable_secrets_engine( backend_type="kv", - mount_point="secret", + path="secret", options=dict(version=1), ) # based on hvac/tests/integration_tests/api/secrets_engins/test_kv_v*.py if self.secret_v1 and "kvv1/" not in mounted_secrets_engines: - self.client.enable_secret_backend( + self.client.sys.enable_secrets_engine( backend_type="kv", - mount_point="kvv1", + path="kvv1", options=dict(version=1), ) if self.secret_v2 and "kvv2/" not in mounted_secrets_engines: - self.client.enable_secret_backend( + self.client.sys.enable_secrets_engine( backend_type="kv", - mount_point="kvv2", + path="kvv2", options=dict(version=2), ) @@ -53,29 +53,32 @@ def tearDown(self): self.dummy_pack_config = None if self.secret_v1: - self.client.disable_secret_backend(mount_point="kvv1") + self.client.sys.disable_secrets_engine(path="kvv1") if self.secret_v2: - self.client.disable_secret_backend(mount_point="kvv2") + self.client.sys.disable_secrets_engine(path="kvv2") def build_dummy_pack_config(self, url="https://localhost:8200"): # based on create_client() in hvac/tests/utils/__init__.py server_cert_path = get_config_file_path("server-cert.pem") - token_result = self.client.create_token(lease=self.default_token_lease) + token_result = self.client.auth.token.create(ttl=self.default_token_lease) token = token_result["auth"]["client_token"] dummy_pack_config = { - "url": url, - # pack config | relation | hvac.Client() - # ------------|----------|-------------- - # cert | != | cert - # cert+verify | == | verify - "cert": server_cert_path, - "verify": True, - "auth_method": "token", - "token": token, - "role_id": None, - "secret_id": None, + "default_profile": "dummy", + "profiles": [ + { + "name": "dummy", + "url": url, + # cert: + # False = no validation + # True = Valid server cert + # "cert_path" = validate server certificate + "verify": server_cert_path, + "auth_method": "token", + "token": token, + } + ], } return dummy_pack_config diff --git a/vault.yaml.example b/vault.yaml.example index 306723f..8b12963 100644 --- a/vault.yaml.example +++ b/vault.yaml.example @@ -1,11 +1,16 @@ --- -url: 'https://127.0.0.1:8200' -cert: '' -verify: false - -auth_method: token -token: '' - -# auth_method: approle -# role_id: '00000000-0000-0000-0000-000000000000' -# secret_id: '00000000-0000-0000-0000-000000000000' +default_profile: production +profiles: + - name: production + url: 'https://169.254.0.1:8200' + ca_cert_path: /etc/ssl/certs/internal_ca.pem + client_cert_path: /etc/ssl/private/client.pem + client_key_path: /etc/ssl/private/client.key + auth_method: approle + role_id: '00000000-0000-0000-0000-000000000000' + secret_id: '00000000-0000-0000-0000-000000000000' + - name: development + url: 'https://127.0.0.1:8200' + verify: false + auth_method: token + token: '' From 066eccbb47df89fcd8e6576d5fad47ccf8c6bed3 Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 5 Sep 2023 16:16:12 +0200 Subject: [PATCH 2/3] Remove contributors, relocate maintainers and repush codeowners --- .github/CODEOWNERS | 1 + README.jinja | 17 +++---- README.md | 124 +++++++++++++++++++++------------------------ 3 files changed, 66 insertions(+), 76 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bdc7033..5ffca0e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,6 +8,7 @@ # This is base configuration. These owners could review the # changes in all files in this repository. + * @cognifloyd * @nzlosh diff --git a/README.jinja b/README.jinja index a0737c5..cb65cf9 100644 --- a/README.jinja +++ b/README.jinja @@ -3,16 +3,6 @@ _{{ pack["pack.yaml"].description }}_ *Author:* {{ pack["pack.yaml"].author }} <{{ pack["pack.yaml"].email }}> -## Maintainers -Active pack maintainers with review & write repository access and expertise with vault: -* Jacob Floyd ([@cognifloyd](https://github.com/cognifloyd)) Copart -* Carlos ([@nzlosh](https://github.com/nzlosh)) - -### Contributors -{% for contributor in pack["pack.yaml"].contributors -%} - - {{ contributor }} -{% endfor %} - {% if pack and pack["config.schema.yaml"] -%} ## Configuration @@ -40,7 +30,7 @@ The following options are required to be configured for the pack to work correct {% if actions | length > 0 %} The pack provides the following actions: -{% for key, value in actions.items() -%} +{% for key, value in (actions.items() | list | sort) -%} ### {{ value.name }} _{{ value.description }}_ {% if "parameters" in value -%} @@ -138,4 +128,9 @@ method that should be used. - radius - userpass +## Maintainers +Active pack maintainers with review & write repository access and expertise with vault: +* Jacob Floyd ([@cognifloyd](https://github.com/cognifloyd)) Copart +* Carlos ([@nzlosh](https://github.com/nzlosh)) + Documentation generated using [pack2md](https://github.com/nzlosh/pack2md) diff --git a/README.md b/README.md index 2e12bfc..2e04d89 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,6 @@ _StackStorm pack integration with HashiCorp Vault_ *Author:* steve.neuharth -## Maintainers -Active pack maintainers with review & write repository access and expertise with vault: -* Jacob Floyd ([@cognifloyd](https://github.com/cognifloyd)) Copart -* Carlos ([@nzlosh](https://github.com/nzlosh)) - -### Contributors -- Andy Moore -- Jacob Floyd -- Carlos - - ## Configuration The following options are required to be configured for the pack to work correctly. @@ -39,35 +28,6 @@ The following options are required to be configured for the pack to work correct The pack provides the following actions: -### delete -_Delete value from Vault server_ -| Parameter | Type | Required | Secret | Description | -|---|---|---|---|---| -| `profile_name` | string | False | default | _The profile to use to run this action._ | -| `path` | string | True | default | _Path to delete from Vault_ | - - -### generate_secret -_Generate a secret and write it to vault._ -| Parameter | Type | Required | Secret | Description | -|---|---|---|---|---| -| `profile_name` | string | False | default | _The profile to use to run this action._ | -| `mount_point` | string | False | default | _Vault moint point in the URL_ | -| `path` | string | True | default | _Path to the secrets_ | -| `key_name` | string | True | default | _Name of the key to write the secret._ | -| `update_tactic` | string | False | default | _The logic to use when writing secret to Vault. See readme for details._ | -| `string_set` | string | default | default | _Unavailable_ | -| `secret_length` | integer | default | default | _The number of characters to use in the secret._ | - - -### read -_Read value from Vault server_ -| Parameter | Type | Required | Secret | Description | -|---|---|---|---|---| -| `profile_name` | string | False | default | _The profile to use to run this action._ | -| `path` | string | True | default | _Key to read from Vault_ | - - ### create_token _Create a new Token_ | Parameter | Type | Required | Secret | Description | @@ -92,13 +52,33 @@ _Create a new Token_ | `mount_point` | string | False | default | _The 'path' the method/backend was mounted on._ | -### write -_Write a key/value to Vault_ +### delete +_Delete value from Vault server_ | Parameter | Type | Required | Secret | Description | |---|---|---|---|---| | `profile_name` | string | False | default | _The profile to use to run this action._ | -| `path` | string | True | default | _Path to the Vault secrets_ | -| `values` | string | True | default | _Keys and values to write in Vault ({"key":"value", "key2": "value2"}_ | +| `path` | string | True | default | _Path to delete from Vault_ | + + +### delete_policy +_Delete policy from Vault server_ +| Parameter | Type | Required | Secret | Description | +|---|---|---|---|---| +| `profile_name` | string | False | default | _The profile to use to run this action._ | +| `name` | string | True | default | _Policy to delete from Vault_ | + + +### generate_secret +_Generate a secret and write it to vault._ +| Parameter | Type | Required | Secret | Description | +|---|---|---|---|---| +| `profile_name` | string | False | default | _The profile to use to run this action._ | +| `mount_point` | string | False | default | _Vault moint point in the URL_ | +| `path` | string | True | default | _Path to the secrets_ | +| `key_name` | string | True | default | _Name of the key to write the secret._ | +| `update_tactic` | string | False | default | _The logic to use when writing secret to Vault. See readme for details._ | +| `string_set` | string | default | default | _Unavailable_ | +| `secret_length` | integer | default | default | _The number of characters to use in the secret._ | ### get_policy @@ -109,12 +89,26 @@ _Read policy from Vault server_ | `name` | string | True | default | _Policy to read from Vault_ | -### delete_policy -_Delete policy from Vault server_ +### is_initialized +_Read initialization status from Vault server_ | Parameter | Type | Required | Secret | Description | |---|---|---|---|---| | `profile_name` | string | False | default | _The profile to use to run this action._ | -| `name` | string | True | default | _Policy to delete from Vault_ | + + +### list_policies +_List Policies from Vault server_ +| Parameter | Type | Required | Secret | Description | +|---|---|---|---|---| +| `profile_name` | string | False | default | _The profile to use to run this action._ | + + +### read +_Read value from Vault server_ +| Parameter | Type | Required | Secret | Description | +|---|---|---|---|---| +| `profile_name` | string | False | default | _The profile to use to run this action._ | +| `path` | string | True | default | _Key to read from Vault_ | ### read_kv @@ -128,6 +122,15 @@ _Read a kv value from Vault server_ | `version` | string | True | default | _The version of the kv *data*_ | +### revoke_token +_Revoke a token and all its child tokens._ +| Parameter | Type | Required | Secret | Description | +|---|---|---|---|---| +| `profile_name` | string | False | default | _The profile to use to run this action._ | +| `token` | string | True | default | _Token to revoke._ | +| `mount_point` | string | False | default | _The 'path' the method/backend was mounted on._ | + + ### set_policy _Create a new Vault policy_ | Parameter | Type | Required | Secret | Description | @@ -137,11 +140,13 @@ _Create a new Vault policy_ | `rules` | string | True | default | _Policy rules_ | -### list_policies -_List Policies from Vault server_ +### write +_Write a key/value to Vault_ | Parameter | Type | Required | Secret | Description | |---|---|---|---|---| | `profile_name` | string | False | default | _The profile to use to run this action._ | +| `path` | string | True | default | _Path to the Vault secrets_ | +| `values` | string | True | default | _Keys and values to write in Vault ({"key":"value", "key2": "value2"}_ | ### write_secret @@ -157,22 +162,6 @@ _Write a secret to Vault._ | `update_tactic` | string | False | default | _The logic to use when writing secret to Vault. See readme for details._ | -### revoke_token -_Revoke a token and all its child tokens._ -| Parameter | Type | Required | Secret | Description | -|---|---|---|---|---| -| `profile_name` | string | False | default | _The profile to use to run this action._ | -| `token` | string | True | default | _Token to revoke._ | -| `mount_point` | string | False | default | _The 'path' the method/backend was mounted on._ | - - -### is_initialized -_Read initialization status from Vault server_ -| Parameter | Type | Required | Secret | Description | -|---|---|---|---|---| -| `profile_name` | string | False | default | _The profile to use to run this action._ | - - @@ -235,4 +224,9 @@ method that should be used. - radius - userpass +## Maintainers +Active pack maintainers with review & write repository access and expertise with vault: +* Jacob Floyd ([@cognifloyd](https://github.com/cognifloyd)) Copart +* Carlos ([@nzlosh](https://github.com/nzlosh)) + Documentation generated using [pack2md](https://github.com/nzlosh/pack2md) \ No newline at end of file From aea76b211d0024752e123a7be1e92024a7e3fa3e Mon Sep 17 00:00:00 2001 From: Eugen C <1533818+armab@users.noreply.github.com> Date: Thu, 7 Sep 2023 13:42:39 +0100 Subject: [PATCH 3/3] Update .github/CODEOWNERS --- .github/CODEOWNERS | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5ffca0e..06a51f3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -9,8 +9,7 @@ # This is base configuration. These owners could review the # changes in all files in this repository. -* @cognifloyd -* @nzlosh +* @cognifloyd @nzlosh # CI configuration files should be reviewed by specific owners # who are more responsible for ensuring the quality of this pack