vault 비밀번호 관리하기

vault 비밀번호 관리하기 (Managing vault passwords)

암호화된 콘텐츠를 관리하려면 vault 비밀번호를 관리하는 전략을 세우는 게 일을 훨씬 쉽게 만들어요. vault 비밀번호는 원하는 어떤 문자열이든 될 수 있고, 별도로 만드는 특별한 명령은 없어요. 다만 vault 비밀번호들을 잘 추적해야 해요.

Ansible Vault로 변수나 파일을 암호화할 때마다 비밀번호를 제공해야 하고, 암호화된 변수·파일을 명령이나 플레이북에서 쓸 때는 암호화할 때 사용한 것과 같은 비밀번호를 제공해야 해요. vault 비밀번호 관리 전략은 두 가지 질문에서 시작해요:

  • 모든 콘텐츠를 같은 비밀번호로 암호화할까요, 아니면 용도에 따라 다른 비밀번호를 쓸까요?
  • 비밀번호(들)을 어디에 저장할까요?

출처: 문서

본문

단일 비밀번호와 다중 비밀번호 중 선택하기

팀이 작거나 민감한 값이 적다면, Ansible Vault로 암호화하는 모든 것에 단일 비밀번호를 쓸 수 있어요. vault 비밀번호는 아래에서 설명하듯 파일이나 시크릿 매니저에 안전하게 저장하세요.

팀이 크거나 민감한 값이 많다면 여러 비밀번호를 사용할 수 있어요. 예를 들어 사용자별, 접근 수준별로 다른 비밀번호를 쓸 수 있죠. 필요에 따라 암호화된 파일마다, 디렉터리마다, 또는 환경마다 다른 비밀번호를 원할 수도 있어요. dev 환경용과 production 환경용 두 개의 vars 파일을 서로 다른 두 비밀번호로 암호화해 포함한 플레이북이 있을 수 있어요. 플레이북을 실행할 때 vault ID를 사용해 대상 환경에 맞는 vault 비밀번호를 선택할 수 있어요.

vault ID로 여러 비밀번호 관리하기

여러 vault 비밀번호를 쓴다면 vault ID로 비밀번호들을 구분할 수 있어요. vault ID는 세 가지 방식으로 사용해요:

  • 암호화 콘텐츠를 만들 때 ansible-vault 명령에 --vault-id로 전달
  • 해당 vault ID의 비밀번호를 저장하는 어디든 포함 (Storing and accessing vault passwords 참고)
  • 해당 vault ID로 암호화한 콘텐츠를 쓰는 플레이북을 실행할 때 ansible-playbook 명령에 --vault-id로 전달

ansible-vault 명령에 vault ID를 옵션으로 전달하면 암호화 콘텐츠에 라벨(힌트나 별명)을 추가해요. 이 라벨은 어떤 비밀번호로 암호화했는지를 기록해 주죠. 암호화된 변수나 파일은 헤더에 vault ID 라벨을 평문으로 포함해요. vault ID는 암호화 콘텐츠 앞의 마지막 요소예요. 예를 들어:

my_encrypted_var: !vault |
          $ANSIBLE_VAULT;1.2;AES256;dev
          30613233633461343837653833666333643061636561303338373661313838333565653635353162
          3263363434623733343538653462613064333634333464660a663633623939393439316636633863
          61636237636537333938306331383339353265363239643939666639386530626330633337633833
          6664656334373166630a363736393262666465663432613932613036303963343263623137386239
          6330

라벨 외에도 해당 비밀번호의 **소스(source)**를 제공해야 해요. 소스는 vault 비밀번호를 어떻게 저장하느냐에 따라 프롬프트(prompt), 파일(file), 또는 스크립트(script)가 될 수 있어요. 패턴은 이렇게 생겼어요:

--vault-id label@source

플레이북이 서로 다른 비밀번호로 암호화한 여러 암호화 변수·파일을 사용한다면, 플레이북을 실행할 때 vault ID들을 전달해야 해요. --vault-id는 단독으로, --vault-password-file과 함께, 또는 --ask-vault-pass와 함께 사용할 수 있어요. 패턴은 암호화 콘텐츠를 만들 때와 같아요. 일치하는 비밀번호의 라벨과 소스를 함께 넣어 주면 돼요.

vault ID로 콘텐츠를 암호화하고, vault ID로 암호화한 콘텐츠를 사용하는 예제는 아래에서 볼 수 있어요. --vault-id 옵션은 ansible-vault, ansible-playbook 등 vault를 다루는 어떤 Ansible 명령과도 함께 동작해요.

vault ID의 한계

