diff --git a/files/build_templates/sonic_debian_extension.j2 b/files/build_templates/sonic_debian_extension.j2 index 39ba42b1a1dd..0c5782665aee 100644 --- a/files/build_templates/sonic_debian_extension.j2 +++ b/files/build_templates/sonic_debian_extension.j2 @@ -642,6 +642,12 @@ j2 files/build_templates/config-setup.service.j2 | sudo tee $FILESYSTEM_ROOT_USR sudo cp $IMAGE_CONFIGS/config-setup/config-setup $FILESYSTEM_ROOT/usr/bin/config-setup sudo mkdir -p $FILESYSTEM_ROOT/etc/config-setup sudo cp $IMAGE_CONFIGS/config-setup/config-setup.conf $FILESYSTEM_ROOT/etc/config-setup/config-setup.conf +sudo mkdir -p $FILESYSTEM_ROOT/etc/config-setup/config-migration-pre-hooks.d +sudo cp $IMAGE_CONFIGS/config-setup/01-pre-security-cipher $FILESYSTEM_ROOT/etc/config-setup/config-migration-pre-hooks.d/01-pre-security-cipher +sudo chmod +x $FILESYSTEM_ROOT/etc/config-setup/config-migration-pre-hooks.d/01-pre-security-cipher +sudo mkdir -p $FILESYSTEM_ROOT/etc/config-setup/config-migration-post-hooks.d +sudo cp $IMAGE_CONFIGS/config-setup/01-post-security-cipher $FILESYSTEM_ROOT/etc/config-setup/config-migration-post-hooks.d/01-post-security-cipher +sudo chmod +x $FILESYSTEM_ROOT/etc/config-setup/config-migration-post-hooks.d/01-post-security-cipher echo "config-setup.service" | sudo tee -a $GENERATED_SERVICE_FILE sudo LANG=C chroot $FILESYSTEM_ROOT systemctl enable config-setup.service diff --git a/files/image_config/config-setup/01-post-security-cipher b/files/image_config/config-setup/01-post-security-cipher new file mode 100755 index 000000000000..9f710f042c11 --- /dev/null +++ b/files/image_config/config-setup/01-post-security-cipher @@ -0,0 +1,11 @@ +#!/bin/bash + +set -e + +# Restore cipher_pass file from persistent storage +if [ -f /host/security_cipher/cipher_pass ]; then + cp /host/security_cipher/cipher_pass.json /etc/cipher_pass.json + chmod 640 /etc/cipher_pass.json + echo "Restored /host/security_cipher/cipher_pass.json to /etc/" +fi + diff --git a/files/image_config/config-setup/01-pre-security-cipher b/files/image_config/config-setup/01-pre-security-cipher new file mode 100755 index 000000000000..6ff8ad53e832 --- /dev/null +++ b/files/image_config/config-setup/01-pre-security-cipher @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e + +# Ensure old_config directory exists +mkdir -p /host/security_cipher + +# Copy cipher_pass file to persistent storage +if [ -f /etc/cipher_pass ]; then + cp /etc/cipher_pass.json /host/security_cipher/cipher_pass.json + echo "Saved /etc/cipher_pass.json to /host/security_cipher/" +fi + diff --git a/src/sonic-py-common/sonic_py_common/security_cipher.py b/src/sonic-py-common/sonic_py_common/security_cipher.py index d48d29cf7fd6..5c67843ec0d3 100644 --- a/src/sonic-py-common/sonic_py_common/security_cipher.py +++ b/src/sonic-py-common/sonic_py_common/security_cipher.py @@ -2,7 +2,7 @@ A common module for handling the encryption and decryption of the feature passkey. It also takes -care of storing the secure cipher at root +care of storing the secure cipher at root protected file system ''' @@ -12,8 +12,11 @@ import syslog import os import base64 +import json from swsscommon.swsscommon import ConfigDBConnector +CIPHER_PASS_FILE = "/etc/cipher_pass.json" + class master_key_mgr: _instance = None _lock = threading.Lock() @@ -28,69 +31,33 @@ def __new__(cls): def __init__(self): if not self._initialized: - self._file_path = "/etc/cipher_pass" + self._file_path = CIPHER_PASS_FILE self._config_db = ConfigDBConnector() self._config_db.connect() - # Note: Kept 1st index NA intentionally to map it with the cipher_pass file - # contents. The file has a comment at the 1st row / line - self._feature_list = ["NA", "TACPLUS", "RADIUS", "LDAP"] - if not os.path.exists(self._file_path): - with open(self._file_path, 'w') as file: - file.writelines("#Auto generated file for storing the encryption passwords\n") - for feature in self._feature_list[1:]: # Skip the first "NA" entry - file.write(f"{feature} : \n") - os.chmod(self._file_path, 0o640) self._initialized = True - # Write cipher_pass file - def __write_passwd_file(self, feature_type, passwd): - if feature_type == 'NA': - syslog.syslog(syslog.LOG_ERR, "__write_passwd_file: Invalid feature type: {}".format(feature_type)) - return + def _load_registry(self): + """ + Read cipher_pass.json file + """ + if not os.path.exists(CIPHER_PASS_FILE): + return {} + try: + with open(CIPHER_PASS_FILE, 'r') as f: + return json.load(f) + except Exception as e: + syslog.syslog(syslog.LOG_ERR, "_load_registry: Exception occurred: {}".format(e)) + return {} - if feature_type in self._feature_list: - try: - with open(self._file_path, 'r') as file: - lines = file.readlines() - # Update the password for given feature - lines[self._feature_list.index(feature_type)] = feature_type + ' : ' + passwd + '\n' - - os.chmod(self._file_path, 0o640) - with open(self._file_path, 'w') as file: - file.writelines(lines) - os.chmod(self._file_path, 0o640) - except FileNotFoundError: - syslog.syslog(syslog.LOG_ERR, "__write_passwd_file: File {} no found".format(self._file_path)) - except PermissionError: - syslog.syslog(syslog.LOG_ERR, "__write_passwd_file: Read permission denied: {}".format(self._file_path)) - - - # Read cipher pass file and return the feature specifc - # password - def __read_passwd_file(self, feature_type): - passwd = None - if feature_type == 'NA': - syslog.syslog(syslog.LOG_ERR, "__read_passwd_file: Invalid feature type: {}".format(feature_type)) - return passwd - - if feature_type in self._feature_list: - try: - os.chmod(self._file_path, 0o644) - with open(self._file_path, "r") as file: - lines = file.readlines() - for line in lines: - if feature_type in line: - passwd = line.split(' : ')[1] - os.chmod(self._file_path, 0o640) - except FileNotFoundError: - syslog.syslog(syslog.LOG_ERR, "__read_passwd_file: File {} no found".format(self._file_path)) - except PermissionError: - syslog.syslog(syslog.LOG_ERR, "__read_passwd_file: Read permission denied: {}".format(self._file_path)) - - return passwd - - - def encrypt_passkey(self, feature_type, secret: str, passwd: str) -> str: + def _save_registry(self, data): + """ + Write cipher_pass.json file + """ + with open(CIPHER_PASS_FILE, 'w') as f: + json.dump(data, f, indent=2) + os.chmod(self._file_path, 0o640) + + def _encrypt_passkey(self, feature_type, secret: str, passwd: str) -> str: """ Encrypts the plaintext using OpenSSL (AES-128-CBC, with salt and pbkdf2, no base64) and returns the result as a hex string. @@ -109,24 +76,16 @@ def encrypt_passkey(self, feature_type, secret: str, passwd: str) -> str: ) encrypted_bytes = result.stdout b64_encoded = base64.b64encode(encrypted_bytes).decode() - self.__write_passwd_file(feature_type, passwd) - return b64_encoded + return b64_encoded except subprocess.CalledProcessError as e: - syslog.syslog(syslog.LOG_ERR, "encrypt_passkey: {} Encryption failed with ERR: {}".format((e))) + syslog.syslog(syslog.LOG_ERR, "_encrypt_passkey: {} Encryption failed with ERR: {}".format(e)) return "" - - def decrypt_passkey(self, feature_type, b64_encoded: str) -> str: + def _decrypt_passkey(self, feature_type, b64_encoded: str, passwd: str) -> str: """ Decrypts a hex-encoded encrypted string using OpenSSL (AES-128-CBC, with salt and pbkdf2, no base64). Returns the decrypted plaintext. """ - - passwd = self.__read_passwd_file(feature_type).strip() - if passwd is None: - syslog.syslog(syslog.LOG_ERR, "decrypt_passkey: Enpty password for {} feature type".format(feature_type)) - return "" - try: encrypted_bytes = base64.b64decode(b64_encoded) @@ -146,39 +105,122 @@ def decrypt_passkey(self, feature_type, b64_encoded: str) -> str: syslog.syslog(syslog.LOG_ERR, "decrypt_passkey: Decryption failed with an ERR: {}".format(e.stderr.decode())) return "" + def register(self, feature_type, table_info): + """ + Register a table_info for a feature type. + Feature types: TACPLUS, RADIUS, LDAP etc. + """ + data = self._load_registry() + if feature_type not in data: + data[feature_type] = {"table_info": [], "password": None} + if table_info not in data[feature_type]["table_info"]: + data[feature_type]["table_info"].append(table_info) + self._save_registry(data) + syslog.syslog(syslog.LOG_INFO, "register: table_info {} attached to {} feature".format(table_info, feature_type)) + + def deregister(self, feature_type, table_info): + """ + Deregister (remove) a table_info string (like "TACPLUS|global") for a feature type. + If, after removal, there are no more table_info entries for that feature, + remove the respective password as well. + """ + data = self._load_registry() + if feature_type in data: + if table_info in data[feature_type]["table_info"]: + data[feature_type]["table_info"].remove(table_info) + if not data[feature_type]["table_info"]: + # No more table_info left; remove password as well + data[feature_type]["password"] = None + syslog.syslog(syslog.LOG_INFO, "deregister: No more table_info for feature {}. Password also removed.".format(feature_type)) + self._save_registry(data) + syslog.syslog(syslog.LOG_INFO, "deregister: table_info {} removed from feature {}".format(table_info, feature_type)) + else: + syslog.syslog(syslog.LOG_ERR, "deregister: table_info {} not found for feature {}".format(table_info, feature_type)) + else: + syslog.syslog(syslog.LOG_ERR, "deregister: No table_info registered for {}".format(feature_type)) + + def set_feature_password(self, feature_type, password): + """ + Set a new password for a feature type. + It will not update if already exist. + """ + data = self._load_registry() + if feature_type not in data: + data[feature_type] = {"table_info": [], "password": None} + if data[feature_type]["password"] is not None: + syslog.syslog(syslog.LOG_INFO, "set_feature_password: Password already set for feature {}, not updating the new password.".format(feature_type)) + syslog.syslog(syslog.LOG_INFO, "set_feature_password: Note: Make use of rotate_feature_passwd() method for updating the existing pass") + return + data[feature_type]["password"] = password + self._save_registry(data) + syslog.syslog(syslog.LOG_INFO, "set_feature_password: Password set for feature {}".format(feature_type)) - # Check if the encryption is enabled - def is_key_encrypt_enabled(self, table, entry): - key = 'key_encrypt' - data = self._config_db.get_entry(table, entry) - if data: - if key in data: - return data[key] - return False - + def rotate_feature_passwd(self, feature_type, new_password): + """ + For each registered table_info, extract encrypted passkey, decrypt, re-encrypt with new password, and update. + """ + data = self._load_registry() + if feature_type not in data: + syslog.syslog(syslog.LOG_ERR, "No table_info registered for {} Feature".format(feature_type)) + return - def del_cipher_pass(self, feature_type): + old_password = data[feature_type]["password"] + table_infos = data[feature_type].get("table_info", []) + for table_info in table_infos: + table, entry = table_info.split("|") + db_entry = self._config_db.get_entry(table, entry) + encrypted_passkey = db_entry.get("passkey") + #Rotate only if valid passkey is present and 'key_encyrpt' flag is True + if encrypted_passkey and str(db_entry.get("key_encrypt")).lower() == 'true': + # Decrypt with old password + plain_passkey = self._decrypt_passkey(feature_type, encrypted_passkey, old_password) + # Re-encrypt with new password + new_encrypted_passkey = self._encrypt_passkey(feature_type, plain_passkey, new_password) + # Update DB + db_entry["passkey"] = new_encrypted_passkey + self._config_db.set_entry(table, entry, db_entry) + syslog.syslog(syslog.LOG_INFO, "rotate_feature_passwd: Updated passkey for {}".format(table_info)) + else: + syslog.syslog(syslog.LOG_WARNING, "Either no passkey found or key_encrypt flag is not set to True for {}".format(table_info)) + + # Update stored password + data[feature_type]["password"] = new_password + self._save_registry(data) + syslog.syslog(syslog.LOG_INFO, "rotate_feature_passwd: Password for {} Feature has been updated.".format(feature_type)) + + def encrypt_passkey(self, feature_type, secret: str) -> str: """ - Removes only the password for the given feature_type while keeping the file structure intact. + Encrypts the plaintext and returns the result as a hex string. """ - try: - os.chmod(self._file_path, 0o640) - with open(self._file_path, "r") as file: - lines = file.readlines() + # Retrieve password from cipher_pass registry + data = self._load_registry() + passwd = None + if feature_type in data: + passwd = data[feature_type].get("password") + if not passwd: + raise ValueError(f"encrypt_passkey: No password set for feature {feature_type}") - updated_lines = [] - for line in lines: - if line.strip().startswith(f"{feature_type} :"): - updated_lines.append(f"{feature_type} : \n") # Remove password but keep format - else: - updated_lines.append(line) + return self._encrypt_passkey(feature_type, secret, passwd) - with open(self._file_path, 'w') as file: - file.writelines(updated_lines) - os.chmod(self._file_path, 0o640) + def decrypt_passkey(self, feature_type, b64_encoded: str) -> str: + """ + Decrypts a hex-encoded encrypted string using OpenSSL (AES-128-CBC, with salt and pbkdf2, no base64). + Returns the decrypted plaintext. + """ + # Retrieve password from cipher_pass registry + data = self._load_registry() + passwd = None + if feature_type in data: + passwd = data[feature_type].get("password") + if not passwd: + raise ValueError(f"decrypt_passkey: No password set for feature {feature_type}") - syslog.syslog(syslog.LOG_INFO, "del_cipher_pass: Password for {} has been removed".format((feature_type))) + return self._decrypt_passkey(feature_type, b64_encoded, passwd) - except Exception as e: - syslog.syslog(syslog.LOG_ERR, "del_cipher_pass: {} Exception occurred: {}".format((e))) + # Check if the encryption is enabled + def is_key_encrypt_enabled(self, table, entry): + data = self._config_db.get_entry(table, entry) + if data and 'key_encrypt' in data: + return data['key_encrypt'].lower() == 'true' + return False diff --git a/src/sonic-py-common/tests/test_security_cipher.py b/src/sonic-py-common/tests/test_security_cipher.py index 792667c7eda9..f43e22150ea5 100644 --- a/src/sonic-py-common/tests/test_security_cipher.py +++ b/src/sonic-py-common/tests/test_security_cipher.py @@ -1,12 +1,14 @@ import sys +import json +import base64 +import pytest if sys.version_info.major == 3: from unittest import mock else: import mock -import pytest -from sonic_py_common.security_cipher import master_key_mgr +from sonic_py_common.security_cipher import master_key_mgr from .mock_swsscommon import ConfigDBConnector # TODO: Remove this if/else block once we no longer support Python 2 @@ -15,58 +17,141 @@ else: BUILTINS = "__builtin__" -DEFAULT_FILE = [ - "#Auto generated file for storing the encryption passwords", - "TACPLUS : ", - "RADIUS : ", - "LDAP :" - ] - -UPDATED_FILE = [ - "#Auto generated file for storing the encryption passwords", - "TACPLUS : ", - "RADIUS : TEST2", - "LDAP :" - ] +DEFAULT_JSON = { + "TACPLUS": {"table_info": [], "password": None}, + "RADIUS": {"table_info": [], "password": None}, + "LDAP": {"table_info": [], "password": None} +} +UPDATED_JSON = { + "TACPLUS": {"table_info": [], "password": None}, + "RADIUS": {"table_info": [], "password": "TEST2"}, + "LDAP": {"table_info": [], "password": None} +} class TestSecurityCipher(object): - def test_passkey_encryption(self): + def setup_method(self): + # Reset singleton for isolation + master_key_mgr._instance = None + + def test_set_feature_password_sets_password(self): + with mock.patch("sonic_py_common.security_cipher.ConfigDBConnector", new=ConfigDBConnector), \ + mock.patch("os.chmod"), \ + mock.patch("{}.open".format(BUILTINS), mock.mock_open(read_data=json.dumps(DEFAULT_JSON))), \ + mock.patch("os.path.exists", return_value=True): + temp = master_key_mgr() + # Patch _save_registry to check written value + with mock.patch.object(temp, "_save_registry") as mock_save: + temp.set_feature_password("RADIUS", "testpw") + args = mock_save.call_args[0][0] + assert args["RADIUS"]["password"] == "testpw" + + def test_set_feature_password_does_not_overwrite_existing(self): + json_data = UPDATED_JSON.copy() with mock.patch("sonic_py_common.security_cipher.ConfigDBConnector", new=ConfigDBConnector), \ - mock.patch("os.chmod") as mock_chmod, \ - mock.patch("{}.open".format(BUILTINS),mock.mock_open()) as mock_file: + mock.patch("os.chmod"), \ + mock.patch("{}.open".format(BUILTINS), mock.mock_open(read_data=json.dumps(json_data))), \ + mock.patch("os.path.exists", return_value=True): temp = master_key_mgr() + with mock.patch.object(temp, "_save_registry") as mock_save: + temp.set_feature_password("RADIUS", "should_not_overwrite") + mock_save.assert_not_called() - # Use patch to replace the built-in 'open' function with a mock - with mock.patch("{}.open".format(BUILTINS), mock.mock_open()) as mock_file, \ - mock.patch("os.chmod") as mock_chmod: - mock_fd = mock.MagicMock() - mock_fd.readlines = mock.MagicMock(return_value=DEFAULT_FILE) - mock_file.return_value.__enter__.return_value = mock_fd - encrypt = temp.encrypt_passkey("TACPLUS", "passkey1", "TEST1") - assert encrypt != "passkey1" + def test_register_table_info(self): + with mock.patch("sonic_py_common.security_cipher.ConfigDBConnector", new=ConfigDBConnector), \ + mock.patch("os.chmod"), \ + mock.patch("{}.open".format(BUILTINS), mock.mock_open(read_data=json.dumps(DEFAULT_JSON))), \ + mock.patch("os.path.exists", return_value=True): + temp = master_key_mgr() + with mock.patch.object(temp, "_save_registry") as mock_save: + temp.register("RADIUS", "RADIUS|global") + args = mock_save.call_args[0][0] + assert "RADIUS|global" in args["RADIUS"]["table_info"] - def test_passkey_decryption(self): + def test_deregister_table_info(self): + # Use an in-memory registry that can be mutated + registry = { + "RADIUS": {"table_info": ["RADIUS|global", "RADIUS|backup"], "password": "radius_secret"} + } with mock.patch("sonic_py_common.security_cipher.ConfigDBConnector", new=ConfigDBConnector), \ - mock.patch("os.chmod") as mock_chmod, \ - mock.patch("{}.open".format(BUILTINS), mock.mock_open()) as mock_file: + mock.patch("os.chmod"), \ + mock.patch("{}.open".format(BUILTINS), mock.mock_open()), \ + mock.patch("os.path.exists", return_value=True): + temp = master_key_mgr() + # Patch _load_registry to always return current registry + temp._load_registry = mock.Mock(side_effect=lambda: registry.copy()) + # Patch _save_registry to update our in-memory registry + def save_registry(data): + registry.clear() + registry.update(json.loads(json.dumps(data))) # Deep copy + temp._save_registry = mock.Mock(side_effect=save_registry) + + temp.deregister("RADIUS", "RADIUS|global") + assert registry["RADIUS"]["table_info"] == ["RADIUS|backup"] + assert registry["RADIUS"]["password"] == "radius_secret" + + temp.deregister("RADIUS", "RADIUS|backup") + assert registry["RADIUS"]["table_info"] == [] + assert registry["RADIUS"]["password"] is None + + def test_encrypt_and_decrypt_passkey(self): + # Use a known password and mock openssl subprocess + json_data = { + "RADIUS": {"table_info": [], "password": "secretpw"} + } + with mock.patch("sonic_py_common.security_cipher.ConfigDBConnector", new=ConfigDBConnector), \ + mock.patch("os.chmod"), \ + mock.patch("{}.open".format(BUILTINS), mock.mock_open(read_data=json.dumps(json_data))), \ + mock.patch("os.path.exists", return_value=True): + temp = master_key_mgr() + # Mock subprocess for encryption + fake_cipher = b"\x01\x02\x03" + with mock.patch("subprocess.run") as mock_subproc: + mock_subproc.return_value = mock.Mock(stdout=fake_cipher) + encrypted = temp.encrypt_passkey("RADIUS", "plaintext") + assert base64.b64decode(encrypted) == fake_cipher + + # Mock subprocess for decryption + with mock.patch("subprocess.run") as mock_subproc: + mock_subproc.return_value = mock.Mock(stdout=b"plaintext") + decrypted = temp.decrypt_passkey("RADIUS", base64.b64encode(fake_cipher).decode()) + assert decrypted == "plaintext" + + def test_encrypt_raises_if_no_password(self): + with mock.patch("sonic_py_common.security_cipher.ConfigDBConnector", new=ConfigDBConnector), \ + mock.patch("os.chmod"), \ + mock.patch("{}.open".format(BUILTINS), mock.mock_open(read_data=json.dumps(DEFAULT_JSON))), \ + mock.patch("os.path.exists", return_value=True): + temp = master_key_mgr() + with pytest.raises(ValueError): + temp.encrypt_passkey("RADIUS", "plaintext") + + def test_is_key_encrypt_enabled(self): + with mock.patch("sonic_py_common.security_cipher.ConfigDBConnector", new=ConfigDBConnector): + temp = master_key_mgr() + temp._config_db.get_entry = mock.Mock() + + # Test when key_encrypt is 'True' + temp._config_db.get_entry.return_value = {"key_encrypt": "True"} + assert temp.is_key_encrypt_enabled("TACPLUS", "global") is True + + # Test when key_encrypt is 'true' + temp._config_db.get_entry.return_value = {"key_encrypt": "true"} + assert temp.is_key_encrypt_enabled("TACPLUS", "global") is True - # Use patch to replace the built-in 'open' function with a mock - with mock.patch("{}.open".format(BUILTINS), mock.mock_open()) as mock_file, \ - mock.patch("os.chmod") as mock_chmod: - mock_fd = mock.MagicMock() - mock_fd.readlines = mock.MagicMock(return_value=DEFAULT_FILE) - mock_file.return_value.__enter__.return_value = mock_fd - encrypt = temp.encrypt_passkey("RADIUS", "passkey2", "TEST2") + # Test when key_encrypt is 'False' + temp._config_db.get_entry.return_value = {"key_encrypt": "False"} + assert temp.is_key_encrypt_enabled("TACPLUS", "global") is False - # Use patch to replace the built-in 'open' function with a mock - with mock.patch("{}.open".format(BUILTINS), mock.mock_open()) as mock_file, \ - mock.patch("os.chmod") as mock_chmod: - mock_fd = mock.MagicMock() - mock_fd.readlines = mock.MagicMock(return_value=UPDATED_FILE) - mock_file.return_value.__enter__.return_value = mock_fd - decrypt = temp.decrypt_passkey("RADIUS", encrypt) - assert decrypt == "passkey2" + # Test when key_encrypt is missing + temp._config_db.get_entry.return_value = {"foo": "bar"} + assert temp.is_key_encrypt_enabled("TACPLUS", "global") is False + # Test when entry is empty + temp._config_db.get_entry.return_value = {} + assert temp.is_key_encrypt_enabled("TACPLUS", "global") is False + # Test when entry is None + temp._config_db.get_entry.return_value = None + assert temp.is_key_encrypt_enabled("TACPLUS", "global") is False diff --git a/src/sonic-yang-models/yang-models/sonic-system-tacacs.yang b/src/sonic-yang-models/yang-models/sonic-system-tacacs.yang index e8a113954ee2..43b79129d272 100644 --- a/src/sonic-yang-models/yang-models/sonic-system-tacacs.yang +++ b/src/sonic-yang-models/yang-models/sonic-system-tacacs.yang @@ -46,6 +46,12 @@ module sonic-system-tacacs { } } + typedef key_encrypt_type { + type boolean; + default false; + description "Indicates if the passkey is encrypted."; + } + container sonic-system-tacacs { container TACPLUS_SERVER { @@ -91,6 +97,10 @@ module sonic-system-tacacs { description "Authentication type"; } + leaf key_encrypt { + type key_encrypt_type; + } + leaf passkey { type string { length "1..256"; @@ -131,10 +141,9 @@ module sonic-system-tacacs { default 5; } - leaf key_encrypt { - type boolean; - description "Indicates if the passkey is encrypted."; - } + leaf key_encrypt { + type key_encrypt_type; + } leaf passkey { type string {