Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/dictionary/en-custom.txt
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,7 @@ redhat
refspec
regexes
repo
repo's
repos
rgw
rhel
Expand Down
12 changes: 12 additions & 0 deletions roles/kustomize_deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@ resolution:
2. **Generate** — a new random value is produced (respecting the special-keys
format rules). Keys in the skip list are never touched.

### libvirt-secret randomization

The architecture repo's `lib/dataplane/nodeset` component generates a
`libvirt-secret` Secret from a hardcoded
[`libvirt-secret.env`](https://github.com/openstack-k8s-operators/architecture/blob/main/lib/dataplane/nodeset/libvirt-secret.env)
file (`LibvirtPassword=12345678`). After each `kustomize build`, if the
rendered dataplane manifest contains a `libvirt-secret` Secret, its data
keys are randomized before `oc apply` using the same two-tier resolution as
`osp-secret` (live cluster value takes priority over a freshly generated
20-char alphanumeric password). This is a no-op for stages that do not
produce a `libvirt-secret` (e.g. control-plane, operators).

## Automation specificities

### Timeouts
Expand Down
92 changes: 56 additions & 36 deletions roles/kustomize_deploy/files/osp_secret_manifest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
#!/usr/bin/env python3
"""Helpers for osp-secret keys in kustomize-built manifests."""
"""Helpers for Secret keys in kustomize-built manifests.

Originally written for ``osp-secret`` (the default target of every
function/command below), the same helpers also operate on other
single-Secret manifests such as ``libvirt-secret`` by passing an explicit
``secret_name``.
"""

import base64
import json
Expand All @@ -17,26 +23,31 @@ def load_docs(path):
return [doc for doc in yaml.safe_load_all(handle) if doc is not None]


def find_osp_secret(docs):
# Each kustomize output is expected to contain at most one osp-secret.
def find_secret(docs, secret_name=OSP_SECRET_NAME):
# Each kustomize output is expected to contain at most one Secret
# with a given name (e.g. osp-secret, libvirt-secret).
for doc in docs:
if doc.get("kind") != "Secret":
continue
if doc.get("metadata", {}).get("name") != OSP_SECRET_NAME:
if doc.get("metadata", {}).get("name") != secret_name:
continue
return doc
return None


def get_secret_namespace(docs):
secret = find_osp_secret(docs)
def find_osp_secret(docs):
return find_secret(docs, OSP_SECRET_NAME)


def get_secret_namespace(docs, secret_name=OSP_SECRET_NAME):
secret = find_secret(docs, secret_name)
if secret is None:
return None
return secret.get("metadata", {}).get("namespace")


def get_secret_key(docs, key):
secret = find_osp_secret(docs)
def get_secret_key(docs, key, secret_name=OSP_SECRET_NAME):
secret = find_secret(docs, secret_name)
if secret is None:
return None
data = secret.get("data", {})
Expand All @@ -45,8 +56,8 @@ def get_secret_key(docs, key):
return base64.b64decode(data[key]).decode()


def apply_secret_keys(docs, keys):
secret = find_osp_secret(docs)
def apply_secret_keys(docs, keys, secret_name=OSP_SECRET_NAME):
secret = find_secret(docs, secret_name)
if secret is None:
return False, []
data = secret.setdefault("data", {})
Expand All @@ -70,15 +81,15 @@ def generate_hex_key(byte_length):
return secrets.token_hex(byte_length)


def randomize_secret_keys(docs, config):
"""Replace osp-secret data values with random ones.
def randomize_secret_keys(docs, config, secret_name=OSP_SECRET_NAME):
"""Replace a Secret's data values with random ones.

``config`` is a dict with optional keys:
cluster_values - dict of key->plaintext from the live cluster
skip_keys - list of keys to leave untouched
special_keys - dict of key->{type, length} for non-password formats
"""
secret = find_osp_secret(docs)
secret = find_secret(docs, secret_name)
if secret is None:
return False, []

Expand Down Expand Up @@ -114,28 +125,28 @@ def randomize_secret_keys(docs, config):
return bool(changed_keys), changed_keys


def cmd_has(path):
# Exit codes: 0 = osp-secret found, 1 = not found, 2 = error while
def cmd_has(path, secret_name=OSP_SECRET_NAME):
# Exit codes: 0 = secret found, 1 = not found, 2 = error while
# reading/parsing the manifest. Callers must not treat 2 the same as 1:
# a parse failure should never be silently mistaken for "nothing to do".
try:
docs = load_docs(path)
except Exception as exc:
sys.stderr.write("error: failed to load manifest {}: {}\n".format(path, exc))
sys.exit(2)
secret = find_osp_secret(docs)
secret = find_secret(docs, secret_name)
sys.exit(0 if secret else 1)


def cmd_get(path, key):
value = get_secret_key(load_docs(path), key)
def cmd_get(path, key, secret_name=OSP_SECRET_NAME):
value = get_secret_key(load_docs(path), key, secret_name)
if value is None:
sys.exit(2)
sys.stdout.write(value)


def cmd_get_namespace(path):
namespace = get_secret_namespace(load_docs(path))
def cmd_get_namespace(path, secret_name=OSP_SECRET_NAME):
namespace = get_secret_namespace(load_docs(path), secret_name)
if not namespace:
sys.exit(2)
sys.stdout.write(namespace)
Expand All @@ -146,21 +157,21 @@ def load_keys(keys_path):
return json.load(handle)


def cmd_set(path, keys_path):
def cmd_set(path, keys_path, secret_name=OSP_SECRET_NAME):
docs = load_docs(path)
keys = load_keys(keys_path)
changed, changed_keys = apply_secret_keys(docs, keys)
changed, changed_keys = apply_secret_keys(docs, keys, secret_name)
if changed:
with open(path, "w") as handle:
yaml.dump_all(docs, handle, default_flow_style=False)
for key in changed_keys:
print("Set: {}".format(key))


def cmd_randomize(path, config_path):
def cmd_randomize(path, config_path, secret_name=OSP_SECRET_NAME):
docs = load_docs(path)
config = load_keys(config_path)
changed, changed_keys = randomize_secret_keys(docs, config)
changed, changed_keys = randomize_secret_keys(docs, config, secret_name)
if changed:
with open(path, "w") as handle:
yaml.dump_all(docs, handle, default_flow_style=False)
Expand All @@ -172,28 +183,37 @@ def main():
if len(sys.argv) < 3:
sys.exit(
"usage: osp_secret_manifest.py"
" <has|get|get-namespace|set|randomize> <path> [args]"
" <has|get|get-namespace|set|randomize> <path> [args] [secret-name]"
)
command = sys.argv[1]
path = sys.argv[2]
if command == "has":
cmd_has(path)
secret_name = sys.argv[3] if len(sys.argv) > 3 else OSP_SECRET_NAME
cmd_has(path, secret_name)
elif command == "get":
if len(sys.argv) != 4:
sys.exit("usage: osp_secret_manifest.py get <path> <key>")
cmd_get(path, sys.argv[3])
if len(sys.argv) not in (4, 5):
sys.exit("usage: osp_secret_manifest.py get <path> <key> [secret-name]")
secret_name = sys.argv[4] if len(sys.argv) == 5 else OSP_SECRET_NAME
cmd_get(path, sys.argv[3], secret_name)
elif command == "get-namespace":
cmd_get_namespace(path)
secret_name = sys.argv[3] if len(sys.argv) > 3 else OSP_SECRET_NAME
cmd_get_namespace(path, secret_name)
elif command == "set":
if len(sys.argv) != 4:
sys.exit("usage: osp_secret_manifest.py set <path> <keys-json-file>")
cmd_set(path, sys.argv[3])
if len(sys.argv) not in (4, 5):
sys.exit(
"usage: osp_secret_manifest.py"
" set <path> <keys-json-file> [secret-name]"
)
secret_name = sys.argv[4] if len(sys.argv) == 5 else OSP_SECRET_NAME
cmd_set(path, sys.argv[3], secret_name)
elif command == "randomize":
if len(sys.argv) != 4:
if len(sys.argv) not in (4, 5):
sys.exit(
"usage: osp_secret_manifest.py randomize <path> <config-json-file>"
"usage: osp_secret_manifest.py"
" randomize <path> <config-json-file> [secret-name]"
)
cmd_randomize(path, sys.argv[3])
secret_name = sys.argv[4] if len(sys.argv) == 5 else OSP_SECRET_NAME
cmd_randomize(path, sys.argv[3], secret_name)
else:
sys.exit("unknown command: {}".format(command))

Expand Down
5 changes: 5 additions & 0 deletions roles/kustomize_deploy/tasks/execute_step.yml
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,11 @@
vars:
cifmw_kustomize_deploy_manifest_path: "{{ _output }}"

- name: Ensure libvirt-secret password is randomized in kustomize output
ansible.builtin.include_tasks: inject_libvirt_secret_key.yml
vars:
cifmw_kustomize_deploy_manifest_path: "{{ _output }}"

- name: "Store kustomized content in artifacts for {{ stage.path }}"
ansible.builtin.copy:
remote_src: true
Expand Down
137 changes: 137 additions & 0 deletions roles/kustomize_deploy/tasks/inject_libvirt_secret_key.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
---
# Copyright Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

# Randomize the libvirt-secret (LibvirtPassword) coming from the
# architecture repo's lib/dataplane/nodeset libvirt-secret.env, which
# hardcodes a well-known default password. Only present in the dataplane
# manifest, so this is a no-op for every other stage.

- name: Ensure libvirt-secret password is randomized in kustomize manifest
vars:
_libvirt_secret_name: libvirt-secret
_nolog: "{{ cifmw_nolog | default(true) | bool }}"
block:
- name: Check whether kustomize output contains libvirt-secret
ansible.builtin.command:
argv:
- python3
- "{{ role_path }}/files/osp_secret_manifest.py"
- has
- "{{ cifmw_kustomize_deploy_manifest_path }}"
- "{{ _libvirt_secret_name }}"
register: _libvirt_secret_manifest_check
changed_when: false
# rc 0 = found, rc 1 = not found (both fine, handled below). Any other
# rc means the manifest could not be read/parsed: fail loudly instead
# of silently skipping randomization of a possibly-default password.
failed_when: _libvirt_secret_manifest_check.rc not in [0, 1]

- name: Randomize libvirt-secret password
when: _libvirt_secret_manifest_check.rc == 0
block:
- name: Read libvirt-secret namespace from kustomize manifest
ansible.builtin.command:
argv:
- python3
- "{{ role_path }}/files/osp_secret_manifest.py"
- get-namespace
- "{{ cifmw_kustomize_deploy_manifest_path }}"
- "{{ _libvirt_secret_name }}"
register: _manifest_libvirt_secret_namespace
changed_when: false
failed_when: false

- name: Set libvirt-secret namespace for cluster lookup
ansible.builtin.set_fact:
_libvirt_secret_namespace: >-
{{
_manifest_libvirt_secret_namespace.stdout
if _manifest_libvirt_secret_namespace.rc == 0 and
(_manifest_libvirt_secret_namespace.stdout | length > 0)
else cifmw_openstack_namespace
}}

- name: Get existing libvirt-secret from cluster
when: not cifmw_kustomize_deploy_generate_crs_only | bool
kubernetes.core.k8s_info:
kubeconfig: "{{ cifmw_openshift_kubeconfig }}"
api_key: "{{ cifmw_openshift_token | default(omit) }}"
context: "{{ cifmw_openshift_context | default(omit) }}"
api_version: v1
kind: Secret
name: "{{ _libvirt_secret_name }}"
namespace: "{{ _libvirt_secret_namespace }}"
register: _existing_libvirt_secret
no_log: "{{ _nolog }}"

- name: Build cluster secret values dict for libvirt-secret randomizer
vars:
_raw_data: >-
{{
_existing_libvirt_secret.resources[0].data
if (_existing_libvirt_secret.resources | default([]) | length > 0)
else {}
}}
ansible.builtin.set_fact:
_cluster_values_for_libvirt_randomize: >-
{% set result = {} -%}
{% for key, val in _raw_data.items() -%}
{% set _ = result.update({key: val | b64decode}) -%}
{% endfor -%}
{{ result }}
no_log: "{{ _nolog }}"

- name: Create temporary libvirt-secret randomize config file
ansible.builtin.tempfile:
state: file
suffix: .libvirt-secret-randomize.json
register: _libvirt_randomize_config_file
no_log: "{{ _nolog }}"

- name: Write config and randomize libvirt-secret key, always removing the temp file
block:
- name: Write libvirt-secret randomize config
ansible.builtin.copy:
dest: "{{ _libvirt_randomize_config_file.path }}"
content: >-
{{
{
"cluster_values": _cluster_values_for_libvirt_randomize
} | to_json
}}
mode: "0600"
no_log: "{{ _nolog }}"

- name: Randomize libvirt-secret key in kustomize manifest
ansible.builtin.command:
argv:
- python3
- "{{ role_path }}/files/osp_secret_manifest.py"
- randomize
- "{{ cifmw_kustomize_deploy_manifest_path }}"
- "{{ _libvirt_randomize_config_file.path }}"
- "{{ _libvirt_secret_name }}"
register: _randomize_libvirt_secret
changed_when: "'Randomized:' in _randomize_libvirt_secret.stdout"
no_log: "{{ _nolog }}"
always:
# Ensures the plaintext cluster secret never lingers on disk,
# even if the write or the randomize command above fails.
- name: Remove temporary libvirt-secret randomize config file
ansible.builtin.file:
path: "{{ _libvirt_randomize_config_file.path }}"
state: absent
no_log: "{{ _nolog }}"
Loading
Loading