From 664c3c8236e759c199d37232784e76dac8dca7e1 Mon Sep 17 00:00:00 2001 From: swaphb Date: Sat, 15 Aug 2026 16:40:37 -0400 Subject: [PATCH] add Ansible Flatcar and platform bootstrap --- TODO.md | 5 + ansible/README.md | 78 ++++++++- ansible/bootstrap-flatcar-k3d.yml | 22 +++ ansible/inventory.flatcar-k3d.example.yml | 12 ++ ansible/requirements.yml | 3 + .../roles/argocd_bootstrap/defaults/main.yml | 6 + ansible/roles/argocd_bootstrap/tasks/main.yml | 32 ++++ .../roles/cilium_bootstrap/defaults/main.yml | 7 + ansible/roles/cilium_bootstrap/tasks/main.yml | 48 ++++++ .../roles/flatcar_k3d_host/defaults/main.yml | 23 +++ ansible/roles/flatcar_k3d_host/tasks/main.yml | 149 ++++++++++++++++++ 11 files changed, 379 insertions(+), 6 deletions(-) create mode 100644 ansible/bootstrap-flatcar-k3d.yml create mode 100644 ansible/inventory.flatcar-k3d.example.yml create mode 100644 ansible/requirements.yml create mode 100644 ansible/roles/argocd_bootstrap/defaults/main.yml create mode 100644 ansible/roles/argocd_bootstrap/tasks/main.yml create mode 100644 ansible/roles/cilium_bootstrap/defaults/main.yml create mode 100644 ansible/roles/cilium_bootstrap/tasks/main.yml create mode 100644 ansible/roles/flatcar_k3d_host/defaults/main.yml create mode 100644 ansible/roles/flatcar_k3d_host/tasks/main.yml diff --git a/TODO.md b/TODO.md index e28ac9f..9e78cd9 100644 --- a/TODO.md +++ b/TODO.md @@ -6,6 +6,11 @@ committed to Git, stored in Terraform state, or printed in CI logs. ## Priority 1: Ansible-driven bootstrap +Implementation is now present in `ansible/bootstrap-flatcar-k3d.yml` with +Flatcar no-Python compatibility. The standard path was run successfully on the +dedicated Flatcar k3d host. The optional Cilium path has syntax validation but +still needs a clean kube-proxy-free cluster validation. + - [ ] Add an Ansible role for post-provision Flatcar bootstrap. - [ ] Install or configure the Docker, k3d, kubectl, Helm, and Git tools required by the selected host profile. diff --git a/ansible/README.md b/ansible/README.md index f651c3a..7d13533 100644 --- a/ansible/README.md +++ b/ansible/README.md @@ -1,9 +1,76 @@ -# Optional Ansible bootstrap +# Ansible bootstrap paths -The playbook applies a small host baseline (AppArmor, unattended security -updates, swap removal, and SSH hardening) and installs a pinned K3s server. It -is intentionally separate from the local k3d path so the portfolio can be -run without cloud credentials. +The repository has two separate Ansible paths: + +- `bootstrap-k3s.yml` configures a production-style Flatcar K3s server. +- `bootstrap-flatcar-k3d.yml` configures the dedicated Flatcar k3d test host, + bootstraps ArgoCD, and optionally installs Cilium. + +The Flatcar k3d path is idempotent and keeps the standard profile as the +default. Cilium is enabled only when the inventory explicitly sets +`cilium_enabled: true` and selects a kube-proxy-free cluster configuration. + +Flatcar does not include Python in the immutable host OS. The role therefore +uses Ansible `raw` tasks for host operations and `ansible.posix.synchronize` +for repository and kubeconfig transfer. Install the collection before use: + +```bash +ansible-galaxy collection install -r ansible/requirements.yml +``` + +## Flatcar k3d bootstrap + +Copy the example inventory and replace the host, SSH key, and optional API +endpoint values: + +```bash +cp ansible/inventory.flatcar-k3d.example.yml ansible/inventory.flatcar-k3d.yml +ansible-playbook \ + -i ansible/inventory.flatcar-k3d.yml \ + ansible/bootstrap-flatcar-k3d.yml +``` + +The playbook installs pinned k3d, kubectl, and Helm binaries, starts Docker, +creates the k3d cluster, writes the core user's kubeconfig, fetches a copy into +the local `artifacts/` directory, installs ArgoCD from a pinned official +manifest, and applies the GitOps root Application from `deployments/argocd/`. + +By default the playbook copies the current repository from the Ansible +controller to the host. For a private repository, set +`bootstrap_repo_url`, `bootstrap_repo_ref`, and provide +`bootstrap_repo_ssh_private_key` through Ansible Vault. The private key task is +marked `no_log` and is removed after the checkout. + +For the optional Cilium path, set these variables in the inventory: + +```yaml +cilium_enabled: true +k3d_config_source: "{{ playbook_dir }}/../local-quickstart/k3d-cilium-cluster-config.yaml" +cilium_k8s_service_host: 192.168.60.252 +cilium_k8s_service_port: 6443 +``` + +The Cilium role refuses to proceed if a kube-proxy DaemonSet is present. It +passes the API endpoint explicitly to Helm and waits for the Cilium DaemonSet. + +The playbook does not create Slack or Discord credentials. Those will be added +through a separate opt-in Ansible secret toggle so ordinary bootstrap remains +credential-free. + +Validate the playbook before connecting to a host: + +```bash +ANSIBLE_LOCAL_TEMP=/tmp/k8s-baseline-ansible-tmp \ + ansible-playbook --syntax-check \ + -i ansible/inventory.flatcar-k3d.example.yml \ + ansible/bootstrap-flatcar-k3d.yml +``` + +## Production-style K3s bootstrap + +The existing production-style path applies a host baseline and installs a +pinned K3s server. It is intentionally separate from the local k3d path so +the portfolio can run without cloud credentials. ```bash cp inventory.example.yml inventory.yml @@ -14,4 +81,3 @@ ansible-playbook -i inventory.yml bootstrap-k3s.yml Before production use, review the pinned K3s version and extend the playbook for your organization’s OS baseline, firewall model, HA topology, and secret management. RKE2 can replace K3s here if the target environment requires it. - diff --git a/ansible/bootstrap-flatcar-k3d.yml b/ansible/bootstrap-flatcar-k3d.yml new file mode 100644 index 0000000..fc8e410 --- /dev/null +++ b/ansible/bootstrap-flatcar-k3d.yml @@ -0,0 +1,22 @@ +--- +- name: Bootstrap Flatcar k3d security test host + hosts: flatcar_k3d_hosts + become: true + gather_facts: false + + vars: + argocd_enabled: true + cilium_enabled: false + + pre_tasks: + - name: Read host operating system identity + ansible.builtin.raw: cat /etc/os-release + register: flatcar_os_release + changed_when: false + + roles: + - role: flatcar_k3d_host + - role: cilium_bootstrap + when: cilium_enabled | bool + - role: argocd_bootstrap + when: argocd_enabled | bool diff --git a/ansible/inventory.flatcar-k3d.example.yml b/ansible/inventory.flatcar-k3d.example.yml new file mode 100644 index 0000000..4cd9a5a --- /dev/null +++ b/ansible/inventory.flatcar-k3d.example.yml @@ -0,0 +1,12 @@ +all: + children: + flatcar_k3d_hosts: + hosts: + flatcar-k3d-test: + ansible_host: REPLACE_WITH_FLATCAR_IP + ansible_user: core + ansible_ssh_private_key_file: ~/.ssh/REPLACE_WITH_KEY + ansible_become_method: sudo + cilium_enabled: false + cilium_k8s_service_host: REPLACE_WITH_REACHABLE_API_IP + cilium_k8s_service_port: 6443 diff --git a/ansible/requirements.yml b/ansible/requirements.yml new file mode 100644 index 0000000..a0cd255 --- /dev/null +++ b/ansible/requirements.yml @@ -0,0 +1,3 @@ +--- +collections: + - name: ansible.posix diff --git a/ansible/roles/argocd_bootstrap/defaults/main.yml b/ansible/roles/argocd_bootstrap/defaults/main.yml new file mode 100644 index 0000000..552524b --- /dev/null +++ b/ansible/roles/argocd_bootstrap/defaults/main.yml @@ -0,0 +1,6 @@ +--- +argocd_enabled: true +argocd_namespace: argocd +argocd_install_manifest_url: https://raw.githubusercontent.com/argoproj/argo-cd/v3.1.8/manifests/install.yaml +argocd_root_application_manifest: "{{ k3d_workspace }}/deployments/argocd/app-of-apps.yaml" +argocd_rollout_timeout: 300s diff --git a/ansible/roles/argocd_bootstrap/tasks/main.yml b/ansible/roles/argocd_bootstrap/tasks/main.yml new file mode 100644 index 0000000..9fc00e6 --- /dev/null +++ b/ansible/roles/argocd_bootstrap/tasks/main.yml @@ -0,0 +1,32 @@ +--- +- name: Create ArgoCD namespace + ansible.builtin.raw: >- + {{ k3d_tool_dir }}/kubectl create namespace {{ argocd_namespace }} + --dry-run=client -o yaml | {{ k3d_tool_dir }}/kubectl apply -f - + become_user: "{{ k3d_user }}" + changed_when: false + +- name: Install pinned ArgoCD manifest + ansible.builtin.raw: >- + {{ k3d_tool_dir }}/kubectl apply --server-side --force-conflicts + -n {{ argocd_namespace }} -f {{ argocd_install_manifest_url }} + become_user: "{{ k3d_user }}" + +- name: Wait for ArgoCD server + ansible.builtin.raw: >- + {{ k3d_tool_dir }}/kubectl -n {{ argocd_namespace }} rollout status + deployment/argocd-server --timeout={{ argocd_rollout_timeout }} + become_user: "{{ k3d_user }}" + +- name: Apply the GitOps root Application + ansible.builtin.raw: "{{ k3d_tool_dir }}/kubectl apply -f {{ argocd_root_application_manifest }}" + become_user: "{{ k3d_user }}" + +- name: Verify root Application source path + ansible.builtin.raw: >- + {{ k3d_tool_dir }}/kubectl get application security-baseline-root + -n {{ argocd_namespace }} -o jsonpath={.spec.source.path} + become_user: "{{ k3d_user }}" + register: argocd_root_path + changed_when: false + failed_when: argocd_root_path.stdout | trim != 'deployments/argocd/apps' diff --git a/ansible/roles/cilium_bootstrap/defaults/main.yml b/ansible/roles/cilium_bootstrap/defaults/main.yml new file mode 100644 index 0000000..7f9e233 --- /dev/null +++ b/ansible/roles/cilium_bootstrap/defaults/main.yml @@ -0,0 +1,7 @@ +--- +cilium_enabled: false +cilium_namespace: kube-system +cilium_chart_version: 1.20.0 +cilium_values_file: "{{ k3d_workspace }}/deployments/cilium/cilium-values.yaml" +cilium_k8s_service_host: "" +cilium_k8s_service_port: 6443 diff --git a/ansible/roles/cilium_bootstrap/tasks/main.yml b/ansible/roles/cilium_bootstrap/tasks/main.yml new file mode 100644 index 0000000..c1d9eb0 --- /dev/null +++ b/ansible/roles/cilium_bootstrap/tasks/main.yml @@ -0,0 +1,48 @@ +--- +- name: Require a reachable Kubernetes API endpoint + ansible.builtin.assert: + that: + - cilium_k8s_service_host | length > 0 + - cilium_k8s_service_port | int > 0 + fail_msg: Set cilium_k8s_service_host and cilium_k8s_service_port for kube-proxy replacement. + +- name: Verify kube-proxy is disabled + ansible.builtin.raw: "{{ k3d_tool_dir }}/kubectl -n kube-system get daemonset kube-proxy" + become_user: "{{ k3d_user }}" + register: kube_proxy_check + changed_when: false + failed_when: false + +- name: Refuse Cilium replacement when kube-proxy is present + ansible.builtin.assert: + that: + - kube_proxy_check.rc != 0 + fail_msg: kube-proxy is present. Create the cluster with a kube-proxy-free profile first. + +- name: Add the Cilium Helm repository + ansible.builtin.raw: "{{ k3d_tool_dir }}/helm repo add cilium https://helm.cilium.io/" + become_user: "{{ k3d_user }}" + changed_when: false + +- name: Update Helm repositories + ansible.builtin.raw: "{{ k3d_tool_dir }}/helm repo update" + become_user: "{{ k3d_user }}" + changed_when: false + +- name: Install Cilium with kube-proxy replacement + ansible.builtin.raw: >- + {{ k3d_tool_dir }}/helm upgrade --install cilium cilium/cilium + --namespace {{ cilium_namespace }} + --version {{ cilium_chart_version }} + --values {{ cilium_values_file }} + --set kubeProxyReplacement=true + --set k8sServiceHost={{ cilium_k8s_service_host }} + --set k8sServicePort={{ cilium_k8s_service_port }} + --wait --timeout 10m + become_user: "{{ k3d_user }}" + +- name: Wait for Cilium agents + ansible.builtin.raw: >- + {{ k3d_tool_dir }}/kubectl -n {{ cilium_namespace }} rollout status + daemonset/cilium --timeout=300s + become_user: "{{ k3d_user }}" diff --git a/ansible/roles/flatcar_k3d_host/defaults/main.yml b/ansible/roles/flatcar_k3d_host/defaults/main.yml new file mode 100644 index 0000000..f6bde84 --- /dev/null +++ b/ansible/roles/flatcar_k3d_host/defaults/main.yml @@ -0,0 +1,23 @@ +--- +flatcar_expected_distribution: Flatcar + +k3d_cluster_name: security-baseline +k3d_config_source: "{{ playbook_dir }}/../local-quickstart/k3d-cluster-config.yaml" +k3d_workspace: /opt/k3d-test/workspace/kubernetes-security-baseline +k3d_tool_dir: /opt/k3d-test/bin +k3d_user: core +k3d_group: core + +k3d_version: 5.9.0 +kubectl_version: v1.35.5 +helm_version: v3.19.0 +tool_architecture: amd64 + +bootstrap_repo_url: "" +bootstrap_repo_ref: main +bootstrap_repo_ssh_private_key: "" +bootstrap_repo_ssh_private_key_path: /run/k3d-test-repo-key +bootstrap_repo_source: "{{ playbook_dir }}/.." + +kubeconfig_fetch_enabled: true +kubeconfig_artifact_dir: "{{ playbook_dir }}/../artifacts" diff --git a/ansible/roles/flatcar_k3d_host/tasks/main.yml b/ansible/roles/flatcar_k3d_host/tasks/main.yml new file mode 100644 index 0000000..3dad29b --- /dev/null +++ b/ansible/roles/flatcar_k3d_host/tasks/main.yml @@ -0,0 +1,149 @@ +--- +- name: Require Flatcar Linux + ansible.builtin.assert: + that: + - flatcar_os_release.stdout is search('ID=flatcar') + fail_msg: This role is for Flatcar Linux. Use bootstrap-k3s.yml for K3s hosts. + +- name: Read host architecture + ansible.builtin.raw: uname -m + register: host_architecture + changed_when: false + +- name: Validate x86-64 host architecture + ansible.builtin.assert: + that: + - host_architecture.stdout | trim in ['x86_64', 'amd64'] + - tool_architecture == 'amd64' + fail_msg: This profile currently pins x86-64 binaries. + +- name: Ensure Docker is enabled and running + ansible.builtin.raw: systemctl enable --now docker + changed_when: false + +- name: Create Flatcar k3d directories + ansible.builtin.raw: >- + install -d -m 0755 -o {{ k3d_user }} -g {{ k3d_group }} + {{ k3d_tool_dir }} {{ k3d_workspace }} /home/core/.kube + +- name: Install pinned k3d + ansible.builtin.raw: >- + if [ ! -x {{ k3d_tool_dir }}/k3d ] || [ ! -f {{ k3d_tool_dir }}/.k3d-{{ k3d_version }} ]; then + curl --fail --location --silent --show-error https://github.com/k3d-io/k3d/releases/download/v{{ k3d_version }}/k3d-linux-{{ tool_architecture }} --output {{ k3d_tool_dir }}/k3d && chmod 0755 {{ k3d_tool_dir }}/k3d && + touch {{ k3d_tool_dir }}/.k3d-{{ k3d_version }}; + fi + +- name: Install pinned kubectl + ansible.builtin.raw: >- + if [ ! -x {{ k3d_tool_dir }}/kubectl ] || [ ! -f {{ k3d_tool_dir }}/.kubectl-{{ kubectl_version }} ]; then + curl --fail --location --silent --show-error https://dl.k8s.io/release/{{ kubectl_version }}/bin/linux/{{ tool_architecture }}/kubectl --output {{ k3d_tool_dir }}/kubectl && chmod 0755 {{ k3d_tool_dir }}/kubectl && + touch {{ k3d_tool_dir }}/.kubectl-{{ kubectl_version }}; + fi + +- name: Download and install pinned Helm + ansible.builtin.raw: >- + if [ ! -x {{ k3d_tool_dir }}/helm ] || [ ! -f {{ k3d_tool_dir }}/.helm-{{ helm_version }} ]; then + curl --fail --location --silent --show-error https://get.helm.sh/helm-{{ helm_version }}-linux-{{ tool_architecture }}.tar.gz --output /tmp/flatcar-k3d-helm.tar.gz && + tar -xzf /tmp/flatcar-k3d-helm.tar.gz -C /tmp && + install -m 0755 /tmp/linux-amd64/helm {{ k3d_tool_dir }}/helm && + touch {{ k3d_tool_dir }}/.helm-{{ helm_version }}; + fi + +- name: Configure core shell PATH + ansible.builtin.raw: >- + grep -Fxq 'export PATH="{{ k3d_tool_dir }}:$HOME/bin:$PATH"' /home/core/.profile || + printf '%s\n' 'export PATH="{{ k3d_tool_dir }}:$HOME/bin:$PATH"' + 'export KUBECONFIG="$HOME/.kube/config"' >> /home/core/.profile + && chown core:core /home/core/.profile && chmod 0644 /home/core/.profile + +- name: Copy repository from the Ansible controller + ansible.posix.synchronize: + src: "{{ bootstrap_repo_source }}/" + dest: "{{ k3d_workspace }}/" + archive: true + delete: false + rsync_opts: + - "--exclude=.git" + - "--exclude=artifacts" + delegate_to: localhost + become: false + when: bootstrap_repo_url | length == 0 + +- name: Install temporary repository deploy key + ansible.builtin.raw: >- + printf '%s' '{{ bootstrap_repo_ssh_private_key | b64encode }}' | base64 -d + > {{ bootstrap_repo_ssh_private_key_path }} && + chmod 0600 {{ bootstrap_repo_ssh_private_key_path }} + no_log: true + when: + - bootstrap_repo_url | length > 0 + - bootstrap_repo_ssh_private_key | length > 0 + +- name: Clone or update the repository on the host + ansible.builtin.raw: >- + if [ -d {{ k3d_workspace }}/.git ]; then + git -C {{ k3d_workspace }} fetch --prune origin {{ bootstrap_repo_ref }} && + git -C {{ k3d_workspace }} checkout {{ bootstrap_repo_ref }} && + git -C {{ k3d_workspace }} reset --hard origin/{{ bootstrap_repo_ref }}; + else + GIT_SSH_COMMAND="ssh -i {{ bootstrap_repo_ssh_private_key_path }} -o StrictHostKeyChecking=accept-new" + git clone --branch {{ bootstrap_repo_ref }} {{ bootstrap_repo_url }} {{ k3d_workspace }}; + fi + no_log: "{{ bootstrap_repo_ssh_private_key | length > 0 }}" + when: bootstrap_repo_url | length > 0 + +- name: Remove temporary repository deploy key + ansible.builtin.raw: rm -f {{ bootstrap_repo_ssh_private_key_path }} + when: + - bootstrap_repo_url | length > 0 + - bootstrap_repo_ssh_private_key | length > 0 + +- name: Check whether the k3d cluster already exists + ansible.builtin.raw: "{{ k3d_tool_dir }}/k3d cluster list" + register: k3d_cluster_list + changed_when: false + +- name: Create the k3d cluster + ansible.builtin.raw: >- + {{ k3d_tool_dir }}/k3d cluster create + --config {{ k3d_workspace }}/local-quickstart/{{ k3d_config_source | basename }} + --wait --timeout 180s + become_user: "{{ k3d_user }}" + when: k3d_cluster_name not in k3d_cluster_list.stdout + +- name: Write the cluster kubeconfig for the core user + ansible.builtin.raw: >- + {{ k3d_tool_dir }}/k3d kubeconfig get {{ k3d_cluster_name }} + > /home/core/.kube/config && chmod 0600 /home/core/.kube/config + become_user: "{{ k3d_user }}" + +- name: Verify Kubernetes nodes are Ready + ansible.builtin.raw: "{{ k3d_tool_dir }}/kubectl get nodes --no-headers" + become_user: "{{ k3d_user }}" + register: k3d_nodes + changed_when: false + retries: 12 + delay: 10 + until: + - k3d_nodes.rc == 0 + - k3d_nodes.stdout is search(' Ready ') + +- name: Ensure local kubeconfig artifact directory exists + ansible.builtin.file: + path: "{{ kubeconfig_artifact_dir }}" + state: directory + mode: '0700' + delegate_to: localhost + become: false + run_once: true + when: kubeconfig_fetch_enabled | bool + +- name: Fetch kubeconfig artifact to the Ansible controller + ansible.posix.synchronize: + src: /home/core/.kube/config + dest: "{{ kubeconfig_artifact_dir }}/{{ inventory_hostname }}-kubeconfig" + mode: pull + archive: true + delegate_to: localhost + become: false + when: kubeconfig_fetch_enabled | bool