Remove TACACS+ authentication (#15547)

Remove TACACS+ authentication from AWX.

Co-authored-by: Hao Liu <44379968+TheRealHaoLiu@users.noreply.github.com>
This commit is contained in:
Djebran Lezzoum 2024-10-02 15:50:17 +02:00 committed by jessicamack
parent f22b192fb4
commit e4c11561cc
27 changed files with 31 additions and 571 deletions

View File

@ -43,8 +43,6 @@ GRAFANA ?= false
VAULT ?= false
# If set to true docker-compose will also start a hashicorp vault instance with TLS enabled
VAULT_TLS ?= false
# If set to true docker-compose will also start a tacacs+ instance
TACACS ?= false
# If set to true docker-compose will also start an OpenTelemetry Collector instance
OTEL ?= false
# If set to true docker-compose will also start a Loki instance
@ -511,7 +509,6 @@ docker-compose-sources: .git/hooks/pre-commit
-e enable_grafana=$(GRAFANA) \
-e enable_vault=$(VAULT) \
-e vault_tls=$(VAULT_TLS) \
-e enable_tacacs=$(TACACS) \
-e enable_otel=$(OTEL) \
-e enable_loki=$(LOKI) \
-e install_editable_dependencies=$(EDITABLE_DEPENDENCIES) \

View File

@ -0,0 +1,26 @@
from django.db import migrations
TACACS_PLUS_AUTH_CONF_KEYS = [
'TACACSPLUS_HOST',
'TACACSPLUS_PORT',
'TACACSPLUS_SECRET',
'TACACSPLUS_SESSION_TIMEOUT',
'TACACSPLUS_AUTH_PROTOCOL',
'TACACSPLUS_REM_ADDR',
]
def remove_tacacs_plus_auth_conf(apps, scheme_editor):
setting = apps.get_model('conf', 'Setting')
setting.objects.filter(key__in=TACACS_PLUS_AUTH_CONF_KEYS).delete()
class Migration(migrations.Migration):
dependencies = [
('conf', '0010_change_to_JSONField'),
]
operations = [
migrations.RunPython(remove_tacacs_plus_auth_conf),
]

View File

@ -98,21 +98,6 @@ def test_radius_settings(get, put, patch, delete, admin, settings):
assert settings.RADIUS_SECRET == ''
@pytest.mark.django_db
def test_tacacsplus_settings(get, put, patch, admin):
url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'tacacsplus'})
response = get(url, user=admin, expect=200)
put(url, user=admin, data=response.data, expect=200)
patch(url, user=admin, data={'TACACSPLUS_SECRET': 'mysecret'}, expect=200)
patch(url, user=admin, data={'TACACSPLUS_SECRET': ''}, expect=200)
patch(url, user=admin, data={'TACACSPLUS_HOST': 'localhost'}, expect=400)
patch(url, user=admin, data={'TACACSPLUS_SECRET': 'mysecret'}, expect=200)
patch(url, user=admin, data={'TACACSPLUS_HOST': 'localhost'}, expect=200)
patch(url, user=admin, data={'TACACSPLUS_HOST': '', 'TACACSPLUS_SECRET': ''}, expect=200)
patch(url, user=admin, data={'TACACSPLUS_HOST': 'localhost', 'TACACSPLUS_SECRET': ''}, expect=400)
patch(url, user=admin, data={'TACACSPLUS_HOST': 'localhost', 'TACACSPLUS_SECRET': 'mysecret'}, expect=200)
@pytest.mark.django_db
def test_ui_settings(get, put, patch, delete, admin):
url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ui'})

View File

