提问人:lonix 提问时间:6/26/2023 最后编辑:lonix 更新时间:6/26/2023 访问量:73
使用 Ansible 管理远程生成的 API 密钥
Managing remotely-generated API keys with Ansible
问:
我正在使用 ansible 来配置特定服务,在与其交互之前,我必须首先生成一个 API 密钥。但是我无法在我的剧本中预定义该密钥(作为机密)——它由服务器生成,返回给我一次,并且永远不会再次公开(API 密钥的典型 scnenario)。
每次运行该 playbook 或该 playbook 中的标签时,我都可以要求服务器生成一个新的 API 密钥,但这非常慢。理想情况下,我应该将该 API 密钥保存在本地并重复使用它。
我的方法是简单地将其写入我的用户目录()中的本地文件,以便它有一些保护。这有效,但感觉有点脏。~/.ansible/custom/api_key.txt
ansible 是否有官方/强大的方法来处理这种情况?
答:
这是一个宽泛的问题。在你的剧本中,有多种处理秘密的方法;本文介绍了几个选项,网上有各种涵盖类似主题的文章。
一个简单的选项可能是将 API 密钥加密为 GPG 公钥,该公钥仅在您登录并能够提供密码时才可用。
下面是一个简单(即不是特别健壮)的例子:
- 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
这假设变量 和 是预先定义的——我已经把它们放进去了,但它们也可以在你的清单或其他地方定义,这取决于你的项目结构。apikey_file
apikey_gpg_id
group_vars/all.yaml
评论
no_log: true
使用 passwordstore。Ansible 提供了一个查找插件。看
shell> ansible-doc -t lookup passwordstore
在当前目录下安装、初始化密码库进行测试,并导出路径
shell> export PASSWORD_STORE_DIR=$PWD/.password-store
显示内容。密码库为空
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 是否丢失或为空
- 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
因为密码库是空的,我们得到
passwd_dict:
host_A: ''
host_B: ''
host_C: ''
在第二个块中,如果为空或缺失,则获取并存储 apikey。根据需要更改获取 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
TASK [debug] **********************************************************************************
ok: [host_B] =>
apikey: YlOTwY9jviKhVaxokzbj
ok: [host_A] =>
apikey: szHcyJNh-vnU-XXsuWt-
ok: [host_C] =>
apikey: 67x4AcAK6_6liU1Ji,8u
密码存储在密码库中
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 的范围限制为任务。例如
- 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('play') == 'play'
用于测试的完整 playbook 示例
- 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'
评论