mirror of
https://github.com/ansible/awx.git
synced 2026-03-01 16:58:46 -03:30
Merge pull request #6745 from ryanpetrello/fix-6659
RFC: install a randomized RSA key for controller -> isolated rampart auth
This commit is contained in:
1
Makefile
1
Makefile
@@ -353,6 +353,7 @@ init:
|
|||||||
if [ "$(EXTRA_GROUP_QUEUES)" == "thepentagon" ]; then \
|
if [ "$(EXTRA_GROUP_QUEUES)" == "thepentagon" ]; then \
|
||||||
tower-manage register_instance --hostname=isolated; \
|
tower-manage register_instance --hostname=isolated; \
|
||||||
tower-manage register_queue --queuename='thepentagon' --hostnames=isolated --controller=tower; \
|
tower-manage register_queue --queuename='thepentagon' --hostnames=isolated --controller=tower; \
|
||||||
|
tower-manage generate_isolated_key | ssh -o "StrictHostKeyChecking no" root@isolated 'cat > /root/.ssh/authorized_keys'; \
|
||||||
elif [ "$(EXTRA_GROUP_QUEUES)" != "" ]; then \
|
elif [ "$(EXTRA_GROUP_QUEUES)" != "" ]; then \
|
||||||
tower-manage register_queue --queuename=$(EXTRA_GROUP_QUEUES) --hostnames=$(COMPOSE_HOST); \
|
tower-manage register_queue --queuename=$(EXTRA_GROUP_QUEUES) --hostnames=$(COMPOSE_HOST); \
|
||||||
fi;
|
fi;
|
||||||
|
|||||||
@@ -173,6 +173,29 @@ register(
|
|||||||
category_slug='jobs',
|
category_slug='jobs',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
register(
|
||||||
|
'AWX_ISOLATED_PRIVATE_KEY',
|
||||||
|
field_class=fields.CharField,
|
||||||
|
default='',
|
||||||
|
allow_blank=True,
|
||||||
|
encrypted=True,
|
||||||
|
label=_('The RSA private key for SSH traffic to isolated instances'),
|
||||||
|
help_text=_('The RSA private key for SSH traffic to isolated instances'), # noqa
|
||||||
|
category=_('Jobs'),
|
||||||
|
category_slug='jobs',
|
||||||
|
)
|
||||||
|
|
||||||
|
register(
|
||||||
|
'AWX_ISOLATED_PUBLIC_KEY',
|
||||||
|
field_class=fields.CharField,
|
||||||
|
default='',
|
||||||
|
allow_blank=True,
|
||||||
|
label=_('The RSA public key for SSH traffic to isolated instances'),
|
||||||
|
help_text=_('The RSA public key for SSH traffic to isolated instances'), # noqa
|
||||||
|
category=_('Jobs'),
|
||||||
|
category_slug='jobs',
|
||||||
|
)
|
||||||
|
|
||||||
register(
|
register(
|
||||||
'STDOUT_MAX_BYTES_DISPLAY',
|
'STDOUT_MAX_BYTES_DISPLAY',
|
||||||
field_class=fields.IntegerField,
|
field_class=fields.IntegerField,
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import StringIO
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import stat
|
import stat
|
||||||
|
import tempfile
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -141,7 +143,7 @@ class IsolatedManager(object):
|
|||||||
args.append('-%s' % ('v' * min(5, self.instance.verbosity)))
|
args.append('-%s' % ('v' * min(5, self.instance.verbosity)))
|
||||||
buff = StringIO.StringIO()
|
buff = StringIO.StringIO()
|
||||||
logger.debug('Starting job on isolated host with `run_isolated.yml` playbook.')
|
logger.debug('Starting job on isolated host with `run_isolated.yml` playbook.')
|
||||||
status, rc = run.run_pexpect(
|
status, rc = IsolatedManager.run_pexpect(
|
||||||
args, self.awx_playbook_path(), self.env, buff,
|
args, self.awx_playbook_path(), self.env, buff,
|
||||||
expect_passwords={
|
expect_passwords={
|
||||||
re.compile(r'Secret:\s*?$', re.M): base64.b64encode(json.dumps(secrets))
|
re.compile(r'Secret:\s*?$', re.M): base64.b64encode(json.dumps(secrets))
|
||||||
@@ -154,6 +156,22 @@ class IsolatedManager(object):
|
|||||||
self.stdout_handle.write(buff.getvalue())
|
self.stdout_handle.write(buff.getvalue())
|
||||||
return status, rc
|
return status, rc
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run_pexpect(cls, pexpect_args, *args, **kw):
|
||||||
|
isolated_ssh_path = None
|
||||||
|
try:
|
||||||
|
if getattr(settings, 'AWX_ISOLATED_PRIVATE_KEY', None):
|
||||||
|
isolated_ssh_path = tempfile.mkdtemp(prefix='ansible_tower_isolated')
|
||||||
|
os.chmod(isolated_ssh_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
|
||||||
|
isolated_key = os.path.join(isolated_ssh_path, '.isolated')
|
||||||
|
ssh_sock = os.path.join(isolated_ssh_path, '.isolated_ssh_auth.sock')
|
||||||
|
run.open_fifo_write(isolated_key, settings.AWX_ISOLATED_PRIVATE_KEY)
|
||||||
|
pexpect_args = run.wrap_args_with_ssh_agent(pexpect_args, isolated_key, ssh_sock)
|
||||||
|
return run.run_pexpect(pexpect_args, *args, **kw)
|
||||||
|
finally:
|
||||||
|
if isolated_ssh_path:
|
||||||
|
shutil.rmtree(isolated_ssh_path)
|
||||||
|
|
||||||
def build_isolated_job_data(self):
|
def build_isolated_job_data(self):
|
||||||
'''
|
'''
|
||||||
Write the playbook and metadata into a collection of files on the local
|
Write the playbook and metadata into a collection of files on the local
|
||||||
@@ -251,7 +269,7 @@ class IsolatedManager(object):
|
|||||||
|
|
||||||
buff = cStringIO.StringIO()
|
buff = cStringIO.StringIO()
|
||||||
logger.debug('Checking job on isolated host with `check_isolated.yml` playbook.')
|
logger.debug('Checking job on isolated host with `check_isolated.yml` playbook.')
|
||||||
status, rc = run.run_pexpect(
|
status, rc = IsolatedManager.run_pexpect(
|
||||||
args, self.awx_playbook_path(), self.env, buff,
|
args, self.awx_playbook_path(), self.env, buff,
|
||||||
cancelled_callback=self.cancelled_callback,
|
cancelled_callback=self.cancelled_callback,
|
||||||
idle_timeout=remaining,
|
idle_timeout=remaining,
|
||||||
@@ -302,7 +320,7 @@ class IsolatedManager(object):
|
|||||||
json.dumps(extra_vars)]
|
json.dumps(extra_vars)]
|
||||||
logger.debug('Cleaning up job on isolated host with `clean_isolated.yml` playbook.')
|
logger.debug('Cleaning up job on isolated host with `clean_isolated.yml` playbook.')
|
||||||
buff = cStringIO.StringIO()
|
buff = cStringIO.StringIO()
|
||||||
status, rc = run.run_pexpect(
|
status, rc = IsolatedManager.run_pexpect(
|
||||||
args, self.awx_playbook_path(), self.env, buff,
|
args, self.awx_playbook_path(), self.env, buff,
|
||||||
idle_timeout=60, job_timeout=60,
|
idle_timeout=60, job_timeout=60,
|
||||||
pexpect_timeout=5
|
pexpect_timeout=5
|
||||||
@@ -333,7 +351,7 @@ class IsolatedManager(object):
|
|||||||
env['ANSIBLE_STDOUT_CALLBACK'] = 'json'
|
env['ANSIBLE_STDOUT_CALLBACK'] = 'json'
|
||||||
|
|
||||||
buff = cStringIO.StringIO()
|
buff = cStringIO.StringIO()
|
||||||
status, rc = run.run_pexpect(
|
status, rc = IsolatedManager.run_pexpect(
|
||||||
args, cls.awx_playbook_path(), env, buff,
|
args, cls.awx_playbook_path(), env, buff,
|
||||||
idle_timeout=60, job_timeout=60,
|
idle_timeout=60, job_timeout=60,
|
||||||
pexpect_timeout=5
|
pexpect_timeout=5
|
||||||
|
|||||||
45
awx/main/management/commands/generate_isolated_key.py
Normal file
45
awx/main/management/commands/generate_isolated_key.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Copyright (c) 2015 Ansible, Inc.
|
||||||
|
# All Rights Reserved
|
||||||
|
import datetime
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from awx.conf.models import Setting
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""Generate and store a randomized RSA key for SSH traffic to isolated instances"""
|
||||||
|
help = 'Generates and stores a randomized RSA key for SSH traffic to isolated instances'
|
||||||
|
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
if getattr(settings, 'AWX_ISOLATED_PRIVATE_KEY', False):
|
||||||
|
print settings.AWX_ISOLATED_PUBLIC_KEY
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
key = rsa.generate_private_key(
|
||||||
|
public_exponent=65537,
|
||||||
|
key_size=4096,
|
||||||
|
backend=default_backend()
|
||||||
|
)
|
||||||
|
Setting.objects.create(
|
||||||
|
key='AWX_ISOLATED_PRIVATE_KEY',
|
||||||
|
value=key.private_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||||
|
encryption_algorithm=serialization.NoEncryption()
|
||||||
|
)
|
||||||
|
).save()
|
||||||
|
pemfile = Setting.objects.create(
|
||||||
|
key='AWX_ISOLATED_PUBLIC_KEY',
|
||||||
|
value=key.public_key().public_bytes(
|
||||||
|
encoding=serialization.Encoding.OpenSSH,
|
||||||
|
format=serialization.PublicFormat.OpenSSH
|
||||||
|
) + " generated-by-awx@%s" % datetime.datetime.utcnow().isoformat()
|
||||||
|
)
|
||||||
|
pemfile.save()
|
||||||
|
print pemfile.value
|
||||||
@@ -140,12 +140,11 @@ controller=security
|
|||||||
```
|
```
|
||||||
|
|
||||||
In the isolated rampart model, "controller" instances interact with "isolated"
|
In the isolated rampart model, "controller" instances interact with "isolated"
|
||||||
instances via a series of Ansible playbooks over SSH. As such, all isolated instances
|
instances via a series of Ansible playbooks over SSH. At installation time,
|
||||||
must be preconfigured by the installer with passwordless SSH access from any potential
|
a randomized RSA key is generated and distributed as an authorized key to all
|
||||||
controller instances. In the example above, the `isolatedA` and `isolatedB` hosts
|
"isolated" instances. The private half of the key is encrypted and stored
|
||||||
must be reachable from `towerB` and `towerC` hosts via `ssh
|
within Tower, and is used to authenticate from "controller" instances to
|
||||||
awx@<isolated-hostname>` (meaning, `authorized_keys` must be pre-distributed to
|
"isolated" instances when jobs are run.
|
||||||
the `isolatedA` and `isolatedB` hosts).
|
|
||||||
|
|
||||||
When a job is scheduled to run on an "isolated" instance:
|
When a job is scheduled to run on an "isolated" instance:
|
||||||
|
|
||||||
@@ -185,6 +184,20 @@ Recommendations for system configuration with isolated groups:
|
|||||||
variable - the behavior in this case can not be predicted.
|
variable - the behavior in this case can not be predicted.
|
||||||
- Do not put an isolated instance in more than 1 isolated group.
|
- Do not put an isolated instance in more than 1 isolated group.
|
||||||
|
|
||||||
|
Isolated Node Authentication
|
||||||
|
----------------------------
|
||||||
|
By default - at installation time - a randomized RSA key is generated and
|
||||||
|
distributed as an authorized key to all "isolated" instances. The private half
|
||||||
|
of the key is encrypted and stored within Tower, and is used to authenticate
|
||||||
|
from "controller" instances to "isolated" instances when jobs are run.
|
||||||
|
|
||||||
|
For users who wish to manage SSH authentication from controlling nodes to
|
||||||
|
isolated nodes via some system _outside_ of Tower (such as externally-managed
|
||||||
|
passwordless SSH keys), this behavior can be disabled by unsetting two Tower
|
||||||
|
API settings values:
|
||||||
|
|
||||||
|
`HTTP PATCH /api/v2/settings/jobs/ {'AWX_ISOLATED_PRIVATE_KEY': '', 'AWX_ISOLATED_PUBLIC_KEY': ''}`
|
||||||
|
|
||||||
|
|
||||||
### Provisioning and Deprovisioning Instances and Groups
|
### Provisioning and Deprovisioning Instances and Groups
|
||||||
|
|
||||||
|
|||||||
@@ -40,27 +40,6 @@ and they are structured as follows:
|
|||||||
The `controller` for the group "thepentagon" and all hosts therein is
|
The `controller` for the group "thepentagon" and all hosts therein is
|
||||||
determined by a ForeignKey within the instance group.
|
determined by a ForeignKey within the instance group.
|
||||||
|
|
||||||
## Development Testing Notes
|
|
||||||
|
|
||||||
### Test the SSH connection between containers
|
|
||||||
|
|
||||||
While the environment is running, you can test the connection like so:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec -i -t tools_tower_1 /bin/bash
|
|
||||||
```
|
|
||||||
|
|
||||||
Inside the context of that container:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ssh root@isolated
|
|
||||||
```
|
|
||||||
|
|
||||||
(note: awx user has been deprecated)
|
|
||||||
|
|
||||||
This should give a shell to the `tools_isolated_1` container, as the
|
|
||||||
`tools_tower_1` container sees it.
|
|
||||||
|
|
||||||
### Run a playbook
|
### Run a playbook
|
||||||
|
|
||||||
In order to run an isolated job, associate the instance group `thepentagon` with
|
In order to run an isolated job, associate the instance group `thepentagon` with
|
||||||
|
|||||||
Reference in New Issue
Block a user