cancel
Showing results for 
Search instead for 
Did you mean: 
cancel
2069
Views
3
Helpful
0
Comments
Santiago Ahumada
Cisco Employee
Cisco Employee

Co-Author: @Qi Li 

Introduction

In our previous blog post, Integrate Cisco DUO with NSO SSO, we explored how to integrate DUO to enable SSO capabilities in NSO and protect the WebUI using Security Assertion Markup Language (SAML).

In this article, we will take the next step by implementing Multifactor Authentication (MFA) to secure CLI access over SSH using Package Challenges.

To accomplish this, we will expand the scope of the already existing scripts/authenticate script in our open-source nso-sso-duo-integration-package which is based on the original cisco-nso-saml2-auth to cover authentications over CLI and we will also use login_duo to authenticate and authorize users from trusted devices. Subsequently, the Package Challenge will process the response from the challenge along with the results returned by login_duo.

Our objective is to have DUO function as a second authentication factor when users log in to the CLI via SSH as shown below:
NSO-Intro-Service-HA-LSA.png

 This means your remote trusted device (in this case, your mobile phone) will double-check with you before the authentication process can be completed.

phoneduo.jpg

Code examples

The demo code uses the same repository as the previous blog post Integrate Cisco DUO with NSO SSO. However, since login_duo only supports Linux-based systems, we created a separate branch -cli_auth– to avoid affecting the original package which does not have an Operational System (OS) restriction.

In this demo code, we focus solely on demonstrating the MFA and do not include mechanisms for password validations. However, password validations can easily be added to the authenticate script if needed.

For general information on Package Challenges and Package Authentication setup, refer to the official NSO guides:

Setting up login_duo

Before starting any development or configuration in NSO, ensure that the DUO CLI Tool – login_duo – can communicate properly with the DUO server.  To do so, follow the official DUO guide below:

https://duo.com/docs/loginduo

In the provided code example, we clone the login_duo source code – unix_duo – and build it from the source. Once login_duo is successfully installed or built, you need to edit the login_duo.conf file located in /etc/duo or /etc/security and fill in the file with the relevant inputs from your protected Unix application in DUO:

 

 

 

[duo]
; Duo integration key
ikey = <integration key>
; Duo secret key
skey = <secret key>
; Duo API host
host = <DUO URL>
; `failmode = safe` In the event of errors with this configuration file or connection to the Duo service
; this mode will allow login without 2FA.
; `failmode = secure` This mode will deny access in the above cases. Misconfigurations with this setting
; enabled may result in you being locked out of your system.
failmode = safe
; Send command for Duo Push authentication
pushinfo = yes
prompts = 1
autopush = yes

 

 

 

Enabling the MFA on NSO CLI over SSH 

When the setting /ncs-config/aaa/package-authentication/package-challenge/enabled is set to true in ncs.conf, the script located in script/challenge is invoked. This script receives input parameters via stdin, including the challenge ID, response, client source IP, client source port, northbound API context, and protocol, formatted as:
"[challengeid;response;src-ip;src-port;ctx;proto;]\n".

The challengeid is generated by the External Authentication/Package Authentication mechanism when a challenge is thrown using the command: challenge $challenge-id $challenge-prompt.

For this setup, package-challenge is prioritized as the first authentication method in the challenge-order. If authentication fails, NSO will proceed with the external-challenge:

 

 

 

<package-challenge>
    <enabled>true</enabled> 
</package-challenge>
<challenge-order>package-challenge external-challenge</challenge-order>
<auth-order>package-authentication external-authentication</auth-order>

 

 

 

Since we are sharing the same package for package authentication where we enabled WebUI to use SAML for SSO, similarly, we now need to define the CLI to use package-challenge via login_duo. This distinction is handled using the ctx input parameter in the main() function of the authenticate script. For CLI requests, the script checks if the user exists locally on the host:

 

 

 

if args["ctx"] == "webui":
    ### Logic for WebUI - not relevant for this demo
elif args["ctx"] == "cli":
    try:
        getpwnam(args["user"])
    except KeyError:
        print("reject 'User does not exist'")
    challenge = f"challenge '{str_to_base64(args['user']).strip()}' '{str_to_base64('Authenticating DUO, Press ENTER to continue....').strip()}'"
    print(challenge, flush=True)
else:
    print("reject 'Permission denied'", flush=True)

 

 

 

If the user exists, the script initiates the challenge sequence and starts login_duo to request authentication and authorization from the remote trusted device. The result from login_duo is written to a temporary file containing DUO’s authentication result:

 

 

 

output = subprocess.Popen(['/bin/bash', '-c', f'/usr/sbin/login_duo -d -f {args["user"]} echo ""'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[1].decode("utf-8")
if len(output) == 0:
    print("reject 'DUO Authentication Error'", flush=True)
file1 = open(f"/tmp/{args['user']}.txt", "w+")
file1.write(str(output))
file1.close()

 

 

 

The scripts/challenge script is then called to process the response from the user. In our design, login_duo handles the response from the remote trusted device.

The script begins by parsing the input parameters from the NSO, formatted as [challengeid;response;src-ip;src-port;ctx;proto;]. This is described in the NSO guide under AAA Infrastructure – Authentication – Package Authentication:

 

 

 

def parse_args(args):
    a = dict(zip(["challengeid", "response", "srcip", "srcport", "ctx", "proto"], args.strip("[]").split(";")))
    a["challengeid"] = base64_to_str(a["challengeid"])
    a["response"] = base64_to_str(a["response"])
    return a

def base64_to_str(b):
    return base64.decodebytes(b.encode("utf-8")).decode("utf-8")

def str_to_base64(s):
    return base64.encodebytes(s.encode("utf-8")).decode("utf-8")

def main():
    l = sys.stdin.readline()
    a = parse_args(l)
    # Code continues here

 

 

 

Next, the script reads the result from login_duo in the temporary file from before:

 

 

 

file1 = open(f"/tmp/{a['challengeid']}.txt", "r")
faresult = file1.read()
file1.close()
logger.info("deleting - " + f"/tmp/{a['challengeid']}.txt")
os.remove(f"/tmp/{a['challengeid']}.txt")

 

 

 

If the authentication was successful, the result is sent to stdout along with the required additional information: username, groups, uid, gid, and homedir. NSO reads these values and proceeds accordingly. If authentication fails, a rejection reason is provided:

 

 

 

if "Successful" in faresult:
    userobj = getpwnam(a["challengeid"])
    uid = userobj.pw_uid
    gid = userobj.pw_gid
    groups = grp.getgrgid(gid).gr_name
    homedir = userobj.pw_dir
    accept = (f"accept_username {str_to_base64(a['challengeid']).strip()} "
              f"{groups} "
              f"{uid} "
              f"{gid} "
              f"{gid} "
              f"{homedir}")
    print(f"{accept}", flush=True)
else:
    print("reject 'DUO authentication failed'", flush=True)

if __name__ == "__main__":
    main()

 

 

 

For further details, please refer to the official NSO documentation and the source code repositories shared in this article.

Happy coding! 

Getting Started

Find answers to your questions by entering keywords or phrases in the Search bar above. New here? Use these resources to familiarize yourself with the NSO Developer community: