From 5cf81da1d6b76ac68240e20ce44ea0a765a3e79b Mon Sep 17 00:00:00 2001 From: haseeb Date: Sun, 21 Sep 2025 12:27:35 +0530 Subject: [PATCH 1/5] Automate Nautobot service account provisioning When service account details are created in Vault (PasswordSafe), a Kubernetes secret is generated. Argo Events then triggers a Job that runs an Ansible playbook to ensure the user is created in Nautobot and a corresponding token is provisioned. --- ansible/playbooks/nautobot-user-token.yaml | 31 +++++ ansible/roles/users/defaults/main.yaml | 1 + ansible/roles/users/library/nautobot_token.py | 113 ++++++++++++++++++ ansible/roles/users/tasks/main.yaml | 34 ++++++ apps/site/secretstore-gen-secrets.yaml | 13 ++ .../secretstore-gen-secrets/.helmignore | 23 ++++ components/secretstore-gen-secrets/Chart.yaml | 6 + .../templates/secrets.yaml.tmpl | 55 +++++++++ .../secretstore-gen-secrets/values.yaml | 8 ++ workflows/kustomization.yaml | 1 + .../nautobot/eventbus/eventbus-default.yaml | 27 +++++ ...disruptionbudget-eventbus-default-pdb.yaml | 11 ++ .../k8s-secret-nautobot-token.yaml | 19 +++ workflows/nautobot/kustomization.yaml | 13 ++ .../nautobot/sensors/k8s-nautobot-secret.yaml | 71 +++++++++++ .../k8s-secret-events-nautobot.yaml | 34 ++++++ 16 files changed, 460 insertions(+) create mode 100644 ansible/playbooks/nautobot-user-token.yaml create mode 100644 ansible/roles/users/defaults/main.yaml create mode 100644 ansible/roles/users/library/nautobot_token.py create mode 100644 ansible/roles/users/tasks/main.yaml create mode 100644 apps/site/secretstore-gen-secrets.yaml create mode 100644 components/secretstore-gen-secrets/.helmignore create mode 100644 components/secretstore-gen-secrets/Chart.yaml create mode 100644 components/secretstore-gen-secrets/templates/secrets.yaml.tmpl create mode 100644 components/secretstore-gen-secrets/values.yaml create mode 100644 workflows/nautobot/eventbus/eventbus-default.yaml create mode 100644 workflows/nautobot/eventbus/poddisruptionbudget-eventbus-default-pdb.yaml create mode 100644 workflows/nautobot/eventsources/k8s-secret-nautobot-token.yaml create mode 100644 workflows/nautobot/kustomization.yaml create mode 100644 workflows/nautobot/sensors/k8s-nautobot-secret.yaml create mode 100644 workflows/nautobot/serviceaccounts/k8s-secret-events-nautobot.yaml diff --git a/ansible/playbooks/nautobot-user-token.yaml b/ansible/playbooks/nautobot-user-token.yaml new file mode 100644 index 000000000..ec9edea9d --- /dev/null +++ b/ansible/playbooks/nautobot-user-token.yaml @@ -0,0 +1,31 @@ +--- +- name: Nautobot service accounts for Undercloud services + connection: local + hosts: nautobot + gather_facts: false + + pre_tasks: + - name: Load Nautobot credentials from environment + ansible.builtin.set_fact: + nautobot_data: "{{ lookup('env', 'EXTRA_VARS') | from_json }}" + + - name: Decode Nautobot credentials + ansible.builtin.set_fact: + nautobot_hostname: "{{ nautobot_data.hostname | b64decode }}" + nautobot_username: "{{ nautobot_data.username | b64decode }}" + nautobot_password: "{{ nautobot_data.password | b64decode }}" + nautobot_user_token: "{{ nautobot_data.token | b64decode }}" + + - name: Ensure nautobot is up and responding + ansible.builtin.uri: + url: "https://{{ nautobot_hostname }}/health/" + method: GET + validate_certs: false + register: nautobot_up_check + until: nautobot_up_check.status == 200 + retries: 24 # Retries for 24 * 5 seconds = 120 seconds = 2 minutes + delay: 5 # Every 5 seconds + check_mode: false + + roles: + - role: users diff --git a/ansible/roles/users/defaults/main.yaml b/ansible/roles/users/defaults/main.yaml new file mode 100644 index 000000000..ed97d539c --- /dev/null +++ b/ansible/roles/users/defaults/main.yaml @@ -0,0 +1 @@ +--- diff --git a/ansible/roles/users/library/nautobot_token.py b/ansible/roles/users/library/nautobot_token.py new file mode 100644 index 000000000..154753a2f --- /dev/null +++ b/ansible/roles/users/library/nautobot_token.py @@ -0,0 +1,113 @@ +from ansible.module_utils.basic import AnsibleModule +import requests + + +def check_existing_token(base_url, username, password, user_token): + """Check if a specific token exists for the user.""" + headers = {"Accept": "application/json"} + tokens_url = f"{base_url}/api/users/tokens/" + + try: + response = requests.get(tokens_url, headers=headers, auth=(username, password)) + response.raise_for_status() + except requests.exceptions.RequestException as e: + return None, f"Failed to fetch tokens: {e}" + + data = response.json() + tokens = data.get("results", []) + + if not tokens: + return None, "No tokens found" + + # Find the token matching user_token + token = next((t for t in tokens if t.get("key") == user_token), None) + if not token: + return None, "Specified token not found for user" + + return token, None + + +def create_new_token( + base_url, username, password, user_token, description="ansible-created-token" +): + """Create a new Nautobot token using Basic Auth.""" + tokens_url = f"{base_url}/api/users/tokens/" + headers = {"Content-Type": "application/json", "Accept": "application/json"} + payload = {"key": user_token, "description": description, "write_enabled": True} + + try: + response = requests.post( + tokens_url, headers=headers, json=payload, auth=(username, password) + ) + response.raise_for_status() + except requests.exceptions.RequestException as e: + return None, f"Failed to create new token: {e}" + + return response.json(), None + + +def run_module(): + module_args = dict( + base_url=dict(type="str", required=True), + username=dict(type="str", required=True), + password=dict(type="str", required=True, no_log=True), + user_token=dict(type="str", required=True, no_log=True), + create_if_notfound=dict(type="bool", default=True), + token_description=dict(type="str", default="ansible-created-token"), + ) + + module = AnsibleModule(argument_spec=module_args, supports_check_mode=True) + result = dict(changed=False, token=None, message="") + + base_url = module.params["base_url"].rstrip("/") + username = module.params["username"] + password = module.params["password"] + user_token = module.params["user_token"] + create_if_notfound = module.params["create_if_notfound"] + token_description = module.params["token_description"] + + if module.check_mode: + module.exit_json(**result) + + # Check existing token + token, error = check_existing_token(base_url, username, password, user_token) + + if token: + result.update( + changed=False, + message=f"Found existing token for {username}", + token=dict( + id=str(token.get("id")), + display=str(token.get("display")), + created=str(token.get("created")), + expires=str(token.get("expires")), + write_enabled=bool(token.get("write_enabled")), + description=str(token.get("description", "No description")), + ), + ) + module.exit_json(**result) + + # No token found → create new if allowed + if create_if_notfound: + new_token, err = create_new_token( + base_url, username, password, user_token, token_description + ) + if err: + module.fail_json(msg=err) + result.update( + changed=True, + message=f"No token found, created new token for {username}", + token=new_token, + ) + module.exit_json(**result) + + # No token and not allowed to create → fail + module.fail_json(msg=f"No token found for {username} and creation disabled") + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/ansible/roles/users/tasks/main.yaml b/ansible/roles/users/tasks/main.yaml new file mode 100644 index 000000000..51aba040f --- /dev/null +++ b/ansible/roles/users/tasks/main.yaml @@ -0,0 +1,34 @@ +--- +- name: Query user in Nautobot + ansible.builtin.uri: + url: "https://{{ nautobot_hostname }}/api/users/users/{{ nautobot_username }}" + method: GET + headers: + Authorization: "Token {{ nautobot_token }}" + return_content: true + status_code: [200, 404] + register: user_query_result + +- name: Create user in Nautobot if missing + ansible.builtin.uri: + url: "https://{{ nautobot_hostname }}/api/users/users/" + method: POST + headers: + Authorization: "Token {{ nautobot_token }}" + Content-Type: "application/json" + body_format: json + body: + username: "{{ nautobot_username }}" + password: "{{ nautobot_password }}" + is_staff: true + is_superuser: true + status_code: [201] + when: user_query_result.status == 404 + +- name: Ensure Nautobot token exists for user + nautobot_token: + base_url: "https://{{ nautobot_hostname }}" + username: "{{ nautobot_username }}" + password: "{{ nautobot_password }}" + user_token: "{{ nautobot_user_token }}" + register: nautobot_token_result diff --git a/apps/site/secretstore-gen-secrets.yaml b/apps/site/secretstore-gen-secrets.yaml new file mode 100644 index 000000000..f0466d27b --- /dev/null +++ b/apps/site/secretstore-gen-secrets.yaml @@ -0,0 +1,13 @@ +--- +component: nautobot-secrets +componentNamespace: nautobot +sources: + - ref: understack + path: 'components/secretstore-gen-secrets' + helm: + releaseName: nautobot-secrets + valueFiles: + - $deploy/{{.name}}/helm-configs/secretstore-nautobot-secrets.yaml + ignoreMissingValueFiles: true + - ref: deploy + path: '{{.name}}/manifests/secret-store' diff --git a/components/secretstore-gen-secrets/.helmignore b/components/secretstore-gen-secrets/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/components/secretstore-gen-secrets/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/components/secretstore-gen-secrets/Chart.yaml b/components/secretstore-gen-secrets/Chart.yaml new file mode 100644 index 000000000..3f7c483d7 --- /dev/null +++ b/components/secretstore-gen-secrets/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: site-secrets +description: Orchestrating secrets across kubernetes clusters (global-site) using External SecretStore +type: application +version: 0.1.0 +appVersion: "1.0" diff --git a/components/secretstore-gen-secrets/templates/secrets.yaml.tmpl b/components/secretstore-gen-secrets/templates/secrets.yaml.tmpl new file mode 100644 index 000000000..00992f0b2 --- /dev/null +++ b/components/secretstore-gen-secrets/templates/secrets.yaml.tmpl @@ -0,0 +1,55 @@ +{{- $site := .Values.site }} +{{- $secretStore := $site.secretStore }} +{{- range $site.secrets }} +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: {{ .name }} + {{- if .externalLinkAnnotationTemplate }} + annotations: + link.argocd.argoproj.io/external-link: {{ tpl .externalLinkAnnotationTemplate . }} + {{- end }} + {{- with .labels }} + labels: +{{ toYaml . | indent 4 }} +{{- end }} +spec: + refreshInterval: {{ .refreshInterval | default "1h" }} + secretStoreRef: + kind: {{ $secretStore.kind }} + name: {{ $secretStore.name }} + target: + name: {{ .name }} + creationPolicy: Owner + template: + engineVersion: v2 + type: {{ .templateType | default "Opaque" }} + {{- with .labels }} + metadata: + labels: +{{ toYaml . | indent 10 }} + {{- end }} +{{- if .templateData }} + data: +{{- range $k, $v := .templateData }} + {{ $k }}: {{ $v | quote }} +{{- end }} +{{- end }} +{{- if .data }} + data: +{{- range .data }} + - secretKey: {{ .secretKey }} + remoteRef: + key: {{ .remoteRef.key }} + property: {{ .remoteRef.property }} + conversionStrategy: {{ .remoteRef.conversionStrategy | default "Default" }} + decodingStrategy: {{ .remoteRef.decodingStrategy | default "None" }} + metadataPolicy: {{ .remoteRef.metadataPolicy | default "None" }} +{{- end }} +{{- end }} +{{- if .dataFrom }} + dataFrom: +{{- toYaml .dataFrom | nindent 4 }} +{{- end }} +{{- end }} diff --git a/components/secretstore-gen-secrets/values.yaml b/components/secretstore-gen-secrets/values.yaml new file mode 100644 index 000000000..a504a7b98 --- /dev/null +++ b/components/secretstore-gen-secrets/values.yaml @@ -0,0 +1,8 @@ +site: + name: uc-iad3-dev + partition: uc-dev + env: dev + role: aio + secretStore: + kind: SecretStore + name: vault diff --git a/workflows/kustomization.yaml b/workflows/kustomization.yaml index 2fd7e1220..610bf888a 100644 --- a/workflows/kustomization.yaml +++ b/workflows/kustomization.yaml @@ -4,3 +4,4 @@ kind: Kustomization resources: - openstack - argo-events + - nautobot diff --git a/workflows/nautobot/eventbus/eventbus-default.yaml b/workflows/nautobot/eventbus/eventbus-default.yaml new file mode 100644 index 000000000..d9412a007 --- /dev/null +++ b/workflows/nautobot/eventbus/eventbus-default.yaml @@ -0,0 +1,27 @@ +# default NATS EventBus sourced from: +# https://raw.githubusercontent.com/argoproj/argo-events/stable/examples/eventbus/native.yaml + +apiVersion: argoproj.io/v1alpha1 +kind: EventBus +metadata: + name: default +spec: + nats: + native: + # Optional, defaults to 3. If it is < 3, set it to 3, that is the minimal requirement. + replicas: 3 + # Optional, authen strategy, "none" or "token", defaults to "none" + auth: token +# containerTemplate: +# resources: +# requests: +# cpu: "10m" +# metricsContainerTemplate: +# resources: +# requests: +# cpu: "10m" +# antiAffinity: false +# persistence: +# storageClassName: standard +# accessMode: ReadWriteOnce +# volumeSize: 10Gi diff --git a/workflows/nautobot/eventbus/poddisruptionbudget-eventbus-default-pdb.yaml b/workflows/nautobot/eventbus/poddisruptionbudget-eventbus-default-pdb.yaml new file mode 100644 index 000000000..90bb443e1 --- /dev/null +++ b/workflows/nautobot/eventbus/poddisruptionbudget-eventbus-default-pdb.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: eventbus-default-pdb +spec: + maxUnavailable: 1 + selector: + matchLabels: + controller: eventbus-controller + eventbus-name: default diff --git a/workflows/nautobot/eventsources/k8s-secret-nautobot-token.yaml b/workflows/nautobot/eventsources/k8s-secret-nautobot-token.yaml new file mode 100644 index 000000000..6d549c64b --- /dev/null +++ b/workflows/nautobot/eventsources/k8s-secret-nautobot-token.yaml @@ -0,0 +1,19 @@ +apiVersion: argoproj.io/v1alpha1 +kind: EventSource +metadata: + name: k8s-secret-nautobot-token +spec: + template: + serviceAccountName: k8s-events-secret-nautobot + resource: + nautobot-token-secret: + namespace: nautobot + resource: secrets + version: v1 + eventTypes: + - ADD +# - UPDATE + filter: + labels: + - key: token/type + value: nautobot diff --git a/workflows/nautobot/kustomization.yaml b/workflows/nautobot/kustomization.yaml new file mode 100644 index 000000000..ab769635c --- /dev/null +++ b/workflows/nautobot/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: nautobot + +resources: + - eventbus/eventbus-default.yaml + - eventbus/poddisruptionbudget-eventbus-default-pdb.yaml + + # noutobot secret + - serviceaccounts/k8s-secret-events-nautobot.yaml + - eventsources/k8s-secret-nautobot-token.yaml + - sensors/k8s-nautobot-secret.yaml diff --git a/workflows/nautobot/sensors/k8s-nautobot-secret.yaml b/workflows/nautobot/sensors/k8s-nautobot-secret.yaml new file mode 100644 index 000000000..bd6e7923e --- /dev/null +++ b/workflows/nautobot/sensors/k8s-nautobot-secret.yaml @@ -0,0 +1,71 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Sensor +metadata: + name: secret-nautobot-token + annotations: + workflows.argoproj.io/title: Nautobot Token + workflows.argoproj.io/description: |+ + Triggered when the Kubernetes Nautobot token secret is created, + this process ensures the user is created in Nautobot and a corresponding token is provisioned. +spec: + template: + serviceAccountName: k8s-events-secret-nautobot + dependencies: + - name: nautobot-token-secret + eventSourceName: k8s-secret-nautobot-token + eventName: nautobot-token-secret + triggers: + - template: + name: nautobot-users + k8s: + operation: create + parameters: + # Pass the body.data as JSON string into the Job environment + - src: + dependencyName: nautobot-token-secret + dataKey: body.data + transformer: + jqFilter: '@json' + dest: spec.template.spec.containers.0.env.0.value + source: + resource: + apiVersion: batch/v1 + kind: Job + metadata: + generateName: nautobot-create-token- + spec: + template: + spec: + containers: + - name: nautobot-create-token + image: ghcr.io/rackerlabs/understack/ansible:pr-1256 + imagePullPolicy: Always + command: + - "ansible-runner" + - "run" + - "/runner" + - "--playbook" + - "nautobot-user-token.yaml" + env: + - name: EXTRA_VARS + value: "" # Will be populated by the Sensor mapping + - name: NAUTOBOT_TOKEN + valueFrom: + secretKeyRef: + name: nautobot-superuser + key: apitoken + volumeMounts: + - name: ansible-inventory + mountPath: /runner/inventory/ + - name: ansible-group-vars + mountPath: /runner/inventory/group_vars/ + volumes: + - name: runner-data + emptyDir: {} + - name: ansible-inventory + configMap: + name: ansible-inventory + - name: ansible-group-vars + configMap: + name: ansible-group-vars + restartPolicy: OnFailure diff --git a/workflows/nautobot/serviceaccounts/k8s-secret-events-nautobot.yaml b/workflows/nautobot/serviceaccounts/k8s-secret-events-nautobot.yaml new file mode 100644 index 000000000..13933e6d2 --- /dev/null +++ b/workflows/nautobot/serviceaccounts/k8s-secret-events-nautobot.yaml @@ -0,0 +1,34 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: k8s-events-secret-nautobot + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: secret-reader +rules: + - apiGroups: + - "" + - batch + resources: + - secrets + - jobs + verbs: + - '*' + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: secret-reader-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: secret-reader +subjects: + - kind: ServiceAccount + name: k8s-events-secret-nautobot + namespace: nautobot From d0988defee0404a5d362600ca54c7eb00a45af8b Mon Sep 17 00:00:00 2001 From: haseeb Date: Tue, 23 Sep 2025 00:56:48 +0530 Subject: [PATCH 2/5] review comments implementation --- ansible/roles/users/library/nautobot_token.py | 95 +++----- ... => nautobot-secretstore-gen-secrets.yaml} | 0 components/secretstore-gen-secrets/Chart.yaml | 4 +- .../templates/secrets.yaml.tmpl | 5 +- .../secretstore-gen-secrets/values.yaml | 9 +- .../deploy-guide/generate-external-secrets.md | 121 ++++++++++ ...ponent-secretstore-gen-secrets.schema.json | 226 ++++++++++++++++++ mkdocs.yml | 1 + .../nautobot/eventbus/eventbus-default.yaml | 13 - workflows/nautobot/kustomization.yaml | 3 +- .../nautobot/sensors/k8s-nautobot-secret.yaml | 4 +- .../serviceaccounts/k8s-job-create.yaml | 29 +++ .../k8s-secret-events-nautobot.yaml | 12 +- 13 files changed, 427 insertions(+), 95 deletions(-) rename apps/site/{secretstore-gen-secrets.yaml => nautobot-secretstore-gen-secrets.yaml} (100%) create mode 100644 docs/deploy-guide/generate-external-secrets.md create mode 100644 docs/schema/component-secretstore-gen-secrets.schema.json create mode 100644 workflows/nautobot/serviceaccounts/k8s-job-create.yaml diff --git a/ansible/roles/users/library/nautobot_token.py b/ansible/roles/users/library/nautobot_token.py index 154753a2f..11b2707b7 100644 --- a/ansible/roles/users/library/nautobot_token.py +++ b/ansible/roles/users/library/nautobot_token.py @@ -2,35 +2,23 @@ import requests -def check_existing_token(base_url, username, password, user_token): - """Check if a specific token exists for the user.""" +def get_existing_token(base_url, username, password, user_token): + """Return the token dict if it exists, otherwise None.""" headers = {"Accept": "application/json"} tokens_url = f"{base_url}/api/users/tokens/" try: response = requests.get(tokens_url, headers=headers, auth=(username, password)) response.raise_for_status() - except requests.exceptions.RequestException as e: - return None, f"Failed to fetch tokens: {e}" + except requests.exceptions.RequestException: + return None - data = response.json() - tokens = data.get("results", []) + tokens = response.json().get("results", []) + return next((t for t in tokens if t.get("key") == user_token), None) - if not tokens: - return None, "No tokens found" - # Find the token matching user_token - token = next((t for t in tokens if t.get("key") == user_token), None) - if not token: - return None, "Specified token not found for user" - - return token, None - - -def create_new_token( - base_url, username, password, user_token, description="ansible-created-token" -): - """Create a new Nautobot token using Basic Auth.""" +def create_new_token(base_url, username, password, user_token, description): + """Create a new Nautobot token using Basic Auth. Returns the token dict or None.""" tokens_url = f"{base_url}/api/users/tokens/" headers = {"Content-Type": "application/json", "Accept": "application/json"} payload = {"key": user_token, "description": description, "write_enabled": True} @@ -40,10 +28,24 @@ def create_new_token( tokens_url, headers=headers, json=payload, auth=(username, password) ) response.raise_for_status() - except requests.exceptions.RequestException as e: - return None, f"Failed to create new token: {e}" + except requests.exceptions.RequestException: + return None + + return response.json() + - return response.json(), None +def format_token_response(token): + """Normalize token dict fields for output.""" + if not token: + return None + return { + "id": str(token.get("id")), + "display": str(token.get("display")), + "created": str(token.get("created")), + "expires": str(token.get("expires")), + "write_enabled": bool(token.get("write_enabled")), + "description": str(token.get("description", "No description")), + } def run_module(): @@ -52,57 +54,36 @@ def run_module(): username=dict(type="str", required=True), password=dict(type="str", required=True, no_log=True), user_token=dict(type="str", required=True, no_log=True), - create_if_notfound=dict(type="bool", default=True), token_description=dict(type="str", default="ansible-created-token"), ) module = AnsibleModule(argument_spec=module_args, supports_check_mode=True) - result = dict(changed=False, token=None, message="") base_url = module.params["base_url"].rstrip("/") username = module.params["username"] password = module.params["password"] user_token = module.params["user_token"] - create_if_notfound = module.params["create_if_notfound"] token_description = module.params["token_description"] - if module.check_mode: - module.exit_json(**result) - - # Check existing token - token, error = check_existing_token(base_url, username, password, user_token) - + # fetch existing token + token = get_existing_token(base_url, username, password, user_token) if token: - result.update( + module.exit_json( changed=False, message=f"Found existing token for {username}", - token=dict( - id=str(token.get("id")), - display=str(token.get("display")), - created=str(token.get("created")), - expires=str(token.get("expires")), - write_enabled=bool(token.get("write_enabled")), - description=str(token.get("description", "No description")), - ), + token=format_token_response(token), ) - module.exit_json(**result) - # No token found → create new if allowed - if create_if_notfound: - new_token, err = create_new_token( - base_url, username, password, user_token, token_description - ) - if err: - module.fail_json(msg=err) - result.update( - changed=True, - message=f"No token found, created new token for {username}", - token=new_token, - ) - module.exit_json(**result) + # No token found → try creating new + new_token = create_new_token(base_url, username, password, user_token, token_description) + if not new_token: + module.fail_json(msg="Failed to create new token") - # No token and not allowed to create → fail - module.fail_json(msg=f"No token found for {username} and creation disabled") + module.exit_json( + changed=True, + message=f"No token found, created new token for {username}", + token=format_token_response(new_token), + ) def main(): diff --git a/apps/site/secretstore-gen-secrets.yaml b/apps/site/nautobot-secretstore-gen-secrets.yaml similarity index 100% rename from apps/site/secretstore-gen-secrets.yaml rename to apps/site/nautobot-secretstore-gen-secrets.yaml diff --git a/components/secretstore-gen-secrets/Chart.yaml b/components/secretstore-gen-secrets/Chart.yaml index 3f7c483d7..c72feede5 100644 --- a/components/secretstore-gen-secrets/Chart.yaml +++ b/components/secretstore-gen-secrets/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 -name: site-secrets -description: Orchestrating secrets across kubernetes clusters (global-site) using External SecretStore +name: secretstore-gen-secrets +description: Secret store backed External Secrets generator type: application version: 0.1.0 appVersion: "1.0" diff --git a/components/secretstore-gen-secrets/templates/secrets.yaml.tmpl b/components/secretstore-gen-secrets/templates/secrets.yaml.tmpl index 00992f0b2..6f071565b 100644 --- a/components/secretstore-gen-secrets/templates/secrets.yaml.tmpl +++ b/components/secretstore-gen-secrets/templates/secrets.yaml.tmpl @@ -1,6 +1,5 @@ -{{- $site := .Values.site }} -{{- $secretStore := $site.secretStore }} -{{- range $site.secrets }} +{{- $secretStore := .Values.secretStore }} +{{- range .Values.secrets }} --- apiVersion: external-secrets.io/v1 kind: ExternalSecret diff --git a/components/secretstore-gen-secrets/values.yaml b/components/secretstore-gen-secrets/values.yaml index a504a7b98..ed97d539c 100644 --- a/components/secretstore-gen-secrets/values.yaml +++ b/components/secretstore-gen-secrets/values.yaml @@ -1,8 +1 @@ -site: - name: uc-iad3-dev - partition: uc-dev - env: dev - role: aio - secretStore: - kind: SecretStore - name: vault +--- diff --git a/docs/deploy-guide/generate-external-secrets.md b/docs/deploy-guide/generate-external-secrets.md new file mode 100644 index 000000000..6a1d4faaf --- /dev/null +++ b/docs/deploy-guide/generate-external-secrets.md @@ -0,0 +1,121 @@ +# External Secrets Guide + +This guide explains how to automate Kubernetes secrets creation using +external secret stores (Vault, AWS Secrets Manager, etc.) and ArgoCD +components in the UnderStack environment. + +------------------------------------------------------------------------ + +## TL;DR Quick Steps + +1. **Assumption**: Credentials already exist in an external system + (Vault, AWS Secrets Manager, PasswordSafe, etc.). +2. **Install SecretStore** in the desired Kubernetes namespace. +3. **Create ArgoCD component** in [apps/site/](https://github.com/rackerlabs/understack/tree/main/apps/site) + to manage SecretStore + ExternalSecrets. +4. **Configure `values.yaml`** to define secret templates. +5. **Apply ArgoCD application** to deploy SecretStore and + ExternalSecrets. + +------------------------------------------------------------------------ + +## Assumptions + +- Credentials are managed in external systems (Vault, AWS Secrets + Manager, PasswordSafe). +- Secrets must be pulled automatically into Kubernetes clusters. + +------------------------------------------------------------------------ + +## Workflow + +1. **Create an External SecretStore per Kubernetes namespace**. + This links Kubernetes to the external backend (Vault, AWS Secrets + Manager, etc.). + +2. **Create an ArgoCD component** inside the site repo: + Example: + [nautobot-secretstore-gen-secrets.yaml](https://raw.githubusercontent.com/rackerlabs/understack/refs/heads/main/apps/site/nautobot-secretstore-gen-secrets.yaml) + + This ArgoCD component performs two tasks: + + - Installs the SecretStore. + - Creates ExternalSecrets referencing the SecretStore. + +3. **Deploy via Kustomize/Helm** + + - One Helm chart installs the SecretStore pointing to the external + backend system. + - Another Helm chart creates the ExternalSecrets from that + SecretStore. + +4. **Configure `values.yaml`** with templates for secrets. + +------------------------------------------------------------------------ + +## Example ArgoCD Component + +``` yaml +--- +component: nautobot-secrets +componentNamespace: nautobot +sources: + - ref: understack + path: 'components/secretstore-gen-secrets' + helm: + releaseName: nautobot-secrets + valueFiles: + - $deploy/{{.name}}/helm-configs/secretstore-nautobot-secrets.yaml + ignoreMissingValueFiles: true + - ref: deploy + path: '{{.name}}/manifests/secret-store' +``` + +------------------------------------------------------------------------ + +## Sample values.yaml + +``` yaml +# yaml-language-server: $schema=https://rackerlabs.understack.io/schema/component-secretstore-gen-secrets.schema.json +--- +secretStore: + kind: SecretStore + name: pwsafe + +secrets: + - name: site1-token + externalLinkAnnotationTemplate: "https://secretvault.example.com/credentials/373525" + templateData: + hostname: "{{ .hostname }}" + username: "{{ .username }}" + password: "{{ .password }}" + token: "{{ .token }}" + dataFrom: + - extract: + key: "373525" + + - name: site2-token + externalLinkAnnotationTemplate: "https://secretvault.example.com/credentials/373539" + labels: + token/type: mycustomlabel + templateData: + hostname: "{{ .hostname }}" + username: "{{ .username }}" + password: "{{ .password }}" + token: "{{ .token }}" + dataFrom: + - extract: + key: "373539" +``` + +------------------------------------------------------------------------ + +## Summary + +- **External systems** (Vault, AWS Secrets Manager, PasswordSafe) hold + credentials. +- **SecretStore** bridges Kubernetes to the external backend. +- **ExternalSecrets** define the actual K8s secrets created from + external data. +- **ArgoCD components** ensure consistent automation of this workflow + across namespaces. diff --git a/docs/schema/component-secretstore-gen-secrets.schema.json b/docs/schema/component-secretstore-gen-secrets.schema.json new file mode 100644 index 000000000..24997bf31 --- /dev/null +++ b/docs/schema/component-secretstore-gen-secrets.schema.json @@ -0,0 +1,226 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Secret store backed External Secrets generator Helm Chart Values", + "description": "Schema for Secret store backed secrets generator Helm chart values.yaml configuration", + "type": "object", + "properties": { + "secretStore": { + "type": "object", + "description": "Secret store backed secrets generator Secret Store configuration", + "properties": { + "kind": { + "type": "string", + "enum": ["ClusterSecretStore", "SecretStore"], + "description": "Type of secret store - ClusterSecretStore or SecretStore for namespaced" + }, + "name": { + "type": "string", + "description": "Name of the ClusterSecretStore or SecretStore to use" + } + }, + "required": ["kind", "name"], + "additionalProperties": false + }, + "secrets": { + "type": "array", + "description": "Array of External Secret configurations", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the ExternalSecret and target Secret resource" + }, + "externalLinkAnnotationTemplate": { + "type": "string", + "description": "Optional template for external link annotations on ExternalSecret resources" + }, + "labels": { + "type": "object", + "description": "Labels to apply to both ExternalSecret and target Secret", + "additionalProperties": { + "type": "string" + } + }, + "refreshInterval": { + "type": "string", + "pattern": "^[0-9]+(s|m|h|d)$", + "default": "1h", + "description": "How often Secret store backed secrets generator refreshes this secret" + }, + "templateType": { + "type": "string", + "default": "Opaque", + "description": "Kubernetes secret type for the target secret", + "enum": ["Opaque", "kubernetes.io/tls", "kubernetes.io/ssh-auth", "kubernetes.io/basic-auth", "kubernetes.io/dockerconfigjson", "kubernetes.io/service-account-token"] + }, + "templateData": { + "type": "object", + "description": "Template data for constructing the target secret using Go templating", + "additionalProperties": { + "type": "string" + } + }, + "data": { + "type": "array", + "description": "Individual data mappings from remote secrets to target secret keys", + "items": { + "type": "object", + "properties": { + "secretKey": { + "type": "string", + "description": "Key name in the target Kubernetes secret" + }, + "remoteRef": { + "type": "object", + "description": "Reference to the remote secret", + "properties": { + "key": { + "type": "string", + "description": "Key/ID of the remote secret" + }, + "property": { + "type": "string", + "description": "Property/field within the remote secret" + }, + "conversionStrategy": { + "type": "string", + "default": "Default", + "enum": ["Default", "Unicode"], + "description": "Strategy for converting the secret value" + }, + "decodingStrategy": { + "type": "string", + "default": "None", + "enum": ["None", "Base64", "Base64URL", "Auto"], + "description": "Strategy for decoding the secret value" + }, + "metadataPolicy": { + "type": "string", + "default": "None", + "enum": ["None", "Fetch"], + "description": "Policy for handling secret metadata" + } + }, + "required": ["key"], + "additionalProperties": false + } + }, + "required": ["secretKey", "remoteRef"], + "additionalProperties": false + } + }, + "dataFrom": { + "type": "array", + "description": "Bulk data extraction from remote secrets", + "items": { + "type": "object", + "properties": { + "extract": { + "type": "object", + "description": "Extract configuration for bulk secret retrieval", + "properties": { + "key": { + "type": "string", + "description": "Key/ID of the remote secret to extract from" + }, + "property": { + "type": "string", + "description": "Specific property to extract (optional)" + }, + "conversionStrategy": { + "type": "string", + "default": "Default", + "enum": ["Default", "Unicode"], + "description": "Strategy for converting the secret values" + }, + "decodingStrategy": { + "type": "string", + "default": "None", + "enum": ["None", "Base64", "Base64URL", "Auto"], + "description": "Strategy for decoding the secret values" + }, + "metadataPolicy": { + "type": "string", + "default": "None", + "enum": ["None", "Fetch"], + "description": "Policy for handling secret metadata" + } + }, + "required": ["key"], + "additionalProperties": false + }, + "find": { + "type": "object", + "description": "Find configuration for pattern-based secret discovery", + "properties": { + "path": { + "type": "string", + "description": "Path pattern to search for secrets" + }, + "name": { + "type": "object", + "description": "Name pattern configuration", + "properties": { + "regexp": { + "type": "string", + "description": "Regular expression for matching secret names" + } + }, + "additionalProperties": false + }, + "tags": { + "type": "object", + "description": "Tag filters for finding secrets", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "sourceRef": { + "type": "object", + "description": "Reference to a different secret store for this data source", + "properties": { + "storeRef": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the secret store" + }, + "kind": { + "type": "string", + "enum": ["ClusterSecretStore", "SecretStore"], + "description": "Kind of secret store" + } + }, + "required": ["name", "kind"], + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "anyOf": [ + {"required": ["extract"]}, + {"required": ["find"]} + ], + "additionalProperties": false + } + } + }, + "required": ["name"], + "anyOf": [ + {"required": ["data"]}, + {"required": ["dataFrom"]}, + {"required": ["templateData"]} + ], + "additionalProperties": false + } + } + }, + "required": ["secretStore", "secrets"], + "additionalProperties": false +} diff --git a/mkdocs.yml b/mkdocs.yml index 89934170a..d07224a41 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -134,6 +134,7 @@ nav: - deploy-guide/config-dex.md - deploy-guide/config-openstack.md - deploy-guide/secrets-eso-setup.md + - deploy-guide/generate-external-secrets.md - deploy-guide/openstack-svc-users.md - deploy-guide/auth.md - deploy-guide/config-argo-workflows.md diff --git a/workflows/nautobot/eventbus/eventbus-default.yaml b/workflows/nautobot/eventbus/eventbus-default.yaml index d9412a007..3aedb8049 100644 --- a/workflows/nautobot/eventbus/eventbus-default.yaml +++ b/workflows/nautobot/eventbus/eventbus-default.yaml @@ -12,16 +12,3 @@ spec: replicas: 3 # Optional, authen strategy, "none" or "token", defaults to "none" auth: token -# containerTemplate: -# resources: -# requests: -# cpu: "10m" -# metricsContainerTemplate: -# resources: -# requests: -# cpu: "10m" -# antiAffinity: false -# persistence: -# storageClassName: standard -# accessMode: ReadWriteOnce -# volumeSize: 10Gi diff --git a/workflows/nautobot/kustomization.yaml b/workflows/nautobot/kustomization.yaml index ab769635c..a38fd1d9f 100644 --- a/workflows/nautobot/kustomization.yaml +++ b/workflows/nautobot/kustomization.yaml @@ -7,7 +7,8 @@ resources: - eventbus/eventbus-default.yaml - eventbus/poddisruptionbudget-eventbus-default-pdb.yaml - # noutobot secret + # nautobot secret - serviceaccounts/k8s-secret-events-nautobot.yaml + - serviceaccounts/k8s-job-create.yaml - eventsources/k8s-secret-nautobot-token.yaml - sensors/k8s-nautobot-secret.yaml diff --git a/workflows/nautobot/sensors/k8s-nautobot-secret.yaml b/workflows/nautobot/sensors/k8s-nautobot-secret.yaml index bd6e7923e..52e0cd3b3 100644 --- a/workflows/nautobot/sensors/k8s-nautobot-secret.yaml +++ b/workflows/nautobot/sensors/k8s-nautobot-secret.yaml @@ -9,7 +9,7 @@ metadata: this process ensures the user is created in Nautobot and a corresponding token is provisioned. spec: template: - serviceAccountName: k8s-events-secret-nautobot + serviceAccountName: k8s-job-create dependencies: - name: nautobot-token-secret eventSourceName: k8s-secret-nautobot-token @@ -38,7 +38,7 @@ spec: spec: containers: - name: nautobot-create-token - image: ghcr.io/rackerlabs/understack/ansible:pr-1256 + image: ghcr.io/rackerlabs/understack/ansible:latest imagePullPolicy: Always command: - "ansible-runner" diff --git a/workflows/nautobot/serviceaccounts/k8s-job-create.yaml b/workflows/nautobot/serviceaccounts/k8s-job-create.yaml new file mode 100644 index 000000000..058e2198f --- /dev/null +++ b/workflows/nautobot/serviceaccounts/k8s-job-create.yaml @@ -0,0 +1,29 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: k8s-job-create + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: k8s-job +rules: + - apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["create"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: k8s-job-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: k8s-job +subjects: + - kind: ServiceAccount + name: k8s-job-create + namespace: nautobot diff --git a/workflows/nautobot/serviceaccounts/k8s-secret-events-nautobot.yaml b/workflows/nautobot/serviceaccounts/k8s-secret-events-nautobot.yaml index 13933e6d2..9fbe27740 100644 --- a/workflows/nautobot/serviceaccounts/k8s-secret-events-nautobot.yaml +++ b/workflows/nautobot/serviceaccounts/k8s-secret-events-nautobot.yaml @@ -3,21 +3,15 @@ apiVersion: v1 kind: ServiceAccount metadata: name: k8s-events-secret-nautobot - --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: secret-reader rules: - - apiGroups: - - "" - - batch - resources: - - secrets - - jobs - verbs: - - '*' + - apiGroups: [""] + resources: ["secrets"] + verbs: ["list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 From c5be50e5cc8db9173e5941c2fc34ced007015314 Mon Sep 17 00:00:00 2001 From: haseeb Date: Thu, 25 Sep 2025 22:04:53 +0530 Subject: [PATCH 3/5] removing secretstore-gen-secrets helm chart --- .../nautobot-secretstore-gen-secrets.yaml | 13 - apps/site/nautobot-site.yaml | 5 + .../secretstore-gen-secrets/.helmignore | 23 -- components/secretstore-gen-secrets/Chart.yaml | 6 - .../templates/secrets.yaml.tmpl | 54 ----- .../secretstore-gen-secrets/values.yaml | 1 - .../deploy-guide/generate-external-secrets.md | 121 ---------- ...ponent-secretstore-gen-secrets.schema.json | 226 ------------------ mkdocs.yml | 1 - .../k8s-secret-nautobot-token.yaml | 2 +- 10 files changed, 6 insertions(+), 446 deletions(-) delete mode 100644 apps/site/nautobot-secretstore-gen-secrets.yaml create mode 100644 apps/site/nautobot-site.yaml delete mode 100644 components/secretstore-gen-secrets/.helmignore delete mode 100644 components/secretstore-gen-secrets/Chart.yaml delete mode 100644 components/secretstore-gen-secrets/templates/secrets.yaml.tmpl delete mode 100644 components/secretstore-gen-secrets/values.yaml delete mode 100644 docs/deploy-guide/generate-external-secrets.md delete mode 100644 docs/schema/component-secretstore-gen-secrets.schema.json diff --git a/apps/site/nautobot-secretstore-gen-secrets.yaml b/apps/site/nautobot-secretstore-gen-secrets.yaml deleted file mode 100644 index f0466d27b..000000000 --- a/apps/site/nautobot-secretstore-gen-secrets.yaml +++ /dev/null @@ -1,13 +0,0 @@ ---- -component: nautobot-secrets -componentNamespace: nautobot -sources: - - ref: understack - path: 'components/secretstore-gen-secrets' - helm: - releaseName: nautobot-secrets - valueFiles: - - $deploy/{{.name}}/helm-configs/secretstore-nautobot-secrets.yaml - ignoreMissingValueFiles: true - - ref: deploy - path: '{{.name}}/manifests/secret-store' diff --git a/apps/site/nautobot-site.yaml b/apps/site/nautobot-site.yaml new file mode 100644 index 000000000..cb34140e5 --- /dev/null +++ b/apps/site/nautobot-site.yaml @@ -0,0 +1,5 @@ +--- +component: nautobot +sources: + - ref: deploy + path: '{{.name}}/manifests/nautobot' diff --git a/components/secretstore-gen-secrets/.helmignore b/components/secretstore-gen-secrets/.helmignore deleted file mode 100644 index 0e8a0eb36..000000000 --- a/components/secretstore-gen-secrets/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ diff --git a/components/secretstore-gen-secrets/Chart.yaml b/components/secretstore-gen-secrets/Chart.yaml deleted file mode 100644 index c72feede5..000000000 --- a/components/secretstore-gen-secrets/Chart.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v2 -name: secretstore-gen-secrets -description: Secret store backed External Secrets generator -type: application -version: 0.1.0 -appVersion: "1.0" diff --git a/components/secretstore-gen-secrets/templates/secrets.yaml.tmpl b/components/secretstore-gen-secrets/templates/secrets.yaml.tmpl deleted file mode 100644 index 6f071565b..000000000 --- a/components/secretstore-gen-secrets/templates/secrets.yaml.tmpl +++ /dev/null @@ -1,54 +0,0 @@ -{{- $secretStore := .Values.secretStore }} -{{- range .Values.secrets }} ---- -apiVersion: external-secrets.io/v1 -kind: ExternalSecret -metadata: - name: {{ .name }} - {{- if .externalLinkAnnotationTemplate }} - annotations: - link.argocd.argoproj.io/external-link: {{ tpl .externalLinkAnnotationTemplate . }} - {{- end }} - {{- with .labels }} - labels: -{{ toYaml . | indent 4 }} -{{- end }} -spec: - refreshInterval: {{ .refreshInterval | default "1h" }} - secretStoreRef: - kind: {{ $secretStore.kind }} - name: {{ $secretStore.name }} - target: - name: {{ .name }} - creationPolicy: Owner - template: - engineVersion: v2 - type: {{ .templateType | default "Opaque" }} - {{- with .labels }} - metadata: - labels: -{{ toYaml . | indent 10 }} - {{- end }} -{{- if .templateData }} - data: -{{- range $k, $v := .templateData }} - {{ $k }}: {{ $v | quote }} -{{- end }} -{{- end }} -{{- if .data }} - data: -{{- range .data }} - - secretKey: {{ .secretKey }} - remoteRef: - key: {{ .remoteRef.key }} - property: {{ .remoteRef.property }} - conversionStrategy: {{ .remoteRef.conversionStrategy | default "Default" }} - decodingStrategy: {{ .remoteRef.decodingStrategy | default "None" }} - metadataPolicy: {{ .remoteRef.metadataPolicy | default "None" }} -{{- end }} -{{- end }} -{{- if .dataFrom }} - dataFrom: -{{- toYaml .dataFrom | nindent 4 }} -{{- end }} -{{- end }} diff --git a/components/secretstore-gen-secrets/values.yaml b/components/secretstore-gen-secrets/values.yaml deleted file mode 100644 index ed97d539c..000000000 --- a/components/secretstore-gen-secrets/values.yaml +++ /dev/null @@ -1 +0,0 @@ ---- diff --git a/docs/deploy-guide/generate-external-secrets.md b/docs/deploy-guide/generate-external-secrets.md deleted file mode 100644 index 6a1d4faaf..000000000 --- a/docs/deploy-guide/generate-external-secrets.md +++ /dev/null @@ -1,121 +0,0 @@ -# External Secrets Guide - -This guide explains how to automate Kubernetes secrets creation using -external secret stores (Vault, AWS Secrets Manager, etc.) and ArgoCD -components in the UnderStack environment. - ------------------------------------------------------------------------- - -## TL;DR Quick Steps - -1. **Assumption**: Credentials already exist in an external system - (Vault, AWS Secrets Manager, PasswordSafe, etc.). -2. **Install SecretStore** in the desired Kubernetes namespace. -3. **Create ArgoCD component** in [apps/site/](https://github.com/rackerlabs/understack/tree/main/apps/site) - to manage SecretStore + ExternalSecrets. -4. **Configure `values.yaml`** to define secret templates. -5. **Apply ArgoCD application** to deploy SecretStore and - ExternalSecrets. - ------------------------------------------------------------------------- - -## Assumptions - -- Credentials are managed in external systems (Vault, AWS Secrets - Manager, PasswordSafe). -- Secrets must be pulled automatically into Kubernetes clusters. - ------------------------------------------------------------------------- - -## Workflow - -1. **Create an External SecretStore per Kubernetes namespace**. - This links Kubernetes to the external backend (Vault, AWS Secrets - Manager, etc.). - -2. **Create an ArgoCD component** inside the site repo: - Example: - [nautobot-secretstore-gen-secrets.yaml](https://raw.githubusercontent.com/rackerlabs/understack/refs/heads/main/apps/site/nautobot-secretstore-gen-secrets.yaml) - - This ArgoCD component performs two tasks: - - - Installs the SecretStore. - - Creates ExternalSecrets referencing the SecretStore. - -3. **Deploy via Kustomize/Helm** - - - One Helm chart installs the SecretStore pointing to the external - backend system. - - Another Helm chart creates the ExternalSecrets from that - SecretStore. - -4. **Configure `values.yaml`** with templates for secrets. - ------------------------------------------------------------------------- - -## Example ArgoCD Component - -``` yaml ---- -component: nautobot-secrets -componentNamespace: nautobot -sources: - - ref: understack - path: 'components/secretstore-gen-secrets' - helm: - releaseName: nautobot-secrets - valueFiles: - - $deploy/{{.name}}/helm-configs/secretstore-nautobot-secrets.yaml - ignoreMissingValueFiles: true - - ref: deploy - path: '{{.name}}/manifests/secret-store' -``` - ------------------------------------------------------------------------- - -## Sample values.yaml - -``` yaml -# yaml-language-server: $schema=https://rackerlabs.understack.io/schema/component-secretstore-gen-secrets.schema.json ---- -secretStore: - kind: SecretStore - name: pwsafe - -secrets: - - name: site1-token - externalLinkAnnotationTemplate: "https://secretvault.example.com/credentials/373525" - templateData: - hostname: "{{ .hostname }}" - username: "{{ .username }}" - password: "{{ .password }}" - token: "{{ .token }}" - dataFrom: - - extract: - key: "373525" - - - name: site2-token - externalLinkAnnotationTemplate: "https://secretvault.example.com/credentials/373539" - labels: - token/type: mycustomlabel - templateData: - hostname: "{{ .hostname }}" - username: "{{ .username }}" - password: "{{ .password }}" - token: "{{ .token }}" - dataFrom: - - extract: - key: "373539" -``` - ------------------------------------------------------------------------- - -## Summary - -- **External systems** (Vault, AWS Secrets Manager, PasswordSafe) hold - credentials. -- **SecretStore** bridges Kubernetes to the external backend. -- **ExternalSecrets** define the actual K8s secrets created from - external data. -- **ArgoCD components** ensure consistent automation of this workflow - across namespaces. diff --git a/docs/schema/component-secretstore-gen-secrets.schema.json b/docs/schema/component-secretstore-gen-secrets.schema.json deleted file mode 100644 index 24997bf31..000000000 --- a/docs/schema/component-secretstore-gen-secrets.schema.json +++ /dev/null @@ -1,226 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Secret store backed External Secrets generator Helm Chart Values", - "description": "Schema for Secret store backed secrets generator Helm chart values.yaml configuration", - "type": "object", - "properties": { - "secretStore": { - "type": "object", - "description": "Secret store backed secrets generator Secret Store configuration", - "properties": { - "kind": { - "type": "string", - "enum": ["ClusterSecretStore", "SecretStore"], - "description": "Type of secret store - ClusterSecretStore or SecretStore for namespaced" - }, - "name": { - "type": "string", - "description": "Name of the ClusterSecretStore or SecretStore to use" - } - }, - "required": ["kind", "name"], - "additionalProperties": false - }, - "secrets": { - "type": "array", - "description": "Array of External Secret configurations", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of the ExternalSecret and target Secret resource" - }, - "externalLinkAnnotationTemplate": { - "type": "string", - "description": "Optional template for external link annotations on ExternalSecret resources" - }, - "labels": { - "type": "object", - "description": "Labels to apply to both ExternalSecret and target Secret", - "additionalProperties": { - "type": "string" - } - }, - "refreshInterval": { - "type": "string", - "pattern": "^[0-9]+(s|m|h|d)$", - "default": "1h", - "description": "How often Secret store backed secrets generator refreshes this secret" - }, - "templateType": { - "type": "string", - "default": "Opaque", - "description": "Kubernetes secret type for the target secret", - "enum": ["Opaque", "kubernetes.io/tls", "kubernetes.io/ssh-auth", "kubernetes.io/basic-auth", "kubernetes.io/dockerconfigjson", "kubernetes.io/service-account-token"] - }, - "templateData": { - "type": "object", - "description": "Template data for constructing the target secret using Go templating", - "additionalProperties": { - "type": "string" - } - }, - "data": { - "type": "array", - "description": "Individual data mappings from remote secrets to target secret keys", - "items": { - "type": "object", - "properties": { - "secretKey": { - "type": "string", - "description": "Key name in the target Kubernetes secret" - }, - "remoteRef": { - "type": "object", - "description": "Reference to the remote secret", - "properties": { - "key": { - "type": "string", - "description": "Key/ID of the remote secret" - }, - "property": { - "type": "string", - "description": "Property/field within the remote secret" - }, - "conversionStrategy": { - "type": "string", - "default": "Default", - "enum": ["Default", "Unicode"], - "description": "Strategy for converting the secret value" - }, - "decodingStrategy": { - "type": "string", - "default": "None", - "enum": ["None", "Base64", "Base64URL", "Auto"], - "description": "Strategy for decoding the secret value" - }, - "metadataPolicy": { - "type": "string", - "default": "None", - "enum": ["None", "Fetch"], - "description": "Policy for handling secret metadata" - } - }, - "required": ["key"], - "additionalProperties": false - } - }, - "required": ["secretKey", "remoteRef"], - "additionalProperties": false - } - }, - "dataFrom": { - "type": "array", - "description": "Bulk data extraction from remote secrets", - "items": { - "type": "object", - "properties": { - "extract": { - "type": "object", - "description": "Extract configuration for bulk secret retrieval", - "properties": { - "key": { - "type": "string", - "description": "Key/ID of the remote secret to extract from" - }, - "property": { - "type": "string", - "description": "Specific property to extract (optional)" - }, - "conversionStrategy": { - "type": "string", - "default": "Default", - "enum": ["Default", "Unicode"], - "description": "Strategy for converting the secret values" - }, - "decodingStrategy": { - "type": "string", - "default": "None", - "enum": ["None", "Base64", "Base64URL", "Auto"], - "description": "Strategy for decoding the secret values" - }, - "metadataPolicy": { - "type": "string", - "default": "None", - "enum": ["None", "Fetch"], - "description": "Policy for handling secret metadata" - } - }, - "required": ["key"], - "additionalProperties": false - }, - "find": { - "type": "object", - "description": "Find configuration for pattern-based secret discovery", - "properties": { - "path": { - "type": "string", - "description": "Path pattern to search for secrets" - }, - "name": { - "type": "object", - "description": "Name pattern configuration", - "properties": { - "regexp": { - "type": "string", - "description": "Regular expression for matching secret names" - } - }, - "additionalProperties": false - }, - "tags": { - "type": "object", - "description": "Tag filters for finding secrets", - "additionalProperties": { - "type": "string" - } - } - }, - "additionalProperties": false - }, - "sourceRef": { - "type": "object", - "description": "Reference to a different secret store for this data source", - "properties": { - "storeRef": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of the secret store" - }, - "kind": { - "type": "string", - "enum": ["ClusterSecretStore", "SecretStore"], - "description": "Kind of secret store" - } - }, - "required": ["name", "kind"], - "additionalProperties": false - } - }, - "additionalProperties": false - } - }, - "anyOf": [ - {"required": ["extract"]}, - {"required": ["find"]} - ], - "additionalProperties": false - } - } - }, - "required": ["name"], - "anyOf": [ - {"required": ["data"]}, - {"required": ["dataFrom"]}, - {"required": ["templateData"]} - ], - "additionalProperties": false - } - } - }, - "required": ["secretStore", "secrets"], - "additionalProperties": false -} diff --git a/mkdocs.yml b/mkdocs.yml index d07224a41..89934170a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -134,7 +134,6 @@ nav: - deploy-guide/config-dex.md - deploy-guide/config-openstack.md - deploy-guide/secrets-eso-setup.md - - deploy-guide/generate-external-secrets.md - deploy-guide/openstack-svc-users.md - deploy-guide/auth.md - deploy-guide/config-argo-workflows.md diff --git a/workflows/nautobot/eventsources/k8s-secret-nautobot-token.yaml b/workflows/nautobot/eventsources/k8s-secret-nautobot-token.yaml index 6d549c64b..8a1e34ad8 100644 --- a/workflows/nautobot/eventsources/k8s-secret-nautobot-token.yaml +++ b/workflows/nautobot/eventsources/k8s-secret-nautobot-token.yaml @@ -12,7 +12,7 @@ spec: version: v1 eventTypes: - ADD -# - UPDATE + - UPDATE filter: labels: - key: token/type From b617687c29fd0bfaad14938554209d78d5442b67 Mon Sep 17 00:00:00 2001 From: haseeb Date: Fri, 26 Sep 2025 16:26:28 +0530 Subject: [PATCH 4/5] using service specific tokens --- components/nautobot/secretstore-nautobot.yaml | 3 +++ workflows/argo-events/secrets/nautobot-token.yaml | 4 ++-- workflows/openstack/secrets/nautobot-token.yaml | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/components/nautobot/secretstore-nautobot.yaml b/components/nautobot/secretstore-nautobot.yaml index b287a4fff..aebfc0252 100644 --- a/components/nautobot/secretstore-nautobot.yaml +++ b/components/nautobot/secretstore-nautobot.yaml @@ -26,6 +26,9 @@ rules: - watch resourceNames: - nautobot-superuser + - ansible-token + - openstack-token + - workflow-token - apiGroups: - authorization.k8s.io resources: diff --git a/workflows/argo-events/secrets/nautobot-token.yaml b/workflows/argo-events/secrets/nautobot-token.yaml index d1489093b..9c40c65eb 100644 --- a/workflows/argo-events/secrets/nautobot-token.yaml +++ b/workflows/argo-events/secrets/nautobot-token.yaml @@ -21,8 +21,8 @@ spec: data: - secretKey: token remoteRef: - key: nautobot-superuser - property: apitoken + key: workflow-token + property: token # necessary to avoid argoproj/argo-cd#13004 conversionStrategy: Default decodingStrategy: None diff --git a/workflows/openstack/secrets/nautobot-token.yaml b/workflows/openstack/secrets/nautobot-token.yaml index d4d7b2978..621ac5188 100644 --- a/workflows/openstack/secrets/nautobot-token.yaml +++ b/workflows/openstack/secrets/nautobot-token.yaml @@ -21,8 +21,8 @@ spec: data: - secretKey: token remoteRef: - key: nautobot-superuser - property: apitoken + key: openstack-token + property: token # necessary to avoid argoproj/argo-cd#13004 conversionStrategy: Default decodingStrategy: None From a59a97564649590ac5328c13020d3a52e19fc7e7 Mon Sep 17 00:00:00 2001 From: haseeb Date: Fri, 26 Sep 2025 19:23:14 +0530 Subject: [PATCH 5/5] undersync specific nautobot token --- ansible/roles/users/library/nautobot_token.py | 49 ++++++++----------- components/nautobot/secretstore-nautobot.yaml | 1 + components/undersync/nautobot-token.yaml | 28 +++++++++++ 3 files changed, 49 insertions(+), 29 deletions(-) create mode 100644 components/undersync/nautobot-token.yaml diff --git a/ansible/roles/users/library/nautobot_token.py b/ansible/roles/users/library/nautobot_token.py index 11b2707b7..40c5b1259 100644 --- a/ansible/roles/users/library/nautobot_token.py +++ b/ansible/roles/users/library/nautobot_token.py @@ -2,23 +2,24 @@ import requests -def get_existing_token(base_url, username, password, user_token): - """Return the token dict if it exists, otherwise None.""" +def get_existing_token(base_url, username, password, user_token, module): headers = {"Accept": "application/json"} tokens_url = f"{base_url}/api/users/tokens/" try: response = requests.get(tokens_url, headers=headers, auth=(username, password)) response.raise_for_status() - except requests.exceptions.RequestException: - return None + except requests.exceptions.RequestException as e: + module.fail_json( + msg=f"Failed to fetch existing tokens for user {username}: {str(e)}" + ) tokens = response.json().get("results", []) return next((t for t in tokens if t.get("key") == user_token), None) -def create_new_token(base_url, username, password, user_token, description): - """Create a new Nautobot token using Basic Auth. Returns the token dict or None.""" +def create_new_token(base_url, username, password, user_token, description, module): + """Create a new Nautobot token using Basic Auth.""" tokens_url = f"{base_url}/api/users/tokens/" headers = {"Content-Type": "application/json", "Accept": "application/json"} payload = {"key": user_token, "description": description, "write_enabled": True} @@ -28,26 +29,14 @@ def create_new_token(base_url, username, password, user_token, description): tokens_url, headers=headers, json=payload, auth=(username, password) ) response.raise_for_status() - except requests.exceptions.RequestException: - return None + except requests.exceptions.RequestException as e: + module.fail_json( + msg=f"Failed to create new token for user {username}: {str(e)}" + ) return response.json() -def format_token_response(token): - """Normalize token dict fields for output.""" - if not token: - return None - return { - "id": str(token.get("id")), - "display": str(token.get("display")), - "created": str(token.get("created")), - "expires": str(token.get("expires")), - "write_enabled": bool(token.get("write_enabled")), - "description": str(token.get("description", "No description")), - } - - def run_module(): module_args = dict( base_url=dict(type="str", required=True), @@ -66,23 +55,25 @@ def run_module(): token_description = module.params["token_description"] # fetch existing token - token = get_existing_token(base_url, username, password, user_token) + token = get_existing_token(base_url, username, password, user_token, module) if token: module.exit_json( changed=False, - message=f"Found existing token for {username}", - token=format_token_response(token), + username=username, + message=f"Found existing Nautobot token for user {username}", ) # No token found → try creating new - new_token = create_new_token(base_url, username, password, user_token, token_description) + new_token = create_new_token( + base_url, username, password, user_token, token_description, module + ) if not new_token: - module.fail_json(msg="Failed to create new token") + module.fail_json(msg=f"Failed to create new token for user {username}") module.exit_json( changed=True, - message=f"No token found, created new token for {username}", - token=format_token_response(new_token), + username=username, + message=f"No token found, created new Nautobot token for user {username}", ) diff --git a/components/nautobot/secretstore-nautobot.yaml b/components/nautobot/secretstore-nautobot.yaml index aebfc0252..c3428e823 100644 --- a/components/nautobot/secretstore-nautobot.yaml +++ b/components/nautobot/secretstore-nautobot.yaml @@ -29,6 +29,7 @@ rules: - ansible-token - openstack-token - workflow-token + - undersync-token - apiGroups: - authorization.k8s.io resources: diff --git a/components/undersync/nautobot-token.yaml b/components/undersync/nautobot-token.yaml new file mode 100644 index 000000000..c9f7fb283 --- /dev/null +++ b/components/undersync/nautobot-token.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: nautobot-token +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: nautobot + target: + name: nautobot-token + creationPolicy: Owner + deletionPolicy: Delete + template: + engineVersion: v2 + data: + token: "{{ .token }}" + bearer_token: "Token {{ .token }}" + data: + - secretKey: token + remoteRef: + key: undersync-token + property: token + # necessary to avoid argoproj/argo-cd#13004 + conversionStrategy: Default + decodingStrategy: None + metadataPolicy: None