@ -393,7 +393,6 @@ REST_FRAMEWORK = {
AUTHENTICATION_BACKENDS = (
'awx.sso.backends.RADIUSBackend',
'awx.sso.backends.TACACSPlusBackend',
'social_core.backends.google.GoogleOAuth2',
'social_core.backends.github.GithubOAuth2',
'social_core.backends.github.GithubOrganizationOAuth2',
@ -424,15 +423,6 @@ RADIUS_SERVER = ''
RADIUS_PORT = 1812
RADIUS_SECRET = ''
# TACACS+ settings (default host to empty string to skip using TACACS+ auth).
# Note: These settings may be overridden by database settings.
TACACSPLUS_HOST = ''
TACACSPLUS_PORT = 49
TACACSPLUS_SECRET = ''
TACACSPLUS_SESSION_TIMEOUT = 5
TACACSPLUS_AUTH_PROTOCOL = 'ascii'
TACACSPLUS_REM_ADDR = False
# Enable / Disable HTTP Basic Authentication used in the API browser
# Note: Session limits are not enforced when using HTTP Basic Authentication.
# Note: This setting may be overridden by database settings.

View File

@ -13,9 +13,6 @@ from django.http import HttpResponse
# radiusauth
from radiusauth.backends import RADIUSBackend as BaseRADIUSBackend
# tacacs+ auth
import tacacs_plus
# social
from social_core.backends.saml import OID_USERID
from social_core.backends.saml import SAMLAuth as BaseSAMLAuth
@ -69,54 +66,6 @@ class RADIUSBackend(BaseRADIUSBackend):
return _get_or_set_enterprise_user(force_str(username), force_str(password), 'radius')
class TACACSPlusBackend(object):
"""
Custom TACACS+ auth backend for AWX
"""
def authenticate(self, request, username, password):
if not django_settings.TACACSPLUS_HOST:
return None
try:
# Upstream TACACS+ client does not accept non-string, so convert if needed.
tacacs_client = tacacs_plus.TACACSClient(
django_settings.TACACSPLUS_HOST,
django_settings.TACACSPLUS_PORT,
django_settings.TACACSPLUS_SECRET,
timeout=django_settings.TACACSPLUS_SESSION_TIMEOUT,
)
auth_kwargs = {'authen_type': tacacs_plus.TAC_PLUS_AUTHEN_TYPES[django_settings.TACACSPLUS_AUTH_PROTOCOL]}
if django_settings.TACACSPLUS_AUTH_PROTOCOL:
client_ip = self._get_client_ip(request)
if client_ip:
auth_kwargs['rem_addr'] = client_ip
auth = tacacs_client.authenticate(username, password, **auth_kwargs)
except Exception as e:
logger.exception("TACACS+ Authentication Error: %s" % str(e))
return None
if auth.valid:
return _get_or_set_enterprise_user(username, password, 'tacacs+')
def get_user(self, user_id):
if not django_settings.TACACSPLUS_HOST:
return None
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
def _get_client_ip(self, request):
if not request or not hasattr(request, 'META'):
return None
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
class TowerSAMLIdentityProvider(BaseSAMLIdentityProvider):
"""
Custom Identity Provider to make attributes to what we expect.

View File

@ -186,11 +186,9 @@ def get_external_account(user):
def is_remote_auth_enabled():
from django.conf import settings
# Append Radius, TACACS+ and SAML options
settings_that_turn_on_remote_auth = [
'SOCIAL_AUTH_SAML_ENABLED_IDPS',
'RADIUS_SERVER',
'TACACSPLUS_HOST',
]
# Also include any SOCAIL_AUTH_*KEY (except SAML)
for social_auth_key in dir(settings):

View File

@ -7,11 +7,8 @@ from django.conf import settings
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
# Django REST Framework
from rest_framework import serializers
# AWX
from awx.conf import register, register_validate, fields
from awx.conf import register, fields
from awx.sso.fields import (
AuthenticationBackendsField,
SAMLContactField,
@ -25,7 +22,6 @@ from awx.sso.fields import (
SocialTeamMapField,
)
from awx.main.validators import validate_private_key, validate_certificate
from awx.sso.validators import validate_tacacsplus_disallow_nonascii # noqa
class SocialAuthCallbackURL(object):
@ -187,79 +183,6 @@ if settings.ALLOW_LOCAL_RESOURCE_MANAGEMENT:
encrypted=True,
)
###############################################################################
# TACACSPLUS AUTHENTICATION SETTINGS
###############################################################################
register(
'TACACSPLUS_HOST',
field_class=fields.CharField,
allow_blank=True,
default='',
label=_('TACACS+ Server'),
help_text=_('Hostname of TACACS+ server.'),
category=_('TACACS+'),
category_slug='tacacsplus',
)
register(
'TACACSPLUS_PORT',
field_class=fields.IntegerField,
min_value=1,
max_value=65535,
default=49,
label=_('TACACS+ Port'),
help_text=_('Port number of TACACS+ server.'),
category=_('TACACS+'),
category_slug='tacacsplus',
)
register(
'TACACSPLUS_SECRET',
field_class=fields.CharField,
allow_blank=True,
default='',
validators=[validate_tacacsplus_disallow_nonascii],
label=_('TACACS+ Secret'),
help_text=_('Shared secret for authenticating to TACACS+ server.'),
category=_('TACACS+'),
category_slug='tacacsplus',
encrypted=True,
)
register(
'TACACSPLUS_SESSION_TIMEOUT',
field_class=fields.IntegerField,
min_value=0,
default=5,
label=_('TACACS+ Auth Session Timeout'),
help_text=_('TACACS+ session timeout value in seconds, 0 disables timeout.'),
category=_('TACACS+'),
category_slug='tacacsplus',
unit=_('seconds'),
)
register(
'TACACSPLUS_AUTH_PROTOCOL',
field_class=fields.ChoiceField,
choices=['ascii', 'pap'],
default='ascii',
label=_('TACACS+ Authentication Protocol'),
help_text=_('Choose the authentication protocol used by TACACS+ client.'),
category=_('TACACS+'),
category_slug='tacacsplus',
)
register(
'TACACSPLUS_REM_ADDR',
field_class=fields.BooleanField,
default=True,
label=_('TACACS+ client address sending enabled'),
help_text=_('Enable the client address sending by TACACS+ client.'),
category=_('TACACS+'),
category_slug='tacacsplus',
)
###############################################################################
# GOOGLE OAUTH2 AUTHENTICATION SETTINGS
###############################################################################
@ -1344,21 +1267,3 @@ if settings.ALLOW_LOCAL_RESOURCE_MANAGEMENT:
category=_('Authentication'),
category_slug='authentication',
)
def tacacs_validate(serializer, attrs):
if not serializer.instance or not hasattr(serializer.instance, 'TACACSPLUS_HOST') or not hasattr(serializer.instance, 'TACACSPLUS_SECRET'):
return attrs
errors = []
host = serializer.instance.TACACSPLUS_HOST
if 'TACACSPLUS_HOST' in attrs:
host = attrs['TACACSPLUS_HOST']
secret = serializer.instance.TACACSPLUS_SECRET
if 'TACACSPLUS_SECRET' in attrs:
secret = attrs['TACACSPLUS_SECRET']
if host and not secret:
errors.append('TACACSPLUS_SECRET is required when TACACSPLUS_HOST is provided.')
if errors:
raise serializers.ValidationError(_('\n'.join(errors)))
return attrs
register_validate('tacacsplus', tacacs_validate)

View File

@ -14,7 +14,6 @@ from rest_framework.fields import empty, Field, SkipField
# AWX
from awx.conf import fields
from awx.main.validators import validate_certificate
from awx.sso.validators import validate_tacacsplus_disallow_nonascii # noqa
def get_subclasses(cls):

View File

@ -7,6 +7,7 @@ from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
# todo: this model to be removed as part of sso removal issue AAP-28380
class UserEnterpriseAuth(models.Model):
"""Enterprise Auth association model"""

View File

@ -1,34 +0,0 @@
import pytest
from django.contrib.auth.models import User
from awx.sso.backends import TACACSPlusBackend
from awx.sso.models import UserEnterpriseAuth
@pytest.fixture
def tacacsplus_backend():
return TACACSPlusBackend()
@pytest.fixture
def existing_normal_user():
try:
user = User.objects.get(username="alice")
except User.DoesNotExist:
user = User(username="alice", password="password")
user.save()
return user
@pytest.fixture
def existing_tacacsplus_user():
try:
user = User.objects.get(username="foo")
except User.DoesNotExist:
user = User(username="foo")
user.set_unusable_password()
user.save()
enterprise_auth = UserEnterpriseAuth(user=user, provider='tacacs+')
enterprise_auth.save()
return user

View File

@ -324,7 +324,7 @@ class TestCommonFunctions:
if enable_enterprise:
from awx.sso.models import UserEnterpriseAuth
enterprise_auth = UserEnterpriseAuth(user=user, provider='tacacs+')
enterprise_auth = UserEnterpriseAuth(user=user, provider='saml')
enterprise_auth.save()
assert get_external_account(user) == expected_results
@ -336,7 +336,6 @@ class TestCommonFunctions:
('JUNK_SETTING', False),
('SOCIAL_AUTH_SAML_ENABLED_IDPS', True),
('RADIUS_SERVER', True),
('TACACSPLUS_HOST', True),
# Set some SOCIAL_SOCIAL_AUTH_OIDC_KEYAUTH_*_KEY settings
('SOCIAL_AUTH_AZUREAD_OAUTH2_KEY', True),
('SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY', True),

View File

@ -1,37 +0,0 @@
# Python
import pytest
from unittest import mock
# AWX
from awx.sso.backends import _get_or_set_enterprise_user
@pytest.mark.django_db
def test_fetch_user_if_exist(existing_tacacsplus_user):
with mock.patch('awx.sso.backends.logger') as mocked_logger:
new_user = _get_or_set_enterprise_user("foo", "password", "tacacs+")
mocked_logger.debug.assert_not_called()
mocked_logger.warning.assert_not_called()
assert new_user == existing_tacacsplus_user
@pytest.mark.django_db
def test_create_user_if_not_exist(existing_tacacsplus_user):
with mock.patch('awx.sso.backends.logger') as mocked_logger:
new_user = _get_or_set_enterprise_user("bar", "password", "tacacs+")
mocked_logger.debug.assert_called_once_with(u'Created enterprise user bar via TACACS+ backend.')
assert new_user != existing_tacacsplus_user
@pytest.mark.django_db
def test_created_user_has_no_usable_password():
new_user = _get_or_set_enterprise_user("bar", "password", "tacacs+")
assert not new_user.has_usable_password()
@pytest.mark.django_db
def test_non_enterprise_user_does_not_get_pass(existing_normal_user):
with mock.patch('awx.sso.backends.logger') as mocked_logger:
new_user = _get_or_set_enterprise_user("alice", "password", "tacacs+")
mocked_logger.warning.assert_called_once_with(u'Enterprise user alice already defined in Tower.')
assert new_user is None

View File

@ -1,116 +0,0 @@
from unittest import mock
import pytest
def test_empty_host_fails_auth(tacacsplus_backend):
with mock.patch('awx.sso.backends.django_settings') as settings:
settings.TACACSPLUS_HOST = ''
ret_user = tacacsplus_backend.authenticate(None, u"user", u"pass")
assert ret_user is None
def test_client_raises_exception(tacacsplus_backend):
client = mock.MagicMock()
client.authenticate.side_effect = Exception("foo")
with mock.patch('awx.sso.backends.django_settings') as settings, mock.patch('awx.sso.backends.logger') as logger, mock.patch(
'tacacs_plus.TACACSClient', return_value=client
):
settings.TACACSPLUS_HOST = 'localhost'
settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii'
ret_user = tacacsplus_backend.authenticate(None, u"user", u"pass")
assert ret_user is None
logger.exception.assert_called_once_with("TACACS+ Authentication Error: foo")
def test_client_return_invalid_fails_auth(tacacsplus_backend):
auth = mock.MagicMock()
auth.valid = False
client = mock.MagicMock()
client.authenticate.return_value = auth
with mock.patch('awx.sso.backends.django_settings') as settings, mock.patch('tacacs_plus.TACACSClient', return_value=client):
settings.TACACSPLUS_HOST = 'localhost'
settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii'
ret_user = tacacsplus_backend.authenticate(None, u"user", u"pass")
assert ret_user is None
def test_client_return_valid_passes_auth(tacacsplus_backend):
auth = mock.MagicMock()
auth.valid = True
client = mock.MagicMock()
client.authenticate.return_value = auth
user = mock.MagicMock()
user.has_usable_password = mock.MagicMock(return_value=False)
with mock.patch('awx.sso.backends.django_settings') as settings, mock.patch('tacacs_plus.TACACSClient', return_value=client), mock.patch(
'awx.sso.backends._get_or_set_enterprise_user', return_value=user
):
settings.TACACSPLUS_HOST = 'localhost'
settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii'
ret_user = tacacsplus_backend.authenticate(None, u"user", u"pass")
assert ret_user == user
@pytest.mark.parametrize(
"client_ip_header,client_ip_header_value,expected_client_ip",
[('HTTP_X_FORWARDED_FOR', '12.34.56.78, 23.45.67.89', '12.34.56.78'), ('REMOTE_ADDR', '12.34.56.78', '12.34.56.78')],
)
def test_remote_addr_is_passed_to_client_if_available_and_setting_enabled(tacacsplus_backend, client_ip_header, client_ip_header_value, expected_client_ip):
auth = mock.MagicMock()
auth.valid = True
client = mock.MagicMock()
client.authenticate.return_value = auth
user = mock.MagicMock()
user.has_usable_password = mock.MagicMock(return_value=False)
request = mock.MagicMock()
request.META = {
client_ip_header: client_ip_header_value,
}
with mock.patch('awx.sso.backends.django_settings') as settings, mock.patch('tacacs_plus.TACACSClient', return_value=client), mock.patch(
'awx.sso.backends._get_or_set_enterprise_user', return_value=user
):
settings.TACACSPLUS_HOST = 'localhost'
settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii'
settings.TACACSPLUS_REM_ADDR = True
tacacsplus_backend.authenticate(request, u"user", u"pass")
client.authenticate.assert_called_once_with('user', 'pass', authen_type=1, rem_addr=expected_client_ip)
def test_remote_addr_is_completely_ignored_in_client_call_if_setting_is_disabled(tacacsplus_backend):
auth = mock.MagicMock()
auth.valid = True
client = mock.MagicMock()
client.authenticate.return_value = auth
user = mock.MagicMock()
user.has_usable_password = mock.MagicMock(return_value=False)
request = mock.MagicMock()
request.META = {}
with mock.patch('awx.sso.backends.django_settings') as settings, mock.patch('tacacs_plus.TACACSClient', return_value=client), mock.patch(
'awx.sso.backends._get_or_set_enterprise_user', return_value=user
):
settings.TACACSPLUS_HOST = 'localhost'
settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii'
settings.TACACSPLUS_REM_ADDR = False
tacacsplus_backend.authenticate(request, u"user", u"pass")
client.authenticate.assert_called_once_with('user', 'pass', authen_type=1)
def test_remote_addr_is_completely_ignored_in_client_call_if_unavailable_and_setting_enabled(tacacsplus_backend):
auth = mock.MagicMock()
auth.valid = True
client = mock.MagicMock()
client.authenticate.return_value = auth
user = mock.MagicMock()
user.has_usable_password = mock.MagicMock(return_value=False)
request = mock.MagicMock()
request.META = {}
with mock.patch('awx.sso.backends.django_settings') as settings, mock.patch('tacacs_plus.TACACSClient', return_value=client), mock.patch(
'awx.sso.backends._get_or_set_enterprise_user', return_value=user
):
settings.TACACSPLUS_HOST = 'localhost'
settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii'
settings.TACACSPLUS_REM_ADDR = True
tacacsplus_backend.authenticate(request, u"user", u"pass")
client.authenticate.assert_called_once_with('user', 'pass', authen_type=1)

View File

@ -2,13 +2,4 @@
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
__all__ = [
'validate_tacacsplus_disallow_nonascii',
]
def validate_tacacsplus_disallow_nonascii(value):
try:
value.encode('ascii')
except (UnicodeEncodeError, UnicodeDecodeError):
raise ValidationError(_('TACACS+ secret does not allow non-ascii characters'))
__all__ = []

View File

@ -21,7 +21,6 @@ page.register_page(
resources.settings_radius,
resources.settings_saml,
resources.settings_system,
resources.settings_tacacsplus,
resources.settings_ui,
resources.settings_user,
resources.settings_user_defaults,

View File

@ -220,7 +220,6 @@ class Resources(object):
_settings_radius = 'settings/radius/'
_settings_saml = 'settings/saml/'
_settings_system = 'settings/system/'
_settings_tacacsplus = 'settings/tacacsplus/'
_settings_ui = 'settings/ui/'
_settings_user = 'settings/user/'
_settings_user_defaults = 'settings/user-defaults/'

View File

@ -12,13 +12,10 @@ When a user wants to log into AWX, she can explicitly choose some of the support
On the other hand, the other authentication methods use the same types of login info (username and password), but authenticate using external auth systems rather than AWX's own database. If some of these methods are enabled, AWX will try authenticating using the enabled methods *before AWX's own authentication method*. The order of precedence is:
* RADIUS
* TACACS+
* SAML
AWX will try authenticating against each enabled authentication method *in the specified order*, meaning if the same username and password is valid in multiple enabled auth methods (*e.g.*, both RADIUS and TACACS+), AWX will only use the first positive match (in the above example, log a user in via RADIUS and skip TACACS+).
## Notes:
SAML users, RADIUS users and TACACS+ users are categorized as 'Enterprise' users. The following rules apply to Enterprise users:
SAML users and RADIUS users are categorized as 'Enterprise' users. The following rules apply to Enterprise users:
* Enterprise users can only be created via the first successful login attempt from remote authentication backend.
* Enterprise users cannot be created/authenticated if non-enterprise users with the same name has already been created in AWX.

View File

@ -1,51 +0,0 @@
# TACACS+
[Terminal Access Controller Access-Control System Plus (TACACS+)](https://en.wikipedia.org/wiki/TACACS) is a protocol developed by Cisco to handle remote authentication and related services for networked access control through a centralized server. In specific, TACACS+ provides authentication, authorization and accounting (AAA) services. AWX currently utilizes its authentication service.
TACACS+ is configured by settings configuration and is available under `/api/v2/settings/tacacsplus/`. Here is a typical configuration with every configurable field included:
```
{
"TACACSPLUS_HOST": "127.0.0.1",
"TACACSPLUS_PORT": 49,
"TACACSPLUS_SECRET": "secret",
"TACACSPLUS_SESSION_TIMEOUT": 5,
"TACACSPLUS_AUTH_PROTOCOL": "ascii",
"TACACSPLUS_REM_ADDR": "false"
}
```
Each field is explained below:
| Field Name | Field Value Type | Field Value Default | Description |
|------------------------------|---------------------|---------------------|--------------------------------------------------------------------|
| `TACACSPLUS_HOST` | String | '' (empty string) | Hostname of TACACS+ server. Empty string disables TACACS+ service. |
| `TACACSPLUS_PORT` | Integer | 49 | Port number of TACACS+ server. |
| `TACACSPLUS_SECRET` | String | '' (empty string) | Shared secret for authenticating to TACACS+ server. |
| `TACACSPLUS_SESSION_TIMEOUT` | Integer | 5 | TACACS+ session timeout value in seconds. |
| `TACACSPLUS_AUTH_PROTOCOL` | String with choices | 'ascii' | The authentication protocol used by TACACS+ client (choices are `ascii` and `pap`). |
| `TACACSPLUS_REM_ADDR` | Boolean | false | Enable the client address sending by TACACS+ client. |
Under the hood, AWX uses [open-source TACACS+ python client](https://github.com/ansible/tacacs_plus) to communicate with the remote TACACS+ server. During authentication, AWX passes username and password to TACACS+ client, which packs up auth information and sends it to the TACACS+ server. Based on what the server returns, AWX will invalidate login attempt if authentication fails. If authentication passes, AWX will create a user if she does not exist in database, and log the user in.
## Test Environment Setup
The suggested TACACS+ server for testing is [shrubbery TACACS+ daemon](http://www.shrubbery.net/tac_plus/). It is supposed to run on a CentOS machine. A verified candidate is CentOS 6.3 AMI in AWS EC2 Community AMIs (search for `CentOS 6.3 x86_64 HVM - Minimal with cloud-init aws-cfn-bootstrap and ec2-api-tools`). Note that it is required to keep TCP port 49 open, since it's the default port used by the TACACS+ daemon.
We provide [a playbook](https://github.com/jangsutsr/ansible-role-tacacs) to install a working TACACS+ server. Here is a typical test setup using the provided playbook:
1. In AWS EC2, spawn the CentOS 6 machine.
2. In Tower, create a test project using the stand-alone playbook inventory.
3. In Tower, create a test inventory with the only host to be the spawned CentOS machine.
4. In Tower, create and run a job template using the created project and inventory with parameters setup as below:
![Example tacacs+ setup jt parameters](../img/auth_tacacsplus_1.png?raw=true)
The playbook creates a user named 'tower' with ascii password default to 'login' and modifiable by `extra_var` `ascii_password` and pap password default to 'papme' and modifiable by `extra_var` `pap_password`. In order to configure TACACS+ server to meet custom test needs, we need to modify server-side file `/etc/tac_plus.conf` and `sudo service tac_plus restart` to restart the daemon. Details on how to modify config file can be found [here](http://manpages.ubuntu.com/manpages/xenial/man5/tac_plus.conf.5.html).
## Acceptance Criteria
* All specified in configuration fields should be shown and configurable as documented.
* A user defined by the TACACS+ server should be able to log into AWX.
* User not defined by TACACS+ server should not be able to log into AWX via TACACS+.
* A user existing in TACACS+ server but not in AWX should be created after the first successful log in.
* TACACS+ backend should stop an authentication attempt after configured timeout and should not block the authentication pipeline in any case.
* If exceptions occur on TACACS+ server side, the exception details should be logged in AWX, and AWX should not authenticate that user via TACACS+.

View File

@ -7,8 +7,6 @@ Through the AWX user interface, you can set up a simplified login through variou
- :ref:`ag_auth_azure`
- :ref:`ag_auth_github`
- :ref:`ag_auth_google_oauth2`
- :ref:`ag_auth_radius`
- :ref:`ag_auth_tacacs`
Different authentication types require you to enter different information. Be sure to include all the information as required.

View File

@ -13,8 +13,6 @@ This section describes setting up authentication for the following enterprise sy
.. contents::
:local:
Azure, RADIUS, and TACACS+ users are categorized as 'Enterprise' users. The following rules apply to Enterprise users:
- Enterprise users can only be created via the first successful login attempt from remote authentication backend.
- Enterprise users cannot be created/authenticated if non-enterprise users with the same name has already been created in AWX.
- AWX passwords of enterprise users should always be empty and cannot be set by any user if there are enterprise backend-enabled.
@ -78,37 +76,3 @@ AWX can be configured to centrally use RADIUS as a source for authentication inf
4. Enter the port and secret information in the next two fields.
5. Click **Save** when done.
.. _ag_auth_tacacs:
TACACS+ settings
-----------------
.. index::
pair: authentication; TACACS+ Authentication Settings
Terminal Access Controller Access-Control System Plus (TACACS+) is a protocol that handles remote authentication and related services for networked access control through a centralized server. In particular, TACACS+ provides authentication, authorization and accounting (AAA) services, in which you can configure AWX to use as a source for authentication.
.. note::
This feature is deprecated and will be removed in a future release.
1. Click **Settings** from the left navigation bar.
2. On the left side of the Settings window, click **TACACs+ settings** from the list of Authentication options.
3. Click **Edit** and enter information in the following fields:
- **TACACS+ Server**: Provide the hostname or IP address of the TACACS+ server with which to authenticate. If this field is left blank, TACACS+ authentication is disabled.
- **TACACS+ Port**: TACACS+ uses port 49 by default, which is already pre-populated.
- **TACACS+ Secret**: Secret key for TACACS+ authentication server.
- **TACACS+ Auth Session Timeout**: Session timeout value in seconds. The default is 5 seconds.
- **TACACS+ Authentication Protocol**: The protocol used by TACACS+ client. Options are **ascii** or **pap**.
.. image:: ../common/images/configure-awx-auth-tacacs.png
:alt: TACACS+ configuration details in AWX settings.
4. Click **Save** when done.

View File

@ -1,24 +0,0 @@
# Copyright (c) 2017 Ansible by Red Hat
# All Rights Reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the <organization> nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -60,7 +60,6 @@ sqlparse>=0.4.4 # Required by django https://github.com/ansible/awx/security/d
redis[hiredis]
requests
slack-sdk
tacacs_plus==1.0 # UPGRADE BLOCKER: auth does not work with later versions
twilio
twisted[tls]>=23.10.0 # CVE-2023-46137
uWSGI

View File

@ -495,7 +495,6 @@ six==1.16.0
# pygerduty
# pyrad
# python-dateutil
# tacacs-plus
slack-sdk==3.27.0
# via -r /awx_devel/requirements/requirements.in
smmap==5.0.1
@ -510,7 +509,6 @@ sqlparse==0.4.4
# via
# -r /awx_devel/requirements/requirements.in
# django
tacacs-plus==1.0
# via -r /awx_devel/requirements/requirements.in
tempora==5.5.1
# via

View File

@ -273,7 +273,6 @@ $ make docker-compose
- [Start with Minikube](#start-with-minikube)
- [SAML and OIDC Integration](#saml-and-oidc-integration)
- [Splunk Integration](#splunk-integration)
- [tacacs+ Integration](#tacacs+-integration)
### Start a Shell
@ -465,30 +464,6 @@ ansible-playbook tools/docker-compose/ansible/plumb_splunk.yml
Once the playbook is done running Splunk should now be setup in your development environment. You can log into the admin console (see above for username/password) and click on "Searching and Reporting" in the left hand navigation. In the search box enter `source="http:tower_logging_collections"` and click search.
### - tacacs+ Integration
tacacs+ is an networking protocol that provides external authentication which can be used with AWX. This section describes how to build a reference tacacs+ instance and plumb it with your AWX for testing purposes.
First, be sure that you have the awx.awx collection installed by running `make install_collection`.
Anytime you want to run a tacacs+ instance alongside AWX we can start docker-compose with the TACACS option to get a containerized instance with the command:
```bash
TACACS=true make docker-compose
```
Once the containers come up a new port (49) should be exposed and the tacacs+ server should be running on those ports.
Now we are ready to configure and plumb tacacs+ with AWX. To do this we have provided a playbook which will:
* Backup and configure the tacacsplus adapter in AWX. NOTE: this will back up your existing settings but the password fields can not be backed up through the API, you need a DB backup to recover this.
```bash
export CONTROLLER_USERNAME=<your username>
export CONTROLLER_PASSWORD=<your password>
ansible-playbook tools/docker-compose/ansible/plumb_tacacs.yml
```
Once the playbook is done running tacacs+ should now be setup in your development environment. This server has the accounts listed on https://hub.docker.com/r/dchidell/docker-tacacs
### HashiVault Integration
Run a HashiVault container alongside of AWX.

View File

@ -1,32 +0,0 @@
---
- name: Plumb a tacacs+ instance
hosts: localhost
connection: local
gather_facts: False
vars:
awx_host: "https://localhost:8043"
tasks:
- name: Load existing and new tacacs+ settings
ansible.builtin.set_fact:
existing_tacacs: "{{ lookup('awx.awx.controller_api', 'settings/tacacsplus', host=awx_host, verify_ssl=false) }}"
new_tacacs: "{{ lookup('template', 'tacacsplus_settings.json.j2') }}"
- name: Display existing tacacs+ configuration
ansible.builtin.debug:
msg:
- "Here is your existing tacacsplus configuration for reference:"
- "{{ existing_tacacs }}"
- ansible.builtin.pause:
prompt: "Continuing to run this will replace your existing tacacs settings (displayed above). They will all be captured. Be sure that is backed up before continuing"
- name: Write out the existing content
ansible.builtin.copy:
dest: "../_sources/existing_tacacsplus_adapter_settings.json"
content: "{{ existing_tacacs }}"
- name: Configure AWX tacacs+ adapter
awx.awx.settings:
settings: "{{ new_tacacs }}"
controller_host: "{{ awx_host }}"
validate_certs: False

View File

@ -188,14 +188,6 @@ services:
- "grafana_storage:/var/lib/grafana:rw"
depends_on:
- prometheus
{% endif %}
{% if enable_tacacs|bool %}
tacacs:
image: dchidell/docker-tacacs
container_name: tools_tacacs_1
hostname: tacacs
ports:
- "49:49"
{% endif %}
# A useful container that simply passes through log messages to the console
# helpful for testing awx/tower logging

View File

@ -1,7 +0,0 @@
{
"TACACSPLUS_HOST": "tacacs",
"TACACSPLUS_PORT": 49,
"TACACSPLUS_SECRET": "ciscotacacskey",
"TACACSPLUS_SESSION_TIMEOUT": 5,
"TACACSPLUS_AUTH_PROTOCOL": "ascii"
}