summaryrefslogtreecommitdiffstats
path: root/ansible_collections/azure/azcollection/plugins/lookup/azure_keyvault_secret.py
blob: ea2183a5a5d4bcea2d8a058f38d773eb701b2096 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# Copyright (c) 2022 Hai Cao, <t-haicao@microsoft.com>, Marcin Slowikowski (@msl0)
#
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = """
---
name: azure_keyvault_secret
author:
    - Hai Cao (@tk5eq) <t-haicao@microsoft.com>
    - Marcin Slowikowski (@msl0)
version_added: '1.12.0'
requirements:
    - requests
    - azure
short_description: Read secret from Azure Key Vault.
description:
  - This lookup returns the content of secret saved in Azure Key Vault.
  - When ansible host is MSI enabled Azure VM, user don't need provide any credential to access to Azure Key Vault.
options:
    _terms:
        description: Secret name, version can be included like secret_name/secret_version.
        required: True
    vault_url:
        description: Url of Azure Key Vault.
        required: True
    client_id:
        description: Client id of service principal that has access to the Azure Key Vault
    secret:
        description: Secret of the service principal.
    tenant_id:
        description: Tenant id of service principal.
notes:
    - If version is not provided, this plugin will return the latest version of the secret.
    - If ansible is running on Azure Virtual Machine with MSI enabled, client_id, secret and tenant isn't required.
    - For enabling MSI on Azure VM, please refer to this doc https://docs.microsoft.com/en-us/azure/active-directory/managed-service-identity/
    - After enabling MSI on Azure VM, remember to grant access of the Key Vault to the VM by adding a new Acess Policy in Azure Portal.
    - If MSI is not enabled on ansible host, it's required to provide a valid service principal which has access to the key vault.
    - To authenticate via service principal, pass client_id, secret and tenant_id or set environment variables
      AZURE_CLIENT_ID, AZURE_CLIENT_SECRET and AZURE_TENANT_ID.
    - Authentication via C(az login) is also supported.
    - To use a plugin from a collection, please reference the full namespace, collection name, and lookup plugin name that you want to use.
"""

EXAMPLE = """
- name: Look up secret when azure cli login
  debug:
    msg: msg: "{{ lookup('azure.azcollection.azure_keyvault_secret', 'testsecret', vault_url=key_vault_uri)}}"

- name: Look up secret when ansible host is MSI enabled Azure VM
  debug:
    msg: "the value of this secret is {{
        lookup(
          'azure.azcollection.azure_keyvault_secret',
          'testSecret/version',
          vault_url='https://yourvault.vault.azure.net'
        )
      }}"

- name: Look up secret when ansible host is general VM
  vars:
    url: 'https://yourvault.vault.azure.net'
    secretname: 'testSecret/version'
    client_id: '123456789'
    secret: 'abcdefg'
    tenant: 'uvwxyz'
  debug:
    msg: "the value of this secret is {{
        lookup(
          'azure.azcollection.azure_keyvault_secret',
          secretname,
          vault_url=url,
          client_id=client_id,
          secret=secret,
          tenant_id=tenant
        )
      }}"

# Example below creates an Azure Virtual Machine with SSH public key from key vault using 'azure_keyvault_secret' lookup plugin.
- name: Create Azure VM
  hosts: localhost
  connection: local
  no_log: True
  vars:
    resource_group: myResourceGroup
    vm_name: testvm
    location: eastus
    ssh_key: "{{ lookup('azure.azcollection.azure_keyvault_secret','myssh_key') }}"
  - name: Create VM
    azure_rm_virtualmachine:
      resource_group: "{{ resource_group }}"
      name: "{{ vm_name }}"
      vm_size: Standard_DS1_v2
      admin_username: azureuser
      ssh_password_enabled: false
      ssh_public_keys:
        - path: /home/azureuser/.ssh/authorized_keys
          key_data: "{{ ssh_key }}"
      network_interfaces: "{{ vm_name }}"
      image:
        offer: 0001-com-ubuntu-server-focal
        publisher: Canonical
        sku: 20_04-lts
        version: latest
"""

RETURN = """
  _raw:
    description: secret content string
"""

from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase
from ansible.utils.display import Display
try:
    import logging
    import requests
    from azure.keyvault.secrets import SecretClient
    from azure.identity import DefaultAzureCredential, ClientSecretCredential
    from azure.keyvault.secrets import SecretClient

except ImportError:
    pass

display = Display()

TOKEN_ACQUIRED = False

logger = logging.getLogger("azure.identity").setLevel(logging.ERROR)

token_params = {
    'api-version': '2018-02-01',
    'resource': 'https://vault.azure.net'
}

token_headers = {
    'Metadata': 'true'
}

token = None

try:
    token_res = requests.get('http://169.254.169.254/metadata/identity/oauth2/token', params=token_params, headers=token_headers, timeout=(3.05, 27))
    if token_res.ok:
        token = token_res.json().get("access_token")
        if token is not None:
            TOKEN_ACQUIRED = True
        else:
            display.v('Successfully called MSI endpoint, but no token was available. Will use service principal if provided.')
    else:
        display.v("Unable to query MSI endpoint, Error Code %s. Will use service principal if provided" % token_res.status_code)
except Exception:
    display.v('Unable to fetch MSI token. Will use service principal if provided.')
    TOKEN_ACQUIRED = False


def lookup_secret_non_msi(terms, vault_url, kwargs):

    client_id = kwargs['client_id'] if kwargs.get('client_id') else None
    secret = kwargs['secret'] if kwargs.get('secret') else None
    tenant_id = kwargs['tenant_id'] if kwargs.get('tenant_id') else None

    if all(v is not None for v in [client_id, secret, tenant_id]):
        credential = ClientSecretCredential(
            tenant_id=tenant_id,
            client_id=client_id,
            client_secret=secret,
        )
    else:
        credential = DefaultAzureCredential()
    client = SecretClient(vault_url, credential)

    ret = []
    for term in terms:
        try:
            secret_val = client.get_secret(term).value
            ret.append(secret_val)
        except Exception:
            raise AnsibleError('Failed to fetch secret ' + term + '.')
    return ret


class LookupModule(LookupBase):

    def run(self, terms, variables, **kwargs):
        ret = []
        vault_url = kwargs.pop('vault_url', None)
        if vault_url is None:
            raise AnsibleError('Failed to get valid vault url.')
        if TOKEN_ACQUIRED:
            secret_params = {'api-version': '2016-10-01'}
            secret_headers = {'Authorization': 'Bearer ' + token}
            for term in terms:
                try:
                    secret_res = requests.get(vault_url + '/secrets/' + term, params=secret_params, headers=secret_headers)
                    ret.append(secret_res.json()["value"])
                except KeyError:
                    raise AnsibleError('Failed to fetch secret ' + term + '.')
                except Exception:
                    raise AnsibleError('Failed to fetch secret: ' + term + ' via MSI endpoint.')
            return ret
        else:
            return lookup_secret_non_msi(terms, vault_url, kwargs)