Skip to content

Conversation

@nmoray
Copy link

@nmoray nmoray commented Jun 25, 2025

  • What I did
    Made use of Security Cipher module to encrypt the TACPLUS passkey.

  • How I did it
    Implemented the feature by following HLD

  • How to verify it
    Unit test logs:

1. Encrypt passkey:
sonic# config tacacs passkey nikhil --encrypt
sonic# show run al | jq .TACPLUS
{
  "global": {
    "auth_type": "login",
    "key_encrypt": "True",
    "passkey": "U2FsdGVkX19YTUcG+0r3+d78A5E0zTXEukS3ZNrfyFk=",
    "src_intf": "Loopback0"
  }
}
sonic# cat /etc/pam.d/common-auth-sonic
#THIS IS AN AUTO-GENERATED FILE
#
# /etc/pam.d/common-auth- authentication settings common to all services
# This file is included from other service-specific PAM config files,
# and should contain a list of the authentication modules that define
# the central authentication scheme for use on the system
# (e.g., /etc/shadow, LDAP, Kerberos, etc.). The default is to use the
# traditional Unix authentication mechanisms.
#
# here are the per-package modules (the "Primary" block)

auth	[success=done new_authtok_reqd=done default=ignore auth_err=die]	pam_tacplus.so server=<> secret=nikhil login=login timeout=5   try_first_pass
auth	[success=done new_authtok_reqd=done default=ignore auth_err=die]	pam_tacplus.so server=<> secret=nikhil login=login timeout=5   try_first_pass
auth	[success=1 default=ignore]	pam_unix.so nullok try_first_pass

