From 9dc424cddd75719a539fa624d77a413338ddf907 Mon Sep 17 00:00:00 2001 From: nmoray Date: Wed, 25 Oct 2023 06:43:01 -0700 Subject: [PATCH 1/9] TACACSPLUS_PASSKEY_ENCRYPTION support --- config/aaa.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/config/aaa.py b/config/aaa.py index 3c76187126..3b1700eaf2 100644 --- a/config/aaa.py +++ b/config/aaa.py @@ -1,6 +1,7 @@ import click import ipaddress import re +import subprocess from swsscommon.swsscommon import ConfigDBConnector from .validated_config_db_connector import ValidatedConfigDBConnector from jsonpatch import JsonPatchConflict @@ -11,6 +12,42 @@ RADIUS_MAXSERVERS = 8 RADIUS_PASSKEY_MAX_LEN = 65 VALID_CHARS_MSG = "Valid chars are ASCII printable except SPACE, '#', and ','" +TACACS_PASSKEY_MAX_LEN = 65 +TACACS_SECRET_SALT = "2e6593364d369fba925092e0c1c51466c276faa127f20d18cc5ed8ae52bedbcd" + +def get_salt(): + file_path = "/etc/shadow" + target_username = "admin" + salt = None + + # Read the file and search for the "admin" username + try: + with open(file_path, 'r') as file: + for line in file: + if "admin:" in line: + # Format: username:$id$salt$hashed user pass + parts = line.split('$') + if len(parts) == 4: + salt = parts[2] + break + + except FileNotFoundError: + click.echo('File not found: ' % file_path) + except Exception as e: + click.echo('An error occurred: ' % str(e)) + + if salt == None: + salt = TACACS_SECRET_SALT + + return salt + +def encrypt_passkey(secret): + salt = get_salt() + print("from aaa.py ", salt) + cmd = [ 'openssl', 'enc', '-aes-128-cbc', '-A', '-a', '-salt', '-pbkdf2', '-pass', 'pass:' + salt ] + p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + outsecret, errs = p.communicate(input=secret) + return outsecret,errs def is_secret(secret): return bool(re.match('^' + '[^ #,]*' + '$', secret)) @@ -240,7 +277,20 @@ def passkey(ctx, secret): if ctx.obj == 'default': del_table_key('TACPLUS', 'global', 'passkey') elif secret: - add_table_kv('TACPLUS', 'global', 'passkey', secret) + if len(secret) > TACACS_PASSKEY_MAX_LEN: + click.echo('Maximum of %d chars can be configured' % TACACS_PASSKEY_MAX_LEN) + return + elif not is_secret(secret): + click.echo(VALID_CHARS_MSG) + return + config_db = ConfigDBConnector() + config_db.connect() + outsecret, errs = encrypt_passkey(secret) + if not errs: + add_table_kv('TACPLUS', 'global', 'passkey', outsecret) + else: + click.echo('Passkey configuration failed' % errs) + return else: click.echo('Argument "secret" is required') tacacs.add_command(passkey) @@ -278,7 +328,12 @@ def add(address, timeout, key, auth_type, port, pri, use_mgmt_vrf): if timeout is not None: data['timeout'] = str(timeout) if key is not None: - data['passkey'] = key + outsecret, errs = encrypt_passkey(key) + if not errs: + data['passkey'] = outsecret + else: + click.echo('Passkey configuration failed' % errs) + return if use_mgmt_vrf : data['vrf'] = "mgmt" try: From 256cb7be40ceafb010b4d97d755bd22e094da4ba Mon Sep 17 00:00:00 2001 From: nmoray Date: Wed, 25 Oct 2023 07:50:04 -0700 Subject: [PATCH 2/9] Removed debug prints --- config/aaa.py | 1 - 1 file changed, 1 deletion(-) diff --git a/config/aaa.py b/config/aaa.py index 3b1700eaf2..7bd269f85c 100644 --- a/config/aaa.py +++ b/config/aaa.py @@ -43,7 +43,6 @@ def get_salt(): def encrypt_passkey(secret): salt = get_salt() - print("from aaa.py ", salt) cmd = [ 'openssl', 'enc', '-aes-128-cbc', '-A', '-a', '-salt', '-pbkdf2', '-pass', 'pass:' + salt ] p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) outsecret, errs = p.communicate(input=secret) From b710e7b056aa9500bd59f83dff86405e7f0bd8c1 Mon Sep 17 00:00:00 2001 From: nmoray Date: Sun, 29 Oct 2023 21:54:17 -0700 Subject: [PATCH 3/9] Addressed comments --- config/aaa.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/aaa.py b/config/aaa.py index 7bd269f85c..0211a0ed30 100644 --- a/config/aaa.py +++ b/config/aaa.py @@ -17,14 +17,14 @@ def get_salt(): file_path = "/etc/shadow" - target_username = "admin" + target_username = "admin" + ":" salt = None # Read the file and search for the "admin" username try: with open(file_path, 'r') as file: for line in file: - if "admin:" in line: + if target_username in line: # Format: username:$id$salt$hashed user pass parts = line.split('$') if len(parts) == 4: From bccbe6ad411baf3d5bd223df380ad7339cce946e Mon Sep 17 00:00:00 2001 From: nmoray Date: Sun, 29 Oct 2023 22:04:17 -0700 Subject: [PATCH 4/9] Addressed comments --- config/aaa.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/config/aaa.py b/config/aaa.py index 0211a0ed30..e468e7e9eb 100644 --- a/config/aaa.py +++ b/config/aaa.py @@ -18,29 +18,26 @@ def get_salt(): file_path = "/etc/shadow" target_username = "admin" + ":" - salt = None + salt = TACACS_SECRET_SALT # Read the file and search for the "admin" username try: with open(file_path, 'r') as file: for line in file: if target_username in line: - # Format: username:$id$salt$hashed user pass + # Format: username:$id$salt$hashed parts = line.split('$') if len(parts) == 4: salt = parts[2] break except FileNotFoundError: - click.echo('File not found: ' % file_path) + syslog.syslog(syslog.LOG_ERR, "File not found: {}".format(file_path)) except Exception as e: - click.echo('An error occurred: ' % str(e)) - - if salt == None: - salt = TACACS_SECRET_SALT - + syslog.syslog(syslog.LOG_ERR, "output: {}".format(str(e))) return salt + def encrypt_passkey(secret): salt = get_salt() cmd = [ 'openssl', 'enc', '-aes-128-cbc', '-A', '-a', '-salt', '-pbkdf2', '-pass', 'pass:' + salt ] @@ -48,6 +45,7 @@ def encrypt_passkey(secret): outsecret, errs = p.communicate(input=secret) return outsecret,errs + def is_secret(secret): return bool(re.match('^' + '[^ #,]*' + '$', secret)) From 084e5a8232d005f033518fc1bf5f774c9613fff6 Mon Sep 17 00:00:00 2001 From: nmoray Date: Thu, 23 Nov 2023 19:54:54 -0800 Subject: [PATCH 5/9] Incorporated security_cipher class into the implementation --- config/aaa.py | 63 ++++++++++++++++----------------------------------- 1 file changed, 20 insertions(+), 43 deletions(-) diff --git a/config/aaa.py b/config/aaa.py index e468e7e9eb..746dbf333a 100644 --- a/config/aaa.py +++ b/config/aaa.py @@ -7,44 +7,15 @@ from jsonpatch import JsonPatchConflict from jsonpointer import JsonPointerException import utilities_common.cli as clicommon +from sonic_py_common.security_cipher import security_cipher ADHOC_VALIDATION = True RADIUS_MAXSERVERS = 8 RADIUS_PASSKEY_MAX_LEN = 65 VALID_CHARS_MSG = "Valid chars are ASCII printable except SPACE, '#', and ','" TACACS_PASSKEY_MAX_LEN = 65 -TACACS_SECRET_SALT = "2e6593364d369fba925092e0c1c51466c276faa127f20d18cc5ed8ae52bedbcd" - -def get_salt(): - file_path = "/etc/shadow" - target_username = "admin" + ":" - salt = TACACS_SECRET_SALT - - # Read the file and search for the "admin" username - try: - with open(file_path, 'r') as file: - for line in file: - if target_username in line: - # Format: username:$id$salt$hashed - parts = line.split('$') - if len(parts) == 4: - salt = parts[2] - break - - except FileNotFoundError: - syslog.syslog(syslog.LOG_ERR, "File not found: {}".format(file_path)) - except Exception as e: - syslog.syslog(syslog.LOG_ERR, "output: {}".format(str(e))) - return salt - - -def encrypt_passkey(secret): - salt = get_salt() - cmd = [ 'openssl', 'enc', '-aes-128-cbc', '-A', '-a', '-salt', '-pbkdf2', '-pass', 'pass:' + salt ] - p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) - outsecret, errs = p.communicate(input=secret) - return outsecret,errs +secure_cipher = security_cipher() def is_secret(secret): return bool(re.match('^' + '[^ #,]*' + '$', secret)) @@ -280,14 +251,16 @@ def passkey(ctx, secret): elif not is_secret(secret): click.echo(VALID_CHARS_MSG) return - config_db = ConfigDBConnector() - config_db.connect() - outsecret, errs = encrypt_passkey(secret) - if not errs: - add_table_kv('TACPLUS', 'global', 'passkey', outsecret) + if secure_cipher.is_key_encrypt_enabled('TACPLUS', 'global'): + passwd = getpass.getpass() + outsecret, errs = secure_cipher.encrypt_passkey('TACPLUS', secret, passwd) + if not errs: + add_table_kv('TACPLUS', 'global', 'passkey', outsecret) + else: + click.echo('Passkey configuration failed' % errs) + return else: - click.echo('Passkey configuration failed' % errs) - return + add_table_kv('TACPLUS', 'global', 'passkey', secret) else: click.echo('Argument "secret" is required') tacacs.add_command(passkey) @@ -325,12 +298,16 @@ def add(address, timeout, key, auth_type, port, pri, use_mgmt_vrf): if timeout is not None: data['timeout'] = str(timeout) if key is not None: - outsecret, errs = encrypt_passkey(key) - if not errs: - data['passkey'] = outsecret + if secure_cipher.is_key_encrypt_enabled('TACPLUS_SERVER', address): + passwd = getpass.getpass() + outsecret, errs = secure_cipher.encrypt_passkey('TACPLUS', key, passwd) + if not errs: + data['passkey'] = outsecret + else: + click.echo('Passkey configuration failed' % errs) + return else: - click.echo('Passkey configuration failed' % errs) - return + data['passkey'] = key if use_mgmt_vrf : data['vrf'] = "mgmt" try: From 98648aef70ea81d1171666109839f94f58baff5d Mon Sep 17 00:00:00 2001 From: nmoray Date: Tue, 9 Jan 2024 06:55:17 -0800 Subject: [PATCH 6/9] Added an option to set key_encrypt flag under TACPLUS table in CONFIG_DB via CLI --- config/aaa.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/config/aaa.py b/config/aaa.py index 746dbf333a..65b6c5f903 100644 --- a/config/aaa.py +++ b/config/aaa.py @@ -239,8 +239,9 @@ def authtype(ctx, type): @click.command() @click.argument('secret', metavar='', required=False) +@click.option('-e', '--encrypt', help='Enable passkey encryption feature', is_flag=True) @click.pass_context -def passkey(ctx, secret): +def passkey(ctx, secret, encrypt): """Specify TACACS+ server global passkey """ if ctx.obj == 'default': del_table_key('TACPLUS', 'global', 'passkey') @@ -251,7 +252,9 @@ def passkey(ctx, secret): elif not is_secret(secret): click.echo(VALID_CHARS_MSG) return - if secure_cipher.is_key_encrypt_enabled('TACPLUS', 'global'): + + if encrypt: + add_table_kv('TACPLUS', 'global', 'key_encrypt', True) passwd = getpass.getpass() outsecret, errs = secure_cipher.encrypt_passkey('TACPLUS', secret, passwd) if not errs: @@ -260,6 +263,7 @@ def passkey(ctx, secret): click.echo('Passkey configuration failed' % errs) return else: + add_table_kv('TACPLUS', 'global', 'key_encrypt', False) add_table_kv('TACPLUS', 'global', 'passkey', secret) else: click.echo('Argument "secret" is required') @@ -276,7 +280,8 @@ def passkey(ctx, secret): @click.option('-o', '--port', help='TCP port range is 1 to 65535, default 49', type=click.IntRange(1, 65535), default=49) @click.option('-p', '--pri', help="Priority, default 1", type=click.IntRange(1, 64), default=1) @click.option('-m', '--use-mgmt-vrf', help="Management vrf, default is no vrf", is_flag=True) -def add(address, timeout, key, auth_type, port, pri, use_mgmt_vrf): +@click.option('-e', '--encrypt', help='Enable passkey encryption feature', is_flag=True) +def add(address, timeout, key, auth_type, port, pri, use_mgmt_vrf, encrypt): """Specify a TACACS+ server""" if ADHOC_VALIDATION: if not clicommon.is_ipaddress(address): @@ -298,7 +303,8 @@ def add(address, timeout, key, auth_type, port, pri, use_mgmt_vrf): if timeout is not None: data['timeout'] = str(timeout) if key is not None: - if secure_cipher.is_key_encrypt_enabled('TACPLUS_SERVER', address): + if encrypt: + add_table_kv('TACPLUS', 'global', 'key_encrypt', True) passwd = getpass.getpass() outsecret, errs = secure_cipher.encrypt_passkey('TACPLUS', key, passwd) if not errs: @@ -307,6 +313,7 @@ def add(address, timeout, key, auth_type, port, pri, use_mgmt_vrf): click.echo('Passkey configuration failed' % errs) return else: + add_table_kv('TACPLUS', 'global', 'key_encrypt', False) data['passkey'] = key if use_mgmt_vrf : data['vrf'] = "mgmt" From f17366024244ecd3ca85ca8c1a16cb302af58215 Mon Sep 17 00:00:00 2001 From: nmoray Date: Wed, 31 Jan 2024 21:51:52 -0800 Subject: [PATCH 7/9] Added a logic to remove the cipher_pass file when encryption is disabled --- config/aaa.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/aaa.py b/config/aaa.py index 65b6c5f903..936d7d034d 100644 --- a/config/aaa.py +++ b/config/aaa.py @@ -265,6 +265,7 @@ def passkey(ctx, secret, encrypt): else: add_table_kv('TACPLUS', 'global', 'key_encrypt', False) add_table_kv('TACPLUS', 'global', 'passkey', secret) + secure_cipher.del_cipher_pass() else: click.echo('Argument "secret" is required') tacacs.add_command(passkey) @@ -314,7 +315,8 @@ def add(address, timeout, key, auth_type, port, pri, use_mgmt_vrf, encrypt): return else: add_table_kv('TACPLUS', 'global', 'key_encrypt', False) - data['passkey'] = key + data['passkey'] = key + secure_cipher.del_cipher_pass() if use_mgmt_vrf : data['vrf'] = "mgmt" try: From 485eb0ebf65bef5764124a1133b9a62c20826d11 Mon Sep 17 00:00:00 2001 From: nmoray Date: Thu, 1 Feb 2024 01:32:29 -0800 Subject: [PATCH 8/9] Added try catch block to catch the getpass() abort --- config/aaa.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/config/aaa.py b/config/aaa.py index 936d7d034d..258cd2ba92 100644 --- a/config/aaa.py +++ b/config/aaa.py @@ -253,9 +253,13 @@ def passkey(ctx, secret, encrypt): click.echo(VALID_CHARS_MSG) return - if encrypt: - add_table_kv('TACPLUS', 'global', 'key_encrypt', True) - passwd = getpass.getpass() + if encrypt: + try: + passwd = getpass.getpass() + except Exception as e: + click.echo('getpass aborted' % e) + return + add_table_kv('TACPLUS', 'global', 'key_encrypt', True) outsecret, errs = secure_cipher.encrypt_passkey('TACPLUS', secret, passwd) if not errs: add_table_kv('TACPLUS', 'global', 'passkey', outsecret) @@ -305,8 +309,12 @@ def add(address, timeout, key, auth_type, port, pri, use_mgmt_vrf, encrypt): data['timeout'] = str(timeout) if key is not None: if encrypt: + try: + passwd = getpass.getpass() + except Exception as e: + click.echo('getpass aborted' % e) + return add_table_kv('TACPLUS', 'global', 'key_encrypt', True) - passwd = getpass.getpass() outsecret, errs = secure_cipher.encrypt_passkey('TACPLUS', key, passwd) if not errs: data['passkey'] = outsecret From 1100d58729a9b7c85aa10bef2c7ca99eebdc69d0 Mon Sep 17 00:00:00 2001 From: nmoray Date: Fri, 12 Apr 2024 05:49:17 +0000 Subject: [PATCH 9/9] Updated class name --- config/aaa.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/aaa.py b/config/aaa.py index 258cd2ba92..5580ebee1a 100644 --- a/config/aaa.py +++ b/config/aaa.py @@ -7,7 +7,7 @@ from jsonpatch import JsonPatchConflict from jsonpointer import JsonPointerException import utilities_common.cli as clicommon -from sonic_py_common.security_cipher import security_cipher +from sonic_py_common.security_cipher import master_key_mgr ADHOC_VALIDATION = True RADIUS_MAXSERVERS = 8 @@ -15,7 +15,7 @@ VALID_CHARS_MSG = "Valid chars are ASCII printable except SPACE, '#', and ','" TACACS_PASSKEY_MAX_LEN = 65 -secure_cipher = security_cipher() +secure_cipher = master_key_mgr() def is_secret(secret): return bool(re.match('^' + '[^ #,]*' + '$', secret))