英文:
Managing remotely-generated API keys with Ansible
问题
我正在使用Ansible来配置特定的服务,在与其交互之前,我必须首先生成一个API密钥。但我不能在我的Playbook中预定义该密钥(作为一个机密) - 它是由服务器生成的,在第一次返回给我后,将不会再次暴露(这是API密钥的典型情况)。
我可以要求服务器在每次运行Playbook或Playbook中的标签时生成一个新的API密钥,但这会很慢。理想情况下,我应该将API密钥保存在本地并重复使用。
我的方法是简单地将其写入到我的用户目录下的本地文件中(~/.ansible/custom/api_key.txt
),以便有一些保护。这种方法可行,但感觉有点不太规范。
Ansible是否有官方/健壮的方法来处理这种情况?
英文:
I'm using ansible to provision a particular service, and before I can interact with it I must first generate an API key. But I can't predefine that key in my playbook (as a secret) - it is generated by the server, returned to me once, and will never be exposed again (the typical scnenario for API keys).
I could ask the server to generate a new API key every time I run that playbook, or tags in that playbook, but that is very slow. Ideally I should save that API key locally and reuse it.
My approach has been to simply write it to a local file in my user directory (~/.ansible/custom/api_key.txt
) so it has some protection. That works but feels kind of dirty.
Does ansible have an official / robust way to handle this scenario?
答案1
得分: 1
这是一个比较广泛的问题。在你的playbooks中处理机密信息有多种方法;这篇文章描述了几种选项,还有许多在线文章涵盖了类似的主题。
一个简单的选项可能是将API密钥加密到一个GPG公钥中,只有在你登录并提供密码时才能获得私钥。
以下是一个简单(不特别健壮)的示例:
- hosts: localhost
gather_facts: false
tasks:
# 检查我们缓存API密钥的文件是否存在。
# 如果不存在,从API获取API密钥并存储在
# GPG加密的文件中。
- when: apikey_file is not file
block:
# 这只是一个虚拟任务,用于提供一个字符串;你当然会用适用于获取API密钥的逻辑来替换它。
- name: 从API获取密钥
command: echo secret.key
register: apikey
# 使用存储在apikey_gpg_id中的身份的公钥加密密钥。
- name: 将API密钥写入文件
command: >-
gpg -o "{{ apikey_file }}" -e -r "{{ apikey_gpg_id }}"
args:
stdin: "{{ apikey.stdout }}"
- hosts: localhost
gather_facts: false
tasks:
# 解密API密钥文件并输出到stdout。这需要我们输入
# 密码(在你的GPG代理中可能会缓存一段时间)。
- name: 从文件读取API密钥
command: >-
gpg -d "{{ apikey_file }}"
register: apikey
# 显示前一个任务获取的内容。
- debug:
var: apikey.stdout
这假定变量 apikey_file
和 apikey_gpg_id
在事先定义好了 - 我将它们放在 group_vars/all.yaml
中,但根据你的项目结构,它们也可以在清单文件或其他地方定义。
英文:
This is kind of a broad question. There are a variety of ways of handling secrets in your playbooks; this article describes several options, and there are a variety of articles online that cover similar topics.
A simple option might be to encrypt the API key to a GPG public key for which the private key is only available when you're logged in and able to provide the passphrase.
Here's a simple (i.e. not particularly robust) example:
- hosts: localhost
gather_facts: false
tasks:
# Check if the file in which we cache the API key exists.
# If not, fetch the API key from the API and store it in
# a GPG-encrypted file.
- when: apikey_file is not file
block:
# This is just a dummy task to give us a string; you would of
# course replace this with the logic to acquire an API key.
- name: Get key from API
command: echo secret.key
register: apikey
# Encrypt the key using the public key for the identity
# stored in apikey_gpg_id.
- name: Write API key to file
command: >-
gpg -o "{{ apikey_file }}" -e -r "{{ apikey_gpg_id }}"
args:
stdin: "{{ apikey.stdout }}"
- hosts: localhost
gather_facts: false
tasks:
# Decrypt the API key file to stdout. This requires us to type in
# the passphrase (which may be cached for some amount of time in your
# GPG agent).
- name: Read API key from file
command: >-
gpg -d "{{ apikey_file }}"
register: apikey
# Show what we got from the previous task.
- debug:
var: apikey.stdout
This presumes that the variables apikey_file
and apikey_gpg_id
are defined in advance -- I've put them in group_vars/all.yaml
, but they could also be defined in your inventory or elsewhere depending on how your project is structured.
答案2
得分: 1
使用 passwordstore。Ansible提供了一个查找插件。参见
shell> ansible-doc -t lookup passwordstore
在当前目录中安装、初始化 passwordstore 用于测试,并导出路径
shell> export PASSWORD_STORE_DIR=$PWD/.password-store
显示内容。passwordstore 是空的
shell> pass
Password Store
创建一个用于测试的项目
shell> tree -a .
.
├── ansible.cfg
├── hosts
├── .password-store
│   └── .gpg-id
└── pb.yml
shell> cat ansible.cfg
[defaults]
gathering = explicit
collections_path = $HOME/.local/lib/python3.9/site-packages/
inventory = $PWD/hosts
roles_path = $PWD/roles
remote_tmp = ~/.ansible/tmp
retry_files_enabled = false
stdout_callback = yaml
创建清单
shell> cat hosts
host_A
host_B
host_C
在第一个块中测试是否缺少或为空的 apikey(s)
- hosts: all
vars:
project: test-400
passwd_dict: "{{ dict(passwd_out.results|
json_query('[].[item, ansible_facts.dummy]')) }}"
tasks:
- name: Test apikey is empty or missing
block:
- set_fact:
dummy: "{{ lookup('community.general.passwordstore',
entity,
missing='empty') }}"
loop: "{{ ansible_play_hosts_all }}"
loop_control:
label: "{{ entity }}"
register: passwd_out
vars:
entity: "{{ project }}/{{ item }}/apikey"
- debug:
var: passwd_out
when: debug_classified|d(false)|bool
- debug:
var: passwd_dict
when: debug_classified|d(false)|bool
run_once: true
因为 passwordstore 为空,我们得到
passwd_dict:
host_A: ''
host_B: ''
host_C: ''
在第二块中,如果为空或缺少 apikey(s),则获取并存储 apikey(s)。根据您的需求更改获取 apikey 的方法
- name: Get and store apikey if empty or missing
block:
- name: Get apikey
set_fact:
apikey: "{{ lookup('password', '/dev/null', seed=inventory_hostname) }}"
- debug:
var: apikey
when: debug_classified|d(false)|bool
- name: Store apikey
set_fact:
dummy: "{{ lookup('community.general.passwordstore',
entity,
create=true,
userpass=apikey) }}"
vars:
entity: "{{ project }}/{{ inventory_hostname }}/apikey"
when: passwd_dict[inventory_hostname]|length == 0
得到创建的 apikey(s)
TASK [debug] **********************************************************************************
ok: [host_B] =>
apikey: YlOTwY9jviKhVaxokzbj
ok: [host_A] =>
apikey: szHcyJNh-vnU-XXsuWt-
ok: [host_C] =>
apikey: 67x4AcAK6_6liU1Ji,8u
passname(s) 存储在 passwordstore 中
shell> pass
Password Store
└── test-400
├── host_A
│   └── apikey
├── host_B
│   └── apikey
└── host_C
└── apikey
shell> pass test-400/host_A/apikey
szHcyJNh-vnU-XXsuWt-
lookup_pass: First generated by ansible on 26/06/2023 12:34:59
shell> pass test-400/host_B/apikey
YlOTwY9jviKhVaxokzbj
lookup_pass: First generated by ansible on 26/06/2023 12:34:59
shell> pass test-400/host_C/apikey
67x4AcAK6_6liU1Ji,8u
lookup_pass: First generated by ansible on 26/06/2023 12:34:59
在播放中,您可以将 apikey(s) 的范围限制为一个任务。例如,
- name: Limit scope of apikey(s) to task
block:
- set_fact:
dummy: ''
passwd_dict: {}
- debug:
msg: "Use {{ entity }}: {{ apikey }}"
vars:
entity: "{{ project }}/{{ inventory_hostname }}/apikey"
apikey: "{{ lookup('community.general.passwordstore', entity) }}"
when: scope|d('play') == 'task'
给出
TASK [debug] **********************************************************************************
ok: [host_A] =>
msg: 'Use test-400/host_A/apikey: szHcyJNh-vnU-XXsuWt-'
ok: [host_C] =>
msg: 'Use test-400/host_C/apikey: 67x4AcAK6_6liU1Ji,8u'
ok: [host_B] =>
msg: 'Use test-400/host_B/apikey: YlOTwY9jviKhVaxokzbj'
否则,在您确定播放范围适用时,请使用字典 passwd_dict。下面的块给出了相同的结果
- name: No limit of apikey(s)
block:
- set_fact:
dummy: ''
- debug:
msg: "Use {{ entity }}: {{ passwd_dict[inventory_hostname] }}"
vars:
entity: "{{ project }}/{{ inventory_hostname }}/apikey"
when: scope|d(&#
<details>
<summary>英文:</summary>
Use [passwordstore](https://www.passwordstore.org/). Ansible provides a lookup plugin. See
```bash
shell> ansible-doc -t lookup passwordstore
Install, initialize passwordstore in the current directory for testing, and export path
shell> export PASSWORD_STORE_DIR=$PWD/.password-store
Show content. The passwordstore is empty
shell> pass
Password Store
Create a project for testing
shell> tree -a .
.
├── ansible.cfg
├── hosts
├── .password-store
│   └── .gpg-id
└── pb.yml
shell> cat ansible.cfg
[defaults]
gathering = explicit
collections_path = $HOME/.local/lib/python3.9/site-packages/
inventory = $PWD/hosts
roles_path = $PWD/roles
remote_tmp = ~/.ansible/tmp
retry_files_enabled = false
stdout_callback = yaml
Create inventory
shell> cat hosts
host_A
host_B
host_C
In the first block test whether apikey(s) are missing or empty
- hosts: all
vars:
project: test-400
passwd_dict: "{{ dict(passwd_out.results|
json_query('[].[item, ansible_facts.dummy]')) }}"
tasks:
- name: Test apikey is empty or missing
block:
- set_fact:
dummy: "{{ lookup('community.general.passwordstore',
entity,
missing='empty') }}"
loop: "{{ ansible_play_hosts_all }}"
loop_control:
label: "{{ entity }}"
register: passwd_out
vars:
entity: "{{ project }}/{{ item }}/apikey"
- debug:
var: passwd_out
when: debug_classified|d(false)|bool
- debug:
var: passwd_dict
when: debug_classified|d(false)|bool
run_once: true
Because the passwordstore is empty we get
passwd_dict:
host_A: ''
host_B: ''
host_C: ''
In the second block get and store apikey(s) if empty or missing. Change the method of getting the apikey to your needs
- name: Get and store apikey if empty or missing
block:
- name: Get apikey
set_fact:
apikey: "{{ lookup('password', '/dev/null', seed=inventory_hostname) }}"
- debug:
var: apikey
when: debug_classified|d(false)|bool
- name: Store apikey
set_fact:
dummy: "{{ lookup('community.general.passwordstore',
entity,
create=true,
userpass=apikey) }}"
vars:
entity: "{{ project }}/{{ inventory_hostname }}/apikey"
when: passwd_dict[inventory_hostname]|length == 0
gives the created apikey(s)
TASK [debug] **********************************************************************************
ok: [host_B] =>
apikey: YlOTwY9jviKhVaxokzbj
ok: [host_A] =>
apikey: szHcyJNh-vnU-XXsuWt-
ok: [host_C] =>
apikey: 67x4AcAK6_6liU1Ji,8u
The passname(s) were stored in passwordstore
shell> pass
Password Store
└── test-400
├── host_A
│   └── apikey
├── host_B
│   └── apikey
└── host_C
└── apikey
shell> pass test-400/host_A/apikey
szHcyJNh-vnU-XXsuWt-
lookup_pass: First generated by ansible on 26/06/2023 12:34:59
shell> pass test-400/host_B/apikey
YlOTwY9jviKhVaxokzbj
lookup_pass: First generated by ansible on 26/06/2023 12:34:59
shell> pass test-400/host_C/apikey
67x4AcAK6_6liU1Ji,8u
lookup_pass: First generated by ansible on 26/06/2023 12:34:59
In the play, you can limit the scope of the apikey(s) to a task. For example,
- name: Limit scope of apikey(s) to task
block:
- set_fact:
dummy: ''
passwd_dict: {}
- debug:
msg: "Use {{ entity }}: {{ apikey }}"
vars:
entity: "{{ project }}/{{ inventory_hostname }}/apikey"
apikey: "{{ lookup('community.general.passwordstore', entity) }}"
when: scope|d('play') == 'task'
gives
TASK [debug] **********************************************************************************
ok: [host_A] =>
msg: 'Use test-400/host_A/apikey: szHcyJNh-vnU-XXsuWt-'
ok: [host_C] =>
msg: 'Use test-400/host_C/apikey: 67x4AcAK6_6liU1Ji,8u'
ok: [host_B] =>
msg: 'Use test-400/host_B/apikey: YlOTwY9jviKhVaxokzbj'
Otherwise, use the dictionary passwd_dict when you decide the play scope is fine. The below block gives the same result
- name: No limit of apikey(s)
block:
- set_fact:
dummy: ''
- debug:
msg: "Use {{ entity }}: {{ passwd_dict[inventory_hostname] }}"
vars:
entity: "{{ project }}/{{ inventory_hostname }}/apikey"
when: scope|d('play') == 'play'
<hr>
<sup>
Example of a complete playbook for testing
- hosts: all
vars:
project: test-400
passwd_dict: "{{ dict(passwd_out.results|
json_query('[].[item, ansible_facts.dummy]')) }}"
tasks:
- name: Test apikey is empty or missing
block:
- set_fact:
dummy: "{{ lookup('community.general.passwordstore',
entity,
missing='empty') }}"
loop: "{{ ansible_play_hosts_all }}"
loop_control:
label: "{{ entity }}"
register: passwd_out
vars:
entity: "{{ project }}/{{ item }}/apikey"
- debug:
var: passwd_out
when: debug_classified|d(false)|bool
- debug:
var: passwd_dict
when: debug_classified|d(false)|bool
run_once: true
- name: Get and store apikey if empty or missing
block:
- name: Get apikey
set_fact:
apikey: "{{ lookup('password', '/dev/null', seed=inventory_hostname) }}"
- debug:
var: apikey
when: debug_classified|d(false)|bool
- name: Store apikey
set_fact:
dummy: "{{ lookup('community.general.passwordstore',
entity,
create=true,
userpass=apikey) }}"
vars:
entity: "{{ project }}/{{ inventory_hostname }}/apikey"
when: passwd_dict[inventory_hostname]|length == 0
- name: Limit scope of apikey(s) to task
block:
- set_fact:
dummy: ''
passwd_dict: {}
- debug:
msg: "Use {{ entity }}: {{ apikey }}"
vars:
entity: "{{ project }}/{{ inventory_hostname }}/apikey"
apikey: "{{ lookup('community.general.passwordstore', entity) }}"
when: scope|d('play') == 'task'
- name: No limit of apikey(s)
block:
- set_fact:
dummy: ''
- debug:
msg: "Use {{ entity }}: {{ passwd_dict[inventory_hostname] }}"
vars:
entity: "{{ project }}/{{ inventory_hostname }}/apikey"
when: scope|d('play') == 'play'
</sup>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论