Skip to content

Commit

Permalink
refactor runtime secret handling (#66)
Browse files Browse the repository at this point in the history
  • Loading branch information
ansibleguy committed Sep 22, 2024
1 parent 6a74153 commit d610082
Show file tree
Hide file tree
Showing 9 changed files with 64 additions and 57 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* Enable SSH host-key checking by default (*ansible-runner seem to disable it by default*)
* Enhanced Bottons of Log Live-View
* Git-Repository Cleanup-Hook (Post Job-Run)
* Refactored runtime secret-handling (`ansible-runner <https://ansible.readthedocs.io/projects/runner/en/latest/intro>`_)

----

Expand Down
14 changes: 1 addition & 13 deletions docs/source/usage/security.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,7 @@ Security considerations this project does take into account:

* Job secrets like passwords are never returned to the user/Web-UI

* Job secrets are not passed as commandline-arguments but written to files:

Example:

.. code-block:: bash
[INFO] [play] Running job 'test': 'ansible-playbook --become-password-file /tmp/ansible-webui/2024-01-26_21-14-0066101/.aw_become_pass --vault-password-file /tmp/ansible-webui/2024-01-26_21-14-0066101/.aw_vault_pass -i inventory/hosts.yml --limit myHost playbook1.yml'
These files are:

* created with mode 0600

* overwritten and deleted at execution-cleanup
* Runtime handling of secrets is done by the official `ansible-runner <https://ansible.readthedocs.io/projects/runner/en/latest/intro>`_ module (using :code:`pexpect` and :code:`ssh-agent`)

* Usage of GitHub's `dependabot <https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-supply-chain-security#what-is-dependabot>`_ and `CodeQL <https://docs.github.com/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/about-code-scanning-with-codeql>`_

Expand Down
2 changes: 1 addition & 1 deletion src/ansibleguy-webui/aw/execute/play.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def _cancel_job() -> bool:
runner_cfg.prepare()
command = ' '.join(runner_cfg.command)
log(msg=f"Running job '{job.name}': '{command}'", level=5)
execution.command = command
execution.command = command[command.find('ansible-playbook'):]
execution.save()

runner = Runner(config=runner_cfg, cancel_callback=_cancel_job)
Expand Down
30 changes: 18 additions & 12 deletions src/ansibleguy-webui/aw/execute/play_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from aw.utils.permission import has_credentials_permission, CHOICE_PERMISSION_READ
from aw.base import USERS
from aw.utils.debug import log # log_warn
from aw.utils.util import is_set, is_null
from aw.execute.util import config_error, write_file_0600
from aw.utils.util import is_set, is_null, write_file_0600
from aw.execute.util import config_error


def get_pwd_file(path_run: (str, Path), attr: str) -> str:
Expand Down Expand Up @@ -95,16 +95,22 @@ def get_credentials_to_use(job: Job, execution: JobExecution) -> (BaseJobCredent
return credentials


def commandline_arguments_credentials(credentials: BaseJobCredentials, path_run: Path) -> list:
cmd_arguments = []
def get_runner_credential_args(creds: BaseJobCredentials) -> dict:
args = {}

for attr, flag in BaseJobCredentials.PUBLIC_ATTRS_ARGS.items():
if is_set(getattr(credentials, attr)):
cmd_arguments.append(f'{flag} {getattr(credentials, attr)}')
if is_set(creds.ssh_key):
args['ssh_key'] = creds.ssh_key

for attr in BaseJobCredentials.SECRET_ATTRS:
pwd_arg = get_pwd_file_arg(credentials, attr=attr, path_run=path_run)
if pwd_arg is not None:
cmd_arguments.append(pwd_arg)
if is_set(creds.connect_pass) or is_set(creds.become_pass) or is_set(creds.vault_pass):
args['passwords'] = {}

return cmd_arguments
if is_set(creds.connect_pass):
args['passwords']['SSH password:'] = creds.connect_pass

if is_set(creds.become_pass):
args['passwords']['BECOME password:'] = creds.become_pass

if is_set(creds.vault_pass):
args['passwords']['Vault password:'] = creds.vault_pass

return args
42 changes: 22 additions & 20 deletions src/ansibleguy-webui/aw/execute/play_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@

from aw.config.main import config
from aw.utils.util import is_set, datetime_w_tz, write_file_0640
from aw.model.job import Job, JobExecution, JobExecutionResult, JobExecutionResultHost, JobError
from aw.model.job_credential import BaseJobCredentials
from aw.model.job import Job, JobExecution, JobExecutionResult, JobExecutionResultHost, JobError
from aw.execute.util import update_status, overwrite_and_delete_file, decode_job_env_vars, \
create_dirs, is_execution_status, config_error
from aw.utils.debug import log
from aw.execute.play_credentials import get_credentials_to_use, commandline_arguments_credentials, \
write_pwd_file, get_pwd_file
from aw.execute.repository import ExecuteRepository
from aw.execute.play_credentials import get_runner_credential_args, get_credentials_to_use

# see: https://ansible.readthedocs.io/projects/runner/en/latest/intro/

Expand All @@ -34,7 +33,7 @@ def _exec_log(execution: JobExecution, msg: str, level: int = 3):
)


def _commandline_arguments(job: Job, execution: JobExecution, path_run: Path) -> str:
def _commandline_arguments(job: Job, execution: JobExecution, creds: BaseJobCredentials) -> str:
cmd_arguments = []
if is_set(job.cmd_args):
cmd_arguments.append(job.cmd_args)
Expand All @@ -48,12 +47,6 @@ def _commandline_arguments(job: Job, execution: JobExecution, path_run: Path) ->
if execution.mode_diff or job.mode_diff:
cmd_arguments.append('--diff')

credentials = get_credentials_to_use(job=job, execution=execution)
if credentials is not None:
cmd_arguments.extend(
commandline_arguments_credentials(credentials=credentials, path_run=path_run)
)

if is_set(config['path_ssh_known_hosts']) and \
' '.join(cmd_arguments).find('ansible_ssh_extra_args') == -1:
if Path(config['path_ssh_known_hosts']).is_file():
Expand All @@ -64,6 +57,15 @@ def _commandline_arguments(job: Job, execution: JobExecution, path_run: Path) ->
else:
_exec_log(execution=execution, msg='Ignoring known_hosts file because it does not exist', level=5)

if is_set(creds.become_pass):
cmd_arguments.append('--ask-become-pass')

if is_set(creds.connect_pass):
cmd_arguments.append('--ask-pass')

if is_set(creds.vault_pass):
cmd_arguments.append('--ask-vault-pass')

return ' '.join(cmd_arguments)


Expand Down Expand Up @@ -120,15 +122,17 @@ def _execution_or_job(job: Job, execution: JobExecution, attr: str):
return None


def _runner_options(job: Job, execution: JobExecution, path_run: Path, project_dir: str) -> dict:
def _runner_options(
job: Job, execution: JobExecution, path_run: Path, project_dir: str, creds: BaseJobCredentials,
) -> dict:
verbosity = None
if execution.verbosity != 0:
verbosity = execution.verbosity

elif job.verbosity != 0:
verbosity = job.verbosity

cmdline_args = _commandline_arguments(job=job, execution=execution, path_run=path_run)
cmdline_args = _commandline_arguments(job=job, execution=execution, creds=creds)

opts = {
'project_dir': project_dir,
Expand All @@ -147,7 +151,8 @@ def _runner_options(job: Job, execution: JobExecution, path_run: Path, project_d
def runner_prep(job: Job, execution: JobExecution, path_run: Path, project_dir: str) -> dict:
update_status(execution, status='Starting')

opts = _runner_options(job=job, execution=execution, path_run=path_run, project_dir=project_dir)
creds = get_credentials_to_use(job=job, execution=execution)
opts = _runner_options(job=job, execution=execution, path_run=path_run, project_dir=project_dir, creds=creds)
opts['playbook'] = job.playbook_file
if is_set(job.inventory_file):
opts['inventory'] = job.inventory_file.split(',')
Expand All @@ -166,12 +171,11 @@ def runner_prep(job: Job, execution: JobExecution, path_run: Path, project_dir:
create_dirs(path=path_run, desc='run')
create_dirs(path=config['path_log'], desc='log')

credentials = get_credentials_to_use(job=job, execution=execution)
for secret_attr in BaseJobCredentials.SECRET_ATTRS:
write_pwd_file(credentials, attr=secret_attr, path_run=path_run)

update_status(execution, status='Running')
return opts
return {
**opts,
**get_runner_credential_args(creds=creds),
}


def runner_logs(cfg: RunnerConfig, log_files: dict):
Expand All @@ -196,8 +200,6 @@ def runner_logs(cfg: RunnerConfig, log_files: dict):
def runner_cleanup(execution: JobExecution, path_run: Path, exec_repo: ExecuteRepository):
overwrite_and_delete_file(f"{path_run}/env/passwords")
overwrite_and_delete_file(f"{path_run}/env/ssh_key")
for attr in BaseJobCredentials.SECRET_ATTRS:
overwrite_and_delete_file(get_pwd_file(path_run=path_run, attr=attr))

try:
exec_repo.cleanup_repository()
Expand Down
7 changes: 1 addition & 6 deletions src/ansibleguy-webui/aw/execute/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
from aw.utils.util import is_null, is_set, write_file_0640
from aw.utils.subps import process
from aw.execute.play_credentials import write_pwd_file, get_pwd_file
from aw.execute.util import overwrite_and_delete_file, update_status, get_path_run, job_logs, create_dirs
from aw.model.job_credential import BaseJobCredentials
from aw.execute.util import update_status, get_path_run, job_logs, create_dirs
from aw.utils.handlers import AnsibleRepositoryError
from aw.model.repository import Repository
from aw.base import USERS
Expand Down Expand Up @@ -158,10 +157,6 @@ def cleanup_repository(self):
if is_null(self.repository) or self.repository.rtype_name == 'Static':
return

path_run_repo = self.get_path_run_repo()
for attr in BaseJobCredentials.SECRET_ATTRS:
overwrite_and_delete_file(get_pwd_file(path_run=path_run_repo, attr=attr))

self._run_repo_config_cmds(cmds=self.repository.git_hook_cleanup, env=self._git_env())
if self.repository.git_isolate:
rmtree(self.get_path_repo(), ignore_errors=True)
Expand Down
19 changes: 17 additions & 2 deletions src/ansibleguy-webui/aw/utils/util.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import os
import unicodedata
import re as regex
from platform import python_version
from datetime import datetime, timedelta
from time import time
from os import open as open_file
from pathlib import Path
from functools import lru_cache, wraps
from math import ceil
import re as regex
from sys import maxunicode
import unicodedata
from threading import Thread

from pkg_resources import get_distribution
from crontab import CronTab
Expand Down Expand Up @@ -73,6 +75,19 @@ def write_file_0600(file: (str, Path), content: str):
_file.write(content)


def write_pipe_0600(file: (str, Path), content: str):
os.mkfifo(file, mode=0o600)

def pipe_writer(f: (str, Path), c: str):
# will be blocked until ansible connects to the other end of the pipe
with open(f, 'wb') as fh:
fh.write(c.encode('utf-8'))

os.remove(file)

Thread(target=pipe_writer, args=(file, content)).start()


def _open_file_0640(path: (str, Path), flags):
return open_file(path, flags, 0o640)

Expand Down
4 changes: 2 additions & 2 deletions src/ansibleguy-webui/aw/views/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from aw.model.job import JobExecution, JobExecutionResultHost
from aw.model.job_credential import JobGlobalCredentials, JobUserCredentials
from aw.api_endpoints.job_util import get_viewable_jobs
from aw.utils.util import get_next_cron_execution_str
from aw.utils.util import get_next_cron_execution_str, is_set
from aw.utils.permission import has_credentials_permission, CHOICE_PERMISSION_READ
from aw.views.forms.job import job_edit, job_clone, job_credentials_edit, job_repository_static_edit, \
job_repository_git_edit
Expand All @@ -34,7 +34,7 @@ def manage(request) -> HttpResponse:

cron = '-'
try:
if job.schedule is not None:
if is_set(job.schedule):
cron = get_next_cron_execution_str(job.schedule)

except ValueError:
Expand Down
2 changes: 1 addition & 1 deletion test/roles/test1/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

- name: TEST 1 | Sleeping
ansible.builtin.pause:
seconds: 30
seconds: 60
when: test == 'run3'

- name: TEST 1 | Fail
Expand Down

0 comments on commit d610082

Please sign in to comment.