Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
136 changes: 136 additions & 0 deletions docs/Handling_Secrets_Guide.md
Original file line number Diff line number Diff line change
@@ -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.
106 changes: 106 additions & 0 deletions plugins/action/common/prepare_plugins/prep_005_resolve_env_vars.py
Original file line number Diff line number Diff line change
@@ -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']
Loading