Nautobot Feature: Secrets and Secrets Groups

Nautobot 1.2 introduced the functionality of Secrets. Secrets provide the ability for Nautobot to access and use sensitive data. To make the best use of automation, jobs and other features need a reliable way to access secrets without having to be built with a strong opinion on access patterns and varying sources of truth (environment variables, Hashicorp vaults, etc.). This post will explain how Nautobot Secrets make that possible.

Using secrets is exposed through two concepts, the Secret model and Secrets Groups.

The Secret model (simply Secret as we continue) abstracts away the retrieving of a piece of sensitive data from its source of truth into a unique identifier that can be used later.

Secrets Groups allow not only associating Secrets together but hinting at their access patterns and use cases. More on that below.

For now, let’s dive into the basics of a Secret.

Basics of Secrets

Since Nautobot’s implementation of secrets is based on leveraging them instead of providing them, there is no direct way to view the secret data from the GUI or APIs (including GraphQL). The secret data can be accessed programmatically, either by plugins or core features like Git repository syncs.

As explained in our documentation:

This model does not store the secret value itself, but instead defines how Nautobot can retrieve the secret value as and when it is needed. By using this model as an abstraction of the underlying secrets storage implementation, this makes it possible for any Nautobot feature to make use of secret values without needing to know or care where or how the secret is actually stored.

To access the value of a Secret, Nautobot must first know who is ultimately providing the secret (called a Secret Provider). Nautobot provides two built-in Secret Providers: Environment Variable and Text File.

For a secret from the Environment Variable Provider, Nautobot will ask for the variable name to look for to retrieve the secret, for example CUSTOM_ENV_SECRET.

Basics of Secrets

For the Text File Provider, this would be the file path, for example /tmp/my-secret-location.txt.

Basics of Secrets

Since you should not be passing secrets as arguments, any runner or container that would need to access secrets will also need to have access to the same secrets back ends as well. For example, secrets provided by environment variables will need to be set with the same name everywhere; or in the case of file provided secrets, files will need to be in the same path on all runners or containers.

Large installations can quickly see a bloat in the number of Secrets they would need to register, for example if you have unique credentials per device type or site. Thankfully these fields support Jinja templating. The object can be provided as obj variable for context. So if device passwords are accessible via the file system, stored as the site name, a Secret can be created with the file path being: /path/to/secrets/devices/{{ obj.site.slug }}.txt.

Use and access of secrets also can be scoped with permissions. More on that over in our documentation.

Secrets was built with extensibility in mind. A core abstract class called SecretsProvider is available for any plugin to integrate any external provider.

While any plugin can publish its own SecretsProvider subclass, an open-source plugin has already been created called nautobot-secrets-providers that already provides Hashicorp Vault and AWS Secrets Manager back-end integrations.

Now that we know how to tell Nautobot how to retrieve secrets, let’s get to using them.

Basics of Secrets Groups

By design, Secrets are generally not accessed directly. This would require rigid naming conventions and may lead to collisions. Instead, Secrets Groups provide a way to link a Secret to how it should be used (as a username, a token, a password, etc.) as well as the method of use (Console, REST, etc.).

This provides several neat features with this level of abstraction:

  • One relationship between a secret group and a model can provide specification as to how a device can be accessed
  • One secret group can provide sets of credentials for multiple access methods (username and password for console access, token for REST access)
  • No single back end needs to have all secrets for a group (passwords could be provided by Hashicorp Vault, tokens could be provided by environment variables)

In providing these features, adding Secrets to Secrets Groups becomes a unique tuple of access method and secret type linking to a Secret.

I want to access OBJECT (the item the secret group is associated with) over ACCESS_METHOD (REST, Console, etc.), what is the value of SECRET_TYPE (username, password, etc.)?

Basics of Secrets Groups

Currently definable access methods are:

  • Generic
  • gNMI
  • HTTP(S)
  • NETCONF
  • REST
  • RESTCONF
  • SNMP
  • SSH

The constants for which are available from SecretsGroupAccessTypeChoices in nautobot.extras.choices.

Currently definable secret types are:

  • Key
  • Password
  • Secret
  • Token
  • Username

The constants for which are available from SecretsGroupSecretTypeChoices in nautobot.extras.choices.

Accessing Secrets

As stated earlier, secrets are meant to be accessed programmatically by core functions like Git, Jobs, or plugins. In the help documentation it should be defined how the module expects the group to be set up to retrieve the secrets.

For example from the Git Repository configuration, clicking the “?” Help modal button displays this:

Accessing Secrets

(also available on the Nautobot Read the Docs site)

This means when linking a Secrets Group to a Git repository, the group should have at least an association to a Secret with the Access Type being “HTTP(S)” and Secret Type being “Token”, and potentially another association for a Secret Type being “Username” depending on the use case.

Accessing Secrets

Another great example is the Nornir plugin for Nautobot. From the docs:

The default assumes Secrets Group contain secrets with “Access Type” of Generic and expects these secrets to have “Secret Type” of usernamepassword, and optionally secret. The “Access Type” is configurable via the plugin configuration parameter use_config_context, which if enabled changes the plugin functionality to pull the key nautobot_plugin_nornir.secret_access_type from each device’s config_context.

What this means is the plugin can either leverage a simple Secrets Group setup of a Generic Access Type and the relevant Secret Types, or it can dynamically query the Access Type based on information provided by the device itself.

How is this possible? Let’s briefly dive into some of the accessor methods of a Secrets Group.

A Secrets Group can be found by knowing the slug directly:

<span role="button" tabindex="0" data-code="my_device_secrets_group = SecretsGroup.objects.get(slug="device-secrets-groups") #
my_device_secrets_group = SecretsGroup.objects.get(slug="device-secrets-groups")
# <SecretsGroup: Device Secrets Groups>

Or, better yet, by grabbing the associated Secrets Group from the object itself, in this case a Device:

<span role="button" tabindex="0" data-code="my_device = Device.objects.all()[0] # <Device: rtr1.site-b> my_device_secrets_group = my_device.secrets_group #
my_device = Device.objects.all()[0]
# <Device: rtr1.site-b>

my_device_secrets_group = my_device.secrets_group
# <SecretsGroup: Device Secrets Groups>

Now that we have the Secrets Group to access the associated Secret, we use the get_secret_value method, passing in the necessary tuple as keyword arguments for Access Type and Secret Type:

from nautobot.extras.choices import SecretsGroupAccessTypeChoices, SecretsGroupSecretTypeChoices

my_device_secrets_group.get_secret_value(
  access_type=SecretsGroupAccessTypeChoices.TYPE_GENERIC,
  secret_type=SecretsGroupSecretTypeChoices.TYPE_PASSWORD
)

# 'THESECRETVALUE'

Note: While this call may work for non-templated Secrets, you will experience issues once a Secret expects the object to be passed in as obj. Therefor, you should always pass in a contextually relevant object. In this case, that’s the device we assigned to my_device.

from nautobot.extras.choices import SecretsGroupAccessTypeChoices, SecretsGroupSecretTypeChoices

my_device_secrets_group.get_secret_value(
  access_type=SecretsGroupAccessTypeChoices.TYPE_GENERIC,
  secret_type=SecretsGroupSecretTypeChoices.TYPE_PASSWORD,
  obj=my_device
)

# 'THE-rtr1-SECRETVALUE'

When building a plugin, if you support multiple authentication methods or want to permit falling back to different access types, that functionality must be explicitly built in the plugin. Secrets Groups provides no opinion on fallbacks or a “superset/subset” understanding. If you are looking to implement a form of fallback, Secrets Groups will throw a DoesNotExist exception from nautobot.extras.models.secrets.SecretsGroupAssociation, which a plugin can catch to try a different query.


Conclusion

I hope by now you can see the amazing power and flexibility that the Secret and Secrets Group provides for streamlining workflows and automations.

Any questions? Feel free to reach out via Disqus below, GitHub, or the #nautobot Slack Channel on the Network to Code Slack.

-Bryan Culver



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

Author