cancel
Showing results for 
Search instead for 
Did you mean: 
cancel
1375
Views
30
Helpful
9
Replies

Storing and deploying BGP authentication keys with NSO

Ole Hansen
Level 1
Level 1

Hi,

I'm working on an NSO service, that configures BGP peerings on Juniper devices.

I'm looking for a method to store authentication-key in an encrypted manner in my NSO service and deploying that to the device(s) via NETCONF.

I've tried storing authentication-key as "md5-digest-string" from tailf-common, but Junos takes the key as plaintext and encrypts it - not really what I wanted.

Any pointers on where to start?

TIA

9 Replies 9

snovello
Cisco Employee
Cisco Employee

Hi,

the link here https://github.com/NSO-developer/ipython-superuser#decrypting-passwords show how you decrypt an encrypted leaf (in python). You must explicitly decrypt before passing it to the device in a template for example.

This works irrespective of the encrypted type by looking at the $N$ prefix on the string which tell NSO what was used to encrypt. See tailf-common.yang for the  hash functions and encrypted types that are supported.

Ole Hansen
Level 1
Level 1

Perfect. Thanks for sending me in that direction. The solution ended up looking like this.

In my YANG-file

     leaf authentication-key {
        type tailf:aes-cfb-128-encrypted-string;
        // type string;
        mandatory true;
      }

and in my Python code I extract and decrypt the auth key

service_dict = ncs.maagic.as_pyval(service)
with ncs.maapi.Maapi() as maapi:
maapi.install_crypto_keys()
if service_dict["authentication_key"]:
authentication_key = ncs._ncs.decrypt(service_dict["authentication_key"])
else:
authentication_key = None
template_vars = ncs.template.Variables()
template_vars.add("AUTHENTICATION_KEY", authentication_key)

And, in my XML template I reference that variable. And voila.

Only thing that bothers me a bit, is that a "commit dry-run" will actually show the clear text password in the diff and the password shows up in clear text in CDB - but in the service definition it's encrypted. It's encrypted in CDB when a sync-from is done.

  

This is a very good use case for testing whether your create callback is running as part of dry run or not. Normally of course you want to be sure your code is behaving identically in both cases but this is exactly the kind of situation where you want to know the difference. The code below lets you test and you can just have a hard coded string appearing in the dry run output.

from ncs.maapi import CommitParams
import ncs.maagic as maagic
is_dry_run = CommitParams(maagic.get_trans(root).get_trans_params()).is_dry_run()

 

Ole Hansen
Level 1
Level 1

Great. That works. Now I'll just print a message instead of the authentication-key when doing a dry run.

 

It does, however, not solve my issue where the clear text password is stored in CDB until a sync-from is done.

The documentation for the Juniper NED states that:

The standard way to handle this (in netconf in general), is to do a sync-from
after setting clear-text values, this will get NSO back in the state where the
stored value in CDB will be the same as the 'hashed' device-value.

 

I simply cannot figure out how to do a partial-sync-from or even a full sync-from automatically when the modification is comitted. I understand, that I can't do it within the cb_create (nor the pre_/post_modification transactions, because I'll create a deadlock (see https://community.cisco.com/t5/nso-developer-hub-discussions/call-partial-sync-from-function-in-cb-create/m-p/3895272/highlight/true#M4055 )

 

Any pointers?

You can update after the end of the transaction using a kicker. See docs here https://developer.cisco.com/docs/nso/guides/#!kicker

You would set the kicker to react to any changes in the authentication-keys or passwords and it would trigger an action that does the sync-from. I think it might be  possible to have an expression that tests whether the key is unencrypted to make the triggering more accurate.

I think you  might have to write your own action that uses the input to figure out what device to sync. That could call a partial sync. A kicker per device might also work if you want to use the existing sync-from action without parameters

Ole Hansen
Level 1
Level 1

Ah. That got down the rabbit hole of trying to pass arguments to partial-sync-from, but this is not supported as per https://community.cisco.com/t5/nso-developer-hub-discussions/how-to-kick-and-action-with-action-parameters/td-p/4110045

 

So. My solution is. Add a "sync-authentication-key" under my service

tailf:action "sync-authentication-key" {
tailf:actionpoint sync-authentication-key;
input {
uses kicker:action-input-params;
}
output {
}
}

 

 

Then add a kicker for the specific instance of the service in my xml-template

 

<kickers xmlns="http://tail-f.com/ns/kicker">
  <data-kicker>
  <id>foo-{/name}-sync-authentication-key</id>
  <monitor>/devices/device[name='{/device}']/config/junos:configuration/routing-instances/instance[name='{/vrf}']/protocols/bgp/group[name='{$GROUP}']/authentication-key</monitor>
  <kick-node>/services/{$SERVICE_NAME}[name='{/name}']</kick-node>
  <action-name>sync-authentication-key</action-name>
  </data-kicker>
</kickers>

 

And then my action-code

 

class SyncAuthenticationKey(Action):
    @Action.action
    def cb_action(self, uinfo, name, kp, input, output, trans):
        self.log.info(f"Action SyncAuthenticationKey({kp=}")
        with ncs.maapi.Maapi() as m:
            m.start_user_session(uinfo.username, "Action_Context")
            with m.attach(uinfo.actx_thandle) as t:
                root = ncs.maagic.get_root(m)
                service = ncs.maagic.get_node(t, kp)

                partial_sync_from = root.ncs__devices.partial_sync_from
                input = partial_sync_from.get_input()

                service_name = service.name.replace("-", "_")

                input_path = f"/devices/device/[name='{service.device}']"
                input_path += f"/config/configuration/routing-instances"
                input_path += f"/instance[name='{service.vrf}']/protocols/bgp"
                input_path += f"/group[name='{service_name}']/authentication-key"

                self.log.debug(f"{input_path=}")

                input.path = [input_path]

                result = partial_sync_from(input)

                for sync_result in result.sync_result:
                    if not sync_result.result:
                        self.log.error(f"Partial sync for {service.device} failed")
                    else:
                        self.log.info(f"Partial sync for {service.device} successful")

 

And to answer your suggestion on checking for dry run, i ended up with

 

        dry_run = CommitParams(maagic.get_trans(root).get_trans_params()).is_dry_run()
        service_dict = ncs.maagic.as_pyval(service)

        if not dry_run:
            with ncs.maapi.Maapi() as maapi:
                # to be able to decrypt credentials, we need to install crypto keys
                maapi.install_crypto_keys()
                if service_dict["authentication_key"]:
                    self.log.debug(
                        f"Going to decrypt {service_dict['authentication_key']}"
                    )
                    authentication_key = ncs._ncs.decrypt(
                        service_dict["authentication_key"]
                    )
                else:
                    authentication_key = None
        else:
            authentication_key = "*** Authentication key is hashed - refusing to print the key in clear text ***"

 

Ole Hansen
Level 1
Level 1

I'll just continue my "blabbering"

This method has one major drawback. The kicker won't be activated when creating a new service, because it happens within one transaction, so the kicker is only active on subsequent edits to the service. Instead of individual triggers, I may just have *one* trigger, that does the sync if any authentication-key is changed.

Ole Hansen
Level 1
Level 1
I simply cannot figure out how where to call the "partial-sync-from". If I put it in post_modification it times out - probably because the transaction is not closed, hence I'm creating a deadlock.

I've tried making a "generic" kicker, that monitors the authentication-key part of the configuration - but I cannot figure out how to write a path, that includes wildcards.
https://developer.cisco.com/docs/nso/guides/#!developing-nso-services/partial-sync mentions "Hence it is a good practice for such service to implement a wrapper action that invokes the generic /devices/partial-sync-from action with the correct list of paths" - but nothing about where to place it.
Any pointers are more than welcome. I've been banging my head against this for a couple of days now

Hey.

You mentioned kicker problems (not firing because kicker and change it should fire based on are part of the same transaction). Doing partial sync in service code itself might not be ideal either. To me it seems like you need a subscriber code to address these two. Difference between kicker and subscriber in function is that subscriber runs constantly (from packages reload that starts it up) and can differentiate between operations made on nodes.

Not directly answering your last problem but in subscriber, depending on how you monitor, you can catch paths that are being monitored and are processed by the NSO. Out of that you can perhaps make a list of which devices you want to partial sync from and pass it to your action as arguments.