add Ansible Flatcar and platform bootstrap
Kube-bench CIS scan / Scan ephemeral K3s cluster (push) Successful in 56s

This commit is contained in:
2026-08-15 16:40:37 -04:00
parent 478cc49e50
commit 664c3c8236
11 changed files with 379 additions and 6 deletions
+5
View File
@@ -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.
+72 -6
View File
@@ -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 organizations OS baseline, firewall model, HA topology, and secret
management. RKE2 can replace K3s here if the target environment requires it.
+22
View File
@@ -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
+12
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
---
collections:
- name: ansible.posix
@@ -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
@@ -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'
@@ -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
@@ -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 }}"
@@ -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"
@@ -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