Ansible은 특정 vault ID 라벨을 사용할 때마다 항상 같은 비밀번호를 쓰도록 강제하지 않아요. 같은 vault ID 라벨로 서로 다른 비밀번호를 써서 다른 변수·파일을 암호화할 수 있어요. 이는 보통 프롬프트에 암호를 입력하다가 실수로 틀릴 때 일어나요. 의도적으로 같은 vault ID 라벨에 다른 비밀번호를 쓰는 것도 가능해요. 예를 들어 각 라벨을 단일 비밀번호가 아니라 비밀번호 "부류(class)"에 대한 참조로 쓸 수 있어요. 이 시나리오에서는 문맥에 맞는 특정 비밀번호나 파일이 무엇인지 항상 알아야 해요. 하지만 실제로는 실수로 같은 vault ID 라벨에 서로 다른 비밀번호로 두 파일을 암호화할 가능성이 더 높아요. 만약 실수로 같은 라벨, 다른 비밀번호로 두 파일을 암호화했다면, 한 파일을 rekey해서 문제를 해결할 수 있어요.

vault ID 일치 강제하기

기본적으로 vault ID 라벨은 어떤 비밀번호로 변수·파일을 암호화했는지 상기시켜 주는 힌트일 뿐이에요. Ansible은 암호화 콘텐츠의 헤더에 있는 vault ID가 사용 시 제공한 vault ID와 일치하는지 확인하지 않아요. Ansible은 명령이나 플레이북이 호출하는, 제공한 비밀번호로 암호화된 모든 파일·변수를 복호화해요. 암호화 콘텐츠를 확인하고, 그것이 담긴 vault ID가 --vault-id로 제공한 것과 일치할 때만 복호화하려면 DEFAULT_VAULT_ID_MATCH 설정을 켜세요. DEFAULT_VAULT_ID_MATCH를 설정하면 각 비밀번호는 같은 라벨로 암호화된 데이터를 복호화하는 데만 사용돼요. 이는 효율적이고 예측 가능하며, 서로 다른 값이 서로 다른 비밀번호로 암호화됐을 때 오류를 줄여 줘요.

참고: DEFAULT_VAULT_ID_MATCH 설정을 켜도 Ansible은 특정 vault ID 라벨을 사용할 때마다 항상 같은 비밀번호를 쓰도록 강제하지는 않아요.

vault 비밀번호 저장하고 접근하기

vault 비밀번호를 외워 두거나, 어떤 소스에서 수동으로 복사해 커맨드 라인 프롬프트에 붙여넣을 수도 있어요. 하지만 대부분의 사용자는 비밀번호를 안전하게 저장하고 Ansible 안에서 필요할 때 접근해요. Ansible 안에서 동작하는 vault 비밀번호 저장 방식은 두 가지예요: 파일에 저장하거나, 시스템 키링(keyring)이나 시크릿 매니저 같은 서드파티 도구에 저장해요. 서드파티 도구에 저장한다면 Ansible 안에서 그 값들을 가져올 vault 비밀번호 클라이언트 스크립트가 필요해요.

파일에 비밀번호 저장하기

vault 비밀번호를 파일에 저장하려면 파일의 한 줄에 비밀번호를 문자열로 입력해요. 파일 권한이 적절한지 확인하고, 비밀번호 파일을 버전 관리에 추가하지 마세요.

파일에 저장한 vault 비밀번호를 사용하는 플레이북을 실행하려면 --vault-password-file 플래그 안에 해당 파일을 지정해요. 예를 들어:

ansible-playbook --extra-vars @secrets.enc --vault-password-file secrets.pass

vault 비밀번호 클라이언트 스크립트로 서드파티 도구에 저장하기

vault 비밀번호를 시스템 키링, 데이터베이스, 또는 시크릿 매니저에 저장하고, vault 비밀번호 클라이언트 스크립트를 사용해 Ansible 안에서 가져올 수 있어요. 비밀번호를 한 줄에 문자열로 입력해요. 비밀번호에 vault ID가 있다면 비밀번호 저장 도구에 맞는 방식으로 저장하세요.

vault 비밀번호 클라이언트 스크립트를 만들려면:

  • 이름이 -client 또는 -client.EXTENSION으로 끝나는 파일을 만든다
  • 파일을 실행 가능하게 만든다
  • 스크립트 자체에서 다음을 수행한다:
    • 비밀번호를 표준 출력으로 출력
    • --vault-id 옵션을 받음
    • 스크립트가 데이터(예: 데이터베이스 비밀번호)를 요구하면 프롬프트를 TTY에 표시

서드파티 도구에 저장한 vault 비밀번호를 쓰는 플레이북을 실행할 때는 --vault-id 플래그 안에서 해당 스크립트를 소스로 지정해요. 예를 들어:

ansible-playbook --vault-id dev@path/to/vault-keyring-client.py

Ansible은 클라이언트 스크립트를 --vault-id 옵션과 함께 실행해서, 스크립트가 여러분이 지정한 vault ID 라벨을 알 수 있게 해 줘요. 예를 들어 시크릿 매니저에서 비밀번호를 불러오는 스크립트는 vault ID 라벨을 사용해 dev 또는 prod 비밀번호 중 하나를 고를 수 있어요. 위 예제 명령은 클라이언트 스크립트를 다음과 같이 실행해요:

path/to/vault-keyring-client.py --vault-id dev

시스템 키링에서 비밀번호를 불러오는 클라이언트 스크립트의 예시는 vault-keyring-client 스크립트를 참고하세요.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# (c) 2014, Matt Martz <[email protected]>
# (c) 2016, Justin Mayer <https://justinmayer.com/>
#
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#
# =============================================================================
#
# This script is to be used with ansible-vault's --vault-id arg
# to retrieve the vault password via your OS's native keyring application.
#
# This file *MUST* be saved with executable permissions. Otherwise, Ansible
# will try to parse as a password file and display: "ERROR! Decryption failed"
#
# The `keyring` Python module is required: https://pypi.org/project/keyring/
#
# By default, this script will store the specified password in the keyring of
# the user that invokes the script. To specify a user keyring, add a [vault]
# section to ~/ansible-vault.cfg file with a 'username' option. Example:
#
# [vault]
# username = 'ansible-vault'
#
# In usage like:
#
#    ansible-vault --vault-id keyring_id@contrib/vault/vault-keyring-client.py view some_encrypted_file
#
#  --vault-id will call this script like:
#
#     contrib/vault/vault-keyring-client.py --vault-id keyring_id
#
# That will retrieve the password from users keyring for the
# keyring service 'keyring_id'. The equivalent of:
#
#      keyring get keyring_id $USER
#
# If no vault-id name is specified to ansible command line, the vault-keyring-client.py
# script will be called without a '--vault-id' and will default to the keyring service 'ansible'
# This is equivalent to:
#
#    keyring get ansible $USER
#
# You can configure the `vault_password_file` option in ansible.cfg:
#
# [defaults]
# ...
# vault_password_file = /path/to/vault-keyring-client.py
# ...
#
# To set your password, `cd` to your project directory and run:
#
#   # will use default keyring service / vault-id of 'ansible'
#   /path/to/vault-keyring-client.py --set
#
# or to specify the keyring service / vault-id of 'my_ansible_secret':
#
#  /path/to/vault-keyring-client.py --vault-id my_ansible_secret --set
#
# If you choose not to configure the path to `vault_password_file` in
# ansible.cfg, your `ansible-playbook` command might look like:
#
# ansible-playbook --vault-id=keyring_id@/path/to/vault-keyring-client.py site.yml

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

import argparse
import configparser
import os
import sys
import getpass
import keyring

KEYNAME_UNKNOWN_RC = 2

def build_arg_parser():
    parser = argparse.ArgumentParser(description='Get a vault password from user keyring')

    parser.add_argument('--vault-id', action='store', default=None,
                        dest='vault_id',
                        help='name of the vault secret to get from keyring')
    parser.add_argument('--username', action='store', default=None,
                        help='the username whose keyring is queried')
    parser.add_argument('--set', action='store_true', default=False,
                        dest='set_password',
                        help='set the password instead of getting it')
    return parser

def main():
    # Set default values
    username = getpass.getuser()
    keyname = 'ansible'

    # Try to load values from config if one exists
    config = configparser.ConfigParser()
    config_file = os.path.expanduser("~/ansible-vault.cfg")
    config.read(config_file)
    if config.has_option('vault', 'username'):
        username = config.get('vault', 'username')
    if config.has_option('vault', 'keyname'):
        keyname = config.get('vault', 'keyname')

    # Read values from command line (which override the previous if given)
    arg_parser = build_arg_parser()
    args = arg_parser.parse_args()

    username = args.username or username
    keyname = args.vault_id or keyname

    if args.set_password:
        intro = 'Storing password in "{}" user keyring using key name: {}\n'
        sys.stdout.write(intro.format(username, keyname))
        password = getpass.getpass()
        confirm = getpass.getpass('Confirm password: ')
        if password == confirm:
            keyring.set_password(keyname, username, password)
        else:
            sys.stderr.write('Passwords do not match\n')
            sys.exit(1)
    else:
        secret = keyring.get_password(keyname, username)
        if secret is None:
            sys.stderr.write('vault-keyring-client could not find key="%s" for user="%s" via backend="%s"\n' %
                             (keyname, username, keyring.get_keyring().name))
            sys.exit(KEYNAME_UNKNOWN_RC)

        sys.stdout.write('%s\n' % secret)

    sys.exit(0)

if __name__ == '__main__':
    main()

더 알아보기 (Learn more)

  • vault ID는 여러 비밀번호를 라벨로 구분하는 메커니즘이에요. --vault-id label@source 패턴으로 만들고 사용해요.
  • 기본적으로 vault ID는 힌트일 뿐이라 일치 검사를 안 해요. 엄격한 일치가 필요하면 DEFAULT_VAULT_ID_MATCH를 켜세요.
  • 비밀번호는 파일이나 서드파티 도구에 저장하고, 서드파티 도구에는 클라이언트 스크립트로 접근해요.