perf: automation change secret linux

pull/8991/head
feng 2022-10-20 20:34:15 +08:00
parent 26278cc9e0
commit 091bffa626
5 changed files with 96 additions and 34 deletions

View File

@ -77,9 +77,9 @@ class BasePlaybookManager:
def generate_inventory(self, platformed_assets, inventory_path): def generate_inventory(self, platformed_assets, inventory_path):
inventory = JMSInventory( inventory = JMSInventory(
manager=self,
assets=platformed_assets, assets=platformed_assets,
account_policy=self.ansible_account_policy, account_policy=self.ansible_account_policy,
host_callback=self.host_callback
) )
inventory.write_to_file(inventory_path) inventory.write_to_file(inventory_path)

View File

@ -3,24 +3,36 @@
tasks: tasks:
- name: Test privileged account - name: Test privileged account
ansible.builtin.ping: ansible.builtin.ping:
# #
# - name: print variables # - name: print variables
# debug: # debug:
# msg: "Username: {{ account.username }}, Secret: {{ account.secret }}, Secret type: {{ account.secret_type }}" # msg: "Username: {{ account.username }}, Secret: {{ account.secret }}, Secret type: {{ secret_type }}"
- name: Change password - name: Change password
ansible.builtin.user: ansible.builtin.user:
name: "{{ account.username }}" name: "{{ account.username }}"
password: "{{ account.secret | password_hash('sha512') }}" password: "{{ account.secret | password_hash('sha512') }}"
update_password: always update_password: always
when: account.secret_type == "password" when: "{{ secret_type == 'password' }}"
- name: Change public key - name: create user If it already exists, no operation will be performed
ansible.builtin.user:
name: "{{ account.username }}"
when: "{{ secret_type == 'ssh_key' }}"
- name: remove jumpserver ssh key
ansible.builtin.lineinfile:
dest: "{{ kwargs.dest }}"
regexp: "{{ kwargs.regexp }}"
state: absent
when: "{{ secret_type == 'ssh_key' and kwargs.strategy == 'set_jms' }}"
- name: Change SSH key
ansible.builtin.authorized_key: ansible.builtin.authorized_key:
user: "{{ account.username }}" user: "{{ account.username }}"
key: "{{ account.public_key }}" key: "{{ account.secret }}"
state: present exclusive: "{{ kwargs.exclusive }}"
when: account.secret_type == "public_key" when: "{{ secret_type == 'ssh_key' }}"
- name: Refresh connection - name: Refresh connection
ansible.builtin.meta: reset_connection ansible.builtin.meta: reset_connection
@ -32,3 +44,13 @@
ansible_user: "{{ account.username }}" ansible_user: "{{ account.username }}"
ansible_password: "{{ account.secret }}" ansible_password: "{{ account.secret }}"
ansible_become: no ansible_become: no
when: "{{ secret_type == 'password' }}"
- name: Verify SSH key
ansible.builtin.ping:
become: no
vars:
ansible_user: "{{ account.username }}"
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
ansible_become: no
when: "{{ secret_type == 'ssh_key' }}"

View File

@ -1,7 +1,7 @@
- hosts: demo - hosts: demo
gather_facts: no gather_facts: no
tasks: tasks:
- name: ping - name: Test privileged account
ansible.windows.win_ping: ansible.windows.win_ping:
# - name: Print variables # - name: Print variables
@ -13,7 +13,7 @@
name: "{{ account.username }}" name: "{{ account.username }}"
password: "{{ account.secret }}" password: "{{ account.secret }}"
update_password: always update_password: always
when: account.secret_type == "password" when: "{{ account.secret_type == 'password' }}"
- name: Refresh connection - name: Refresh connection
ansible.builtin.meta: reset_connection ansible.builtin.meta: reset_connection
@ -23,3 +23,4 @@
vars: vars:
ansible_user: "{{ account.username }}" ansible_user: "{{ account.username }}"
ansible_password: "{{ account.secret }}" ansible_password: "{{ account.secret }}"
when: "{{ account.secret_type == 'password' }}"

View File