#
# here's the fallback if no module succeeds
auth    requisite                       pam_deny.so
# prime the stack with a positive return value if there isn't one already;
# this avoids us returning an error just because nothing sets a success code
# since the modules above will each just jump around
auth    required                        pam_permit.so
# and here are more per-package modules (the "Additional" block)
sonic# cat /etc/cipher_pass.json
{
  "TACPLUS": {
    "table_info": [
      "TACPLUS|global"
    ],
    "password": "TEST1"
  }

2. Encrypt passkey and update existing password
sonic# config tacacs passkey nikhil --encrypt --rotate
Password:
sonic# cat /etc/cipher_pass.json
{
  "TACPLUS": {
    "table_info": [
      "TACPLUS|global"
    ],
    "password": "TEST3"
  }
sonic# cat /etc/pam.d/common-auth-sonic
#THIS IS AN AUTO-GENERATED FILE
#
# /etc/pam.d/common-auth- authentication settings common to all services
# This file is included from other service-specific PAM config files,
# and should contain a list of the authentication modules that define
# the central authentication scheme for use on the system
# (e.g., /etc/shadow, LDAP, Kerberos, etc.). The default is to use the
# traditional Unix authentication mechanisms.
#
# here are the per-package modules (the "Primary" block)

auth	[success=done new_authtok_reqd=done default=ignore auth_err=die]	pam_tacplus.so server=<> secret=nikhil login=login timeout=5   try_first_pass
auth	[success=done new_authtok_reqd=done default=ignore auth_err=die]	pam_tacplus.so server=<> secret=nikhil login=login timeout=5   try_first_pass
auth	[success=1 default=ignore]	pam_unix.so nullok try_first_pass

#
# here's the fallback if no module succeeds
auth    requisite                       pam_deny.so
# prime the stack with a positive return value if there isn't one already;
# this avoids us returning an error just because nothing sets a success code
# since the modules above will each just jump around
auth    required                        pam_permit.so
# and here are more per-package modules (the "Additional" block)
sonic# show run al | jq .TACPLUS
{
  "global": {
    "auth_type": "login",
    "key_encrypt": "True",
    "passkey": "U2FsdGVkX18/8nFrAiCe01pOqgPoUUZFaTpmtawdi8I=",
    "src_intf": "Loopback0"
  }
}

3. Revert back to plaintext passkey
sonic# config tacacs passkey ravi
sonic# cat /etc/pam.d/common-auth-sonic
#THIS IS AN AUTO-GENERATED FILE
#
# /etc/pam.d/common-auth- authentication settings common to all services
# This file is included from other service-specific PAM config files,
# and should contain a list of the authentication modules that define
# the central authentication scheme for use on the system
# (e.g., /etc/shadow, LDAP, Kerberos, etc.). The default is to use the
# traditional Unix authentication mechanisms.
#
# here are the per-package modules (the "Primary" block)

auth	[success=done new_authtok_reqd=done default=ignore auth_err=die]	pam_tacplus.so server=<> secret=ravi login=login timeout=5   try_first_pass
auth	[success=done new_authtok_reqd=done default=ignore auth_err=die]	pam_tacplus.so server=<> secret=ravi login=login timeout=5   try_first_pass
auth	[success=1 default=ignore]	pam_unix.so nullok try_first_pass

#
# here's the fallback if no module succeeds
auth    requisite                       pam_deny.so
# prime the stack with a positive return value if there isn't one already;
# this avoids us returning an error just because nothing sets a success code
# since the modules above will each just jump around
auth    required                        pam_permit.so
# and here are more per-package modules (the "Additional" block)
sonic# show run al | jq .TACPLUS
{
  "global": {
    "auth_type": "login",
    "key_encrypt": "False",
    "passkey": "ravi",
    "src_intf": "Loopback0"
  }
}
sonic# cat /etc/cipher_pass.json
{
  "TACPLUS": {
    "table_info": [],
    "password": null
  }

Note: This PR is created by closing the old PR due to several merge conflicts.

@mssonicbld
Copy link
Collaborator

/azp run

@nmoray nmoray changed the title Integrated Security Cipher for TACPLUS passkey encryption/decryption Integrated Security Cipher for TACPLUS passkey encryption Jun 25, 2025
@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@nmoray
Copy link
Author

nmoray commented Jun 25, 2025

@anders-nexthop @qiluo-msft Please review the updated integration of Security Cipher module with TACPLUS feature.

@mssonicbld
Copy link
Collaborator

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@nmoray
Copy link
Author

nmoray commented Jun 26, 2025

@anders-nexthop please help in adding the reviewers.

@linux-foundation-easycla
Copy link

linux-foundation-easycla bot commented Jun 26, 2025

CLA Signed

The committers listed above are authorized under a signed CLA.

@mssonicbld
Copy link
Collaborator

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@nmoray nmoray force-pushed the security_cipher_tacplus_encrypt branch from 12a512c to df0cbec Compare June 26, 2025 05:23
@mssonicbld
Copy link
Collaborator

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@mssonicbld
Copy link
Collaborator

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@mssonicbld
Copy link
Collaborator

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@mssonicbld
Copy link
Collaborator

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@mssonicbld
Copy link
Collaborator

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@mssonicbld
Copy link
Collaborator

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@mssonicbld
Copy link
Collaborator

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Copy link
Contributor

@anders-nexthop anders-nexthop left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs some work still, but seems much cleaner after your changes to the underlying key_encrypt system.

VALID_CHARS_MSG = "Valid chars are ASCII printable except SPACE, '#', and ','"
TACACS_PASSKEY_MAX_LEN = 65

secure_cipher = master_key_mgr(security_cipher_clbk_lookup)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is security_cipher_cblk_lookup defined?

def login(db, auth_protocol):
"""Switch login authentication [ {ldap, radius, tacacs+, local} | default ]"""
if len(auth_protocol) is 0:
if len(auth_protocol) == 0:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems unrelated?

add_table_kv('TACPLUS', 'global', 'passkey', b64_encoded)
else:
# Deregister feature with Security Cipher module
secure_cipher.deregister("TACPLUS", rotate_tacplus_key)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why would we deregister if the encryption failed? also, where is rotate_tacplus_key defined?

secure_cipher.set_feature_password("TACPLUS", passwd)
else:
# Check if password rotation is enabled
if rotate:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could be else-if with else statement above

if rotate:
passwd = getpass.getpass()
# Rotate password for TACPLUS feature and re-encrypt the secret
secure_cipher.rotate_feature_passwd("TACPLUS", passwd)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't we need to encrypt the secret here as well? maybe this shouldn't return?

"""Specify TACACS+ server global passkey <STRING>"""
if ctx.obj == 'default':
del_table_key(db, 'TACPLUS', 'global', 'passkey')
elif secret:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in secret is not defined? then what? is the idea that an empty secret disables the passkey entirely? what happens if we pass an empty secret and --encrypt?

else:
click.echo('Argument "secret" is required')
if secure_cipher.is_key_encrypt_enabled("TACPLUS", "global"):
secure_cipher.deregister("TACPLUS", "TACPLUS|global")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so if we run without --encrypt, we will remove the associated key encryption password and set key_encrypt to false, since the user is no longer wanting encryption. this seems correct.

What about the --rotate option though? if a user passes --rotate without --encrypt, what then? we should at least throw an error in that case, rather than removing the encryption.

@click.option('-m', '--use-mgmt-vrf', help="Management vrf, default is no vrf", is_flag=True)
@clicommon.pass_db
def add(db, address, timeout, key, auth_type, port, pri, use_mgmt_vrf):
def add(address, timeout, key, encrypted_key, rotate, auth_type, port, pri, use_mgmt_vrf, encrypt):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see --encrypt as an arg?

raise click.UsageError("You must provide either --key or --encrypted_key")

if encrypted_key is not None:
try:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is almost the same as the block above, lets refactor them to re-use the logic

if key is not None:
data['passkey'] = key

if key and encrypted_key:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make more sense to keep just --key and have an --encrypt argument, instead of --encrypted_key? --encrypted_key sounds like you are passing in a key that is already encrypted, which is not true; you are passing in a plaintext key that you WANT to be encrypted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants