09-15-2022 05:36 AM
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
09-15-2022 08:09 AM
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.
09-16-2022 02:09 AM
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.
09-16-2022 08:22 AM - edited 09-16-2022 08:27 AM
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()
09-19-2022 04:49 AM
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?
09-19-2022 10:54 AM
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
09-20-2022 06:19 AM
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 ***"
09-21-2022 12:11 AM
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.
09-26-2022 12:23 PM
09-28-2022 12:23 AM - edited 09-28-2022 12:25 AM
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.
Discover and save your favorite ideas. Come back to expert answers, step-by-step guides, recent topics, and more.
New here? Get started with these tips. How to use Community New member guide