@ -1,14 +1,17 @@
import os
import random import random
import string import string
from hashlib import md5
from copy import deepcopy from copy import deepcopy
from socket import gethostname
from collections import defaultdict from collections import defaultdict
from django.utils import timezone from django.utils import timezone
from common.utils import lazyproperty, gen_key_pair from common.utils import lazyproperty, gen_key_pair, ssh_pubkey_gen, ssh_key_string_to_obj
from assets.models import ChangeSecretRecord from assets.models import ChangeSecretRecord
from assets.const import ( from assets.const import (
AutomationTypes, SecretType, SecretStrategy, DEFAULT_PASSWORD_RULES AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy, DEFAULT_PASSWORD_RULES
) )
from ..base.manager import BasePlaybookManager from ..base.manager import BasePlaybookManager
@ -17,15 +20,15 @@ class ChangeSecretManager(BasePlaybookManager):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.method_hosts_mapper = defaultdict(list) self.method_hosts_mapper = defaultdict(list)
self.secret_type = self.execution.plan_snapshot.get('secret_type')
self.secret_strategy = self.execution.plan_snapshot['secret_strategy'] self.secret_strategy = self.execution.plan_snapshot['secret_strategy']
self.ssh_key_change_strategy = self.execution.plan_snapshot['ssh_key_change_strategy']
self._password_generated = None self._password_generated = None
self._ssh_key_generated = None self._ssh_key_generated = None
self.name_recorder_mapper = {} # 做个映射,方便后面处理 self.name_recorder_mapper = {} # 做个映射,方便后面处理
@classmethod @classmethod
def method_type(cls): def method_type(cls):
return AutomationTypes.change_secret return AutomationTypes.method_id_meta_mapper
@lazyproperty @lazyproperty
def related_accounts(self): def related_accounts(self):
@ -36,6 +39,19 @@ class ChangeSecretManager(BasePlaybookManager):
private_key, public_key = gen_key_pair() private_key, public_key = gen_key_pair()
return private_key return private_key
@staticmethod
def generate_public_key(private_key):
return ssh_pubkey_gen(private_key=private_key, hostname=gethostname())
@staticmethod
def generate_private_key_path(secret, path_dir):
key_name = '.' + md5(secret.encode('utf-8')).hexdigest()
key_path = os.path.join(path_dir, key_name)
if not os.path.exists(key_path):
ssh_key_string_to_obj(secret, password=None).write_private_key_file(key_path)
os.chmod(key_path, 0o400)
return key_path
def generate_password(self): def generate_password(self):
kwargs = self.automation.plan_snapshot['password_rules'] or {} kwargs = self.automation.plan_snapshot['password_rules'] or {}
length = int(kwargs.get('length', DEFAULT_PASSWORD_RULES['length'])) length = int(kwargs.get('length', DEFAULT_PASSWORD_RULES['length']))
@ -77,16 +93,29 @@ class ChangeSecretManager(BasePlaybookManager):
else: else:
return self.generate_password() return self.generate_password()
def get_secret(self, account): def get_secret(self):
if account.secret_type == SecretType.ssh_key: if self.secret_type == SecretType.ssh_key:
secret = self.get_ssh_key() secret = self.get_ssh_key()
elif account.secret_type == SecretType.password: elif self.secret_type == SecretType.password:
secret = self.get_password() secret = self.get_password()
else: else:
raise ValueError("Secret must be set") raise ValueError("Secret must be set")
return secret return secret
def host_callback(self, host, asset=None, account=None, automation=None, **kwargs): def get_kwargs(self, account, secret):
kwargs = {}
if self.secret_type != SecretType.ssh_key:
return kwargs
kwargs['strategy'] = self.automation.plan_snapshot['ssh_key_change_strategy']
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
if kwargs['strategy'] == SSHKeyStrategy.set_jms:
kwargs['dest'] = '/home/{}/.ssh/authorized_keys'.format(account.username)
kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip())
return kwargs
def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs):
host = super().host_callback(host, asset=asset, account=account, automation=automation, **kwargs) host = super().host_callback(host, asset=asset, account=account, automation=automation, **kwargs)
if host.get('error'): if host.get('error'):
return host return host
@ -95,7 +124,9 @@ class ChangeSecretManager(BasePlaybookManager):
if account: if account:
accounts = accounts.exclude(id=account.id) accounts = accounts.exclude(id=account.id)
if '*' not in self.automation.accounts: if '*' not in self.automation.accounts:
accounts = accounts.filter(username__in=self.automation.accounts) accounts = accounts.filter(
username__in=self.automation.accounts, secret_type=self.secret_type
)
method_attr = getattr(automation, self.method_type() + '_method') method_attr = getattr(automation, self.method_type() + '_method')
method_hosts = self.method_hosts_mapper[method_attr] method_hosts = self.method_hosts_mapper[method_attr]
@ -103,11 +134,12 @@ class ChangeSecretManager(BasePlaybookManager):
inventory_hosts = [] inventory_hosts = []
records = [] records = []
host['secret_type'] = self.secret_type
for account in accounts: for account in accounts:
h = deepcopy(host) h = deepcopy(host)
h['name'] += '_' + account.username h['name'] += '_' + account.username
new_secret = self.get_secret()
new_secret = self.get_secret(account)
recorder = ChangeSecretRecord( recorder = ChangeSecretRecord(
account=account, execution=self.execution, account=account, execution=self.execution,
old_secret=account.secret, new_secret=new_secret, old_secret=account.secret, new_secret=new_secret,
@ -115,11 +147,19 @@ class ChangeSecretManager(BasePlaybookManager):
records.append(recorder) records.append(recorder)
self.name_recorder_mapper[h['name']] = recorder self.name_recorder_mapper[h['name']] = recorder
private_key_path = None
if self.secret_type == SecretType.ssh_key:
private_key_path = self.generate_private_key_path(new_secret, path_dir)
new_secret = self.generate_public_key(new_secret)
h['kwargs'] = self.get_kwargs(account, new_secret)
h['account'] = { h['account'] = {
'name': account.name, 'name': account.name,
'username': account.username, 'username': account.username,
'secret_type': account.secret_type, 'secret_type': account.secret_type,
'secret': new_secret, 'secret': new_secret,
'private_key_path': private_key_path
} }
inventory_hosts.append(h) inventory_hosts.append(h)
method_hosts.append(h['name']) method_hosts.append(h['name'])

View File

@ -5,28 +5,26 @@ from collections import defaultdict
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
__all__ = ['JMSInventory'] __all__ = ['JMSInventory']
class JMSInventory: class JMSInventory:
def __init__(self, assets, account_policy='smart', account_prefer='root,administrator', host_callback=None): def __init__(self, manager, assets=None, account_policy='smart', account_prefer='root,administrator'):
""" """
:param assets: :param assets:
:param account_prefer: account username name if not set use account_policy :param account_prefer: account username name if not set use account_policy
:param account_policy: :param account_policy: smart, privileged_must, privileged_first
:param host_callback: after generate host, call this callback to modify host
""" """
self.manager = manager
self.assets = self.clean_assets(assets) self.assets = self.clean_assets(assets)
self.account_prefer = account_prefer self.account_prefer = account_prefer
self.account_policy = account_policy self.account_policy = account_policy
self.host_callback = host_callback
@staticmethod @staticmethod
def clean_assets(assets): def clean_assets(assets):
from assets.models import Asset from assets.models import Asset
asset_ids = [asset.id for asset in assets] asset_ids = [asset.id for asset in assets]
assets = Asset.objects.filter(id__in=asset_ids, is_active=True)\ assets = Asset.objects.filter(id__in=asset_ids, is_active=True) \
.prefetch_related('platform', 'domain', 'accounts') .prefetch_related('platform', 'domain', 'accounts')
return assets return assets
@ -107,7 +105,7 @@ class JMSInventory:
'protocol': asset.protocol, 'port': asset.port, 'protocol': asset.protocol, 'port': asset.port,
'protocols': [{'name': p.name, 'port': p.port} for p in protocols], 'protocols': [{'name': p.name, 'port': p.port} for p in protocols],
}, },
'jms_account': { 'jms_account': {
'id': str(account.id), 'username': account.username, 'id': str(account.id), 'username': account.username,
'secret': account.secret, 'secret_type': account.secret_type 'secret': account.secret, 'secret_type': account.secret_type
} if account else None } if account else None
@ -156,7 +154,7 @@ class JMSInventory:
account_selected = accounts[0] if accounts else None account_selected = accounts[0] if accounts else None
return account_selected return account_selected
def generate(self): def generate(self, path_dir):
hosts = [] hosts = []
platform_assets = self.group_by_platform(self.assets) platform_assets = self.group_by_platform(self.assets)
for platform, assets in platform_assets.items(): for platform, assets in platform_assets.items():
@ -170,10 +168,11 @@ class JMSInventory:
if not automation.ansible_enabled: if not automation.ansible_enabled:
host['error'] = _('Ansible disabled') host['error'] = _('Ansible disabled')
if self.host_callback is not None: if self.manager.host_callback is not None:
host = self.host_callback( host = self.manager.host_callback(
host, asset=asset, account=account, host, asset=asset, account=account,
platform=platform, automation=automation platform=platform, automation=automation,
path_dir=path_dir
) )
if isinstance(host, list): if isinstance(host, list):
@ -195,8 +194,8 @@ class JMSInventory:
return data return data
def write_to_file(self, path): def write_to_file(self, path):
data = self.generate()
path_dir = os.path.dirname(path) path_dir = os.path.dirname(path)
data = self.generate(path_dir)
if not os.path.exists(path_dir): if not os.path.exists(path_dir):
os.makedirs(path_dir, 0o700, True) os.makedirs(path_dir, 0o700, True)
with open(path, 'w') as f: with open(path, 'w') as f: