diff --git a/README.md b/README.md index ac3ef7a30..a8d0861df 100644 --- a/README.md +++ b/README.md @@ -297,17 +297,26 @@ export NDFC_SW_USERNAME=admin export NDFC_SW_PASSWORD=Admin_123 ``` +#### Handling Secrets + +This collection supports two options to securely handle secrets in the NaC VXLAN data model such as passwords and authentication keys: + +* **🔐 Ansible Vault**: Encrypts credentials +* **✅ Environment Variables**: Resolves secrets from environment variables + +**📖 Guide**: [Handling Secrets Guide](docs/Handling_Secrets_Guide.md) + ##### Switch Credential Management -This collection supports flexible credential management for network switches with three security levels: +For switch credentials specifically, this collection supports flexible credential management with three security levels: * **🔐 Ansible Vault**: Encrypted credentials for production deployments * **✅ Environment Variables**: Secure credential injection for CI/CD pipelines * **⚠️ Plain Text**: Simple credentials for lab testing only -The system supports both switch-specific credentials and group-level defaults with automatic fallback. Environment variable lookups can be configured in group_vars for enhanced security and automation compatibility. +NaC VXLAN supports both switch-specific credentials and group-level defaults with automatic fallback. Environment variable lookups can be configured in group_vars for enhanced security and automation compatibility. -**📖 Complete Guide**: [Switch Credentials Configuration](docs/SWITCH_CREDENTIALS_GUIDE.md) +**📖 Guide**: [Switch Credentials Configuration](docs/SWITCH_CREDENTIALS_GUIDE.md) ## Quick Start Guide diff --git a/docs/Handling_Secrets_Guide.md b/docs/Handling_Secrets_Guide.md new file mode 100644 index 000000000..32c977a6e --- /dev/null +++ b/docs/Handling_Secrets_Guide.md @@ -0,0 +1,136 @@ +# Handling Secrets Guide + +This guide describes how to manage secrets such as passwords and authentication keys in the Network as Code VXLAN data model. There are two mechanisms available: + +1. **Ansible Vault** - Encrypts entire variable values at rest. +2. **Environment Variable Lookup (`env_var_`)** - Resolves secrets from environment variables at runtime. Suitable for CI/CD integration. + +Both approaches ensure that plaintext secrets are not visible in the data YAML files. The key difference is that Ansible Vault encrypts whole values while `env_var_` replaces individual strings — making `env_var_` the only option for secrets embedded in freeform configuration blocks. + +--- + +## Ansible Vault + +[Ansible Vault](https://docs.ansible.com/ansible/latest/vault_guide/index.html) encrypts sensitive data so it can be stored safely in data files. Encrypted values are decrypted automatically at playbook runtime when a vault password is provided. + +### Encrypting a Value + +```bash +ansible-vault encrypt_string 'MySecretPassword' --name 'authentication_key' +``` + +This produces an encrypted variable which you can paste into your data files: + +```yaml +vxlan: + underlay: + bgp: + authentication_enable: true + authentication_key_type: 3 + authentication_key: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 61626364656667686970... +``` + +### Running Playbooks with Vault + +```bash +# Prompt for vault password +ansible-playbook -i inventory deploy.yaml --ask-vault-pass + +# Use a password file +ansible-playbook -i inventory deploy.yaml --vault-password-file ~/.vault_pass +``` + +### Limitation: Freeform Configurations + +Ansible Vault encrypts the entire value of a YAML field. This works for standalone fields like `authentication_key` where the complete value is a secret. + +However, freeform configuration blocks may contain a mix of regular configuration and secrets within a single multi-line string: + +```yaml +# Does not work with Ansible Vault because only the TACACS key is a secret, +# not the entire block. +aaa_freeform: | + feature tacacs+ + tacacs-server key 7 MySecretTacacsKey + ip tacacs source-interface mgmt0 + tacacs-server timeout 20 +``` + +Encrypting the entire `aaa_freeform` value with Ansible Vault would encrypt all four lines, which is not desired. + +--- + +## Environment Variable Lookup (`env_var_`) + +The `env_var_` prefix instructs NaC to resolve a value from an environment variable at runtime. This works for: + +- **Standalone values** - the entire YAML value is an reference to an environment variable +- **Embedded secrets** - `env_var_` references inside larger strings (e.g. freeform configuration blocks) + +### How It Works + +Any string value in the data model that contains `env_var_` is resolved by the NaC VXLAN `validate` role. The secret is replaced with the value of the corresponding environment variable. + +* Variable names in YAML must exactly match the environment variable names (including the env_var_ prefix). +* Valid environment variable names may include letters, digits, and underscores. +* Use single quotes to prevent shell interpretation of special characters +* If the environment variable is not set, a warning is displayed and the secret is left unchanged. + +### Setting Environment Variables + +```bash +export env_var_BGP_AUTH_KEY='MyBgpP@$$w0rd' +export env_var_MCAST_AUTH_KEY='mcast_secret_123' +export env_var_TACACS_KEY='TacacsSecret!' +export env_var_DCI_PASSWORD='DciP@ssword' +``` + +### Examples: Standalone Values + +```yaml +vxlan: + underlay: + multicast: + ipv4: + authentication_enable: true + authentication_key: env_var_MCAST_AUTH_KEY + bgp: + authentication_enable: true + authentication_key_type: 3 + authentication_key: env_var_BGP_AUTH_KEY +``` + +```yaml +vxlan: + multisite: + overlay_dci: + enable_ebgp_password: True + ebgp_password: env_var_DCI_PASSWORD + ebgp_password_encryption_type: 3 +``` + +### Example: Freeform Configurations + +```yaml +vxlan: + global: + ebgp: + aaa_freeform: | + feature tacacs+ + tacacs-server key 7 env_var_TACACS_KEY + ip tacacs source-interface mgmt0 + tacacs-server timeout 20 +``` + +At runtime, only `env_var_TACACS_KEY` is replaced with the value of the `env_var_TACACS_KEY` environment variable. The rest of the block is unchanged: + +``` +feature tacacs+ +tacacs-server key 7 TacacsSecret! +ip tacacs source-interface mgmt0 +tacacs-server timeout 20 +``` + +This pattern works in any freeform field (`aaa_freeform`, `banner_freeform`, `bootstrap_freeform`, `intra_fabric_link_freeform`, `freeform_config`, etc.) and supports multiple `env_var_` tokens in the same block. diff --git a/plugins/action/common/prepare_plugins/prep_005_resolve_env_vars.py b/plugins/action/common/prepare_plugins/prep_005_resolve_env_vars.py new file mode 100644 index 000000000..141ff79fe --- /dev/null +++ b/plugins/action/common/prepare_plugins/prep_005_resolve_env_vars.py @@ -0,0 +1,106 @@ +# Copyright (c) 2025 Cisco Systems, Inc. and its affiliates +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# +# SPDX-License-Identifier: MIT + +import os +import re +from ansible.utils.display import Display + +display = Display() + +ENV_VAR_PREFIX = 'env_var_' + +# Matches env_var_ followed by one or more word characters (a-z, A-Z, 0-9, _) +ENV_VAR_PATTERN = re.compile(r'env_var_\w+') + + +def resolve_env_var_token(token, path): + """ + Resolve a single env_var_ token to its environment variable value. + + Note: Environment variables containing special characters like $, `, \\, etc. + should be properly escaped when setting them in the shell. + Example: export BGP_AUTH_KEY='MyP@$$w0rd' (use single quotes to prevent shell interpretation) + """ + resolved = os.getenv(token) + + if resolved is None: + display.warning( + f"Environment variable '{token}' referenced at " + f"'{path}' is not set. The value will not be resolved." + ) + return token + + display.vvv(f"Resolved '{token}' from environment variable at '{path}'") + return resolved + + +def resolve_env_vars_in_string(value, path): + resolved_count = 0 + + def replace_match(match): + nonlocal resolved_count + token = match.group(0) + replacement = resolve_env_var_token(token, path) + if replacement != token: + resolved_count += 1 + return replacement + + new_value = ENV_VAR_PATTERN.sub(replace_match, value) + return new_value, resolved_count + + +def resolve_env_vars_recursive(data, path=''): + resolved_count = 0 + + if isinstance(data, dict): + for key, value in data.items(): + current_path = f"{path}.{key}" if path else key + if isinstance(value, str) and ENV_VAR_PREFIX in value: + data[key], count = resolve_env_vars_in_string(value, current_path) + resolved_count += count + elif isinstance(value, (dict, list)): + resolved_count += resolve_env_vars_recursive(value, current_path) + elif isinstance(data, list): + for index, item in enumerate(data): + current_path = f"{path}[{index}]" + if isinstance(item, str) and ENV_VAR_PREFIX in item: + data[index], count = resolve_env_vars_in_string(item, current_path) + resolved_count += count + elif isinstance(item, (dict, list)): + resolved_count += resolve_env_vars_recursive(item, current_path) + + return resolved_count + + +class PreparePlugin: + def __init__(self, **kwargs): + self.kwargs = kwargs + self.keys = [] + + def prepare(self): + data_model = self.kwargs['results']['model_extended'] + + resolved_count = resolve_env_vars_recursive(data_model) + if resolved_count > 0: + display.v(f"Resolved {resolved_count} environment variable(s) in the data model") + + self.kwargs['results']['model_extended'] = data_model + return self.kwargs['results']