nautobot / nautobot-app-golden-config

Golden Configuration App for Nautobot.
https://docs.nautobot.com/projects/golden-config/en/latest/
Other
98 stars 56 forks source link

Get_secrets Jinja Filter #234

Closed chadell closed 1 year ago

chadell commented 2 years ago

Proposed Functionality

Related to #221 , this issue is specially focused on creating a Jinja filter that could render secrets from Nautobot, either for Secrets Groups FK from an object (for instance Device), but also for custom Relationship to a SecretsGroup. Obviously, taking into account the permissions of the user rendering the secret to have Read access to the SecretGroup.

Use Case

This will allow to render configurations with secrets, which are not available via Graphql, directly in Jinja templates.

chadell commented 2 years ago

@mundruid is currently working on it

mundruid commented 2 years ago

We have a proposed implementation that has gotten extensive testing for the past 1.5 months by client.

Proposal

get_secret jinja Nautobot filter that will be added in helper.py

Implementation Details:

The signature of the function is shown below:

@library.filter()
@register.filter()
def get_secret(
    user: User,
    obj_id: str,
    obj_type: str,
    secret_type: str,
    secret_access_type: Optional[str] = SecretsGroupAccessTypeChoices.TYPE_GENERIC,
) -> Optional[str]:
    """Gets the secrets attached to an object based on an ORM relationship.

    We assume that there is only one secret group corresponding to a model.

    Args:
        user (User): User object that performs API call to render push template with secrets.
        obj_id (str): Primary key returned for the specific object in the template. The id needs to be part of the GraphQL query.
        obj_type (str): Type of the object in the form of <module_name>.<model_name>, for example: circuits.Circuit
        secret_type (str): Type of secret, such as "username", "password", "token", "secret", or "key".
        secret_access_type (Optional[str], optional): Type of secret such as "Generic", "gNMI", "HTTP(S)". Defaults to "Generic".

    .. rubric:: Example Jinja get_secret filter usage
    .. highlight:: jinja
    .. code-block:: jinja

    password {{ id | get_secret("dcim.Device", "password") | encrypt_type5 }}
    ppp pap sent-username {{ interface["connected_circuit_termination"]["circuit"]["id"] | get_secret("circuits.Circuit", "username") }} password {{ interface["connected_circuit_termination"]["circuit"]["id"] | get_secret("circuits.Circuit", "password") | encrypt_type7 }}

    Returns:
        Optional[str] : Secret value. None if there is no match. An error string if there is an error.
    """

Rationale

  1. The get_secret filter should extract a secret based on the Nautobot object that has a relationship with the specific secrets group. Therefore, the user needs to provide the type of object using the arguments object_id and object_type in the function signature above. The object id will be returned by a Nautobot GraphQL query. The object_type has to be a valid Nautobot class, ex. dcim.Device
  2. The get_secret should specify the type of secret, ex. password. This left is up to the user that is using the filter.
  3. The get_secret does not need to provide an encrypted secret. That is the responsibility of the user to place the proper encryption algorithm or a hashed secret in their vault.
  4. The get_secret needs to provide authentication capability so that the secrets are rendered in a need to know basis only by authorized users. Note from the usage examples above, that the user argument is not provided when the user is using the get_secret filter in their template to avoid fake authorization. A wrapper of the get_secret is defined in the config_intended.py nornir play where the user argument is injected based on who is running the job that is rendering the configuration:
    def user_gets_secret(*args, **kwargs):
            return get_secret(request.user, *args, **kwargs)
  5. The secrets are not stored in git repos, they are only rendered through an additional url that can be easily find by an authorized user in the device panel: image
  6. Another option has been added to get a configuration with secrets in an API view. Again, these secrets are rendered "on the fly" and not stored anywhere in the DB.

This way the get_secret filter and additional API endpoint and view ensure security and utility.

@itdependsnetworks @jeffkala @chadell I look forward to your comments on this implementation.

chadell commented 2 years ago

@mundruid to explain step 5 properly, you should compare what is Intended, within Golden Config, vs Candidate, and why the second is the one that should use the get_secrets filter. I would also use an example of its usage, in the Jinja template. It would help to understand why we needed this filter. Also, to give context, the idea behind it is so give Golden Config with config provisioning/remediation capabilities (this links to my first point).

itdependsnetworks commented 2 years ago

Does it make sense to have this filter in Nautobot Core?

For point 6, I am not sure I understand.

chadell commented 2 years ago

Regarding Nautobot Core point. We had a sync with @glennmatthews , and evaluating the potential risk when using the filter to leak secrets, and its usage (the only identified use-case so far it's this plugin), we concluded to implement it here until we find out another use-case (another plugin) that could benefit from it, and then consider importing to Core.

mundruid commented 2 years ago

@itdependsnetworks, I think @chadell covered the point about Nautobot core: the filter seems GC specific for now so we suggest to keep int in the plugin and if we find another application then put into core.

Also, there is a set of two different configurations that I am referencing that Christian has already mentioned:

Regarding the question: "What are you comparing when you have this split capability." See my point above. For compliance, we compare backup config with intended config.

Regarding the questions: "Rendering on the fly when?

This seems like it would be a blocking api request which would mean it should be async, but if not stored, how would that work?"
There is no blocking, we are rendering when the user requests it, i.e., running the task `generate_config` with the filter rendering the secrets.

Regarding the question: "What is the goal? " The goal is to render a candidate configuration with secrets that can be applied to a router using an orchestrator or a config provisioning job in GC. The second goal is to provide an API endpoint to retrieve this candidate configuration with a simple call.

I hope this clarifies my points.

itdependsnetworks commented 2 years ago

For within core or not, thanks for the clarification.

Regarding the question: "What are you comparing when you have this split capability." See my point above. For compliance, we compare backup config with intended config.

I read this as backup will have secrets removed and intended will not have secrets, makes sense, thanks for clarification.

There is no blocking, we are rendering when the user requests it, i.e., running the task generate_config with the filter rendering the secrets.

The first part of this sentence "There is no blocking" seems to conflict with "running the task generate_config". I do not understand how a task would not be a job and/or blocking?

Regarding the question: "What is the goal? " The goal is to render a candidate configuration with secrets that can be applied to a router using an orchestrator or a config provisioning job in GC. The second goal is to provide an API endpoint to retrieve this candidate configuration with a simple call.

ok, kinda get it. I think we need to speak to figure out between #255 #256 and this issue how they all live together. I am inclined to have a "config_prepare" or similar function, that allows you to modify the config before pushing. That would be there for remediation, secrets replacement (as you are describing), filtering configs, etc..

itdependsnetworks commented 1 year ago

Completed in #339