fix cilium routing and cluster dns
Kube-bench CIS scan / Scan ephemeral K3s cluster (push) Successful in 1m0s

This commit is contained in:
2026-08-16 19:41:40 -04:00
parent 88e735109f
commit 1a372b7eac
8 changed files with 250 additions and 11 deletions
+23 -4
View File
@@ -32,8 +32,14 @@ ansible-playbook \
The playbook installs pinned k3d, kubectl, and Helm binaries, starts Docker, 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 creates the k3d cluster, writes the core user's kubeconfig, fetches a copy into
the local `artifacts/` directory, installs ArgoCD from a pinned official the local `artifacts/` directory, merges that config into the operator's
manifest, and applies the GitOps root Application from `deployments/argocd/`. default `~/.kube/config`, selects the new context as current, installs ArgoCD
from a pinned official manifest, and applies the GitOps root Application from
`deployments/argocd/`. The merge preserves existing contexts and the playbook
verifies the selected context before continuing.
Set `kubeconfig_import_enabled: false` when the fetched artifact should not
change the operator's default kubeconfig.
By default the playbook copies the current repository from the Ansible By default the playbook copies the current repository from the Ansible
controller to the host. For a private repository, set controller to the host. For a private repository, set
@@ -41,22 +47,35 @@ controller to the host. For a private repository, set
`bootstrap_repo_ssh_private_key` through Ansible Vault. The private key task is `bootstrap_repo_ssh_private_key` through Ansible Vault. The private key task is
marked `no_log` and is removed after the checkout. marked `no_log` and is removed after the checkout.
When k3d exports its kubeconfig, it may use `0.0.0.0` as the API server host.
Ansible rewrites that host to `kubeconfig_server_host`, which defaults to the
Flatcar host address, while preserving the dynamically assigned API port. The
rewritten endpoint is verified before the config is merged into the default
kubeconfig.
For the optional Cilium path, set these variables in the inventory: For the optional Cilium path, set these variables in the inventory:
```yaml ```yaml
cilium_enabled: true cilium_enabled: true
k3d_config_source: "{{ playbook_dir }}/../local-quickstart/k3d-cilium-cluster-config.yaml" k3d_config_source: "{{ playbook_dir }}/../local-quickstart/k3d-cilium-cluster-config.yaml"
cilium_k8s_service_host: 192.168.60.252 cilium_k8s_service_host: 192.168.60.252
cilium_k8s_service_port: 6443 cilium_k8s_service_port: 0
``` ```
The Cilium role refuses to proceed if a kube-proxy DaemonSet is present. It 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. derives the dynamically published k3d API port from the new cluster's
kubeconfig, 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 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 through a separate opt-in Ansible secret toggle so ordinary bootstrap remains
credential-free. credential-free.
If CoreDNS cannot resolve external names from a nested Docker network, set
`k3d_dns_servers` to DNS servers reachable from the Flatcar host. Ansible
creates the supported K3s `coredns-custom` ConfigMap and restarts CoreDNS so
ArgoCD can resolve the Git server and Falcosidekick can resolve its webhook.
Validate the playbook before connecting to a host: Validate the playbook before connecting to a host:
```bash ```bash
+8 -1
View File
@@ -9,4 +9,11 @@ all:
ansible_become_method: sudo ansible_become_method: sudo
cilium_enabled: false cilium_enabled: false
cilium_k8s_service_host: REPLACE_WITH_REACHABLE_API_IP cilium_k8s_service_host: REPLACE_WITH_REACHABLE_API_IP
cilium_k8s_service_port: 6443 # Set to zero for k3d so Ansible derives the published API port from
# the kubeconfig created during cluster bootstrap.
cilium_k8s_service_port: 0
# Optional. Use resolvers reachable from the Flatcar network when
# Docker's nested bridge resolver cannot answer external names.
# k3d_dns_servers:
# - REPLACE_WITH_PRIMARY_DNS
# - REPLACE_WITH_SECONDARY_DNS
@@ -4,4 +4,6 @@ cilium_namespace: kube-system
cilium_chart_version: 1.20.0 cilium_chart_version: 1.20.0
cilium_values_file: "{{ k3d_workspace }}/deployments/cilium/cilium-values.yaml" cilium_values_file: "{{ k3d_workspace }}/deployments/cilium/cilium-values.yaml"
cilium_k8s_service_host: "" cilium_k8s_service_host: ""
cilium_k8s_service_port: 6443 # k3d publishes the API on a dynamically allocated host port. A value of zero
# makes the role derive that port from the kubeconfig created by k3d.
cilium_k8s_service_port: 0
@@ -1,4 +1,17 @@
--- ---
- name: Discover the k3d API server port from the active kubeconfig
ansible.builtin.raw: >-
awk -F: '/server:/{print $4; exit}' /home/{{ k3d_user }}/.kube/config
become_user: "{{ k3d_user }}"
register: cilium_discovered_api_port
changed_when: false
when: cilium_k8s_service_port | int == 0
- name: Use the discovered k3d API server port
ansible.builtin.set_fact:
cilium_k8s_service_port: "{{ cilium_discovered_api_port.stdout | trim }}"
when: cilium_k8s_service_port | int == 0
- name: Require a reachable Kubernetes API endpoint - name: Require a reachable Kubernetes API endpoint
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -46,3 +59,14 @@
{{ k3d_tool_dir }}/kubectl -n {{ cilium_namespace }} rollout status {{ k3d_tool_dir }}/kubectl -n {{ cilium_namespace }} rollout status
daemonset/cilium --timeout=300s daemonset/cilium --timeout=300s
become_user: "{{ k3d_user }}" become_user: "{{ k3d_user }}"
- name: Verify all Kubernetes nodes become Ready after Cilium starts
ansible.builtin.raw: "{{ k3d_tool_dir }}/kubectl get nodes --no-headers"
become_user: "{{ k3d_user }}"
register: cilium_ready_nodes
changed_when: false
retries: 18
delay: 10
until:
- cilium_ready_nodes.rc == 0
- cilium_ready_nodes.stdout is search(' Ready ')
@@ -21,3 +21,12 @@ bootstrap_repo_source: "{{ playbook_dir }}/.."
kubeconfig_fetch_enabled: true kubeconfig_fetch_enabled: true
kubeconfig_artifact_dir: "{{ playbook_dir }}/../artifacts" kubeconfig_artifact_dir: "{{ playbook_dir }}/../artifacts"
kubeconfig_import_enabled: true
kubeconfig_default_path: "{{ lookup('env', 'HOME') }}/.kube/config"
kubeconfig_context_name: "k3d-{{ k3d_cluster_name }}"
kubeconfig_server_host: "{{ ansible_host }}"
# Optional upstream DNS servers for nested k3d hosts. Docker may expose an
# unreachable bridge resolver to CoreDNS, so provide the host's real resolvers
# when pods must resolve external Git or webhook endpoints.
k3d_dns_servers: []
+158 -1
View File
@@ -103,6 +103,17 @@
register: k3d_cluster_list register: k3d_cluster_list
changed_when: false changed_when: false
- name: Configure the Cilium profile API certificate SAN
ansible.builtin.raw: >-
sed -i
-e 's/K3D_EXTERNAL_API_SERVER_IP/{{ kubeconfig_server_host }}/g'
-e 's/K3D_INTERNAL_API_SERVER_IP/{{ cilium_k8s_service_host }}/g'
{{ k3d_workspace }}/local-quickstart/{{ k3d_config_source | basename }}
when:
- cilium_enabled | bool
- cilium_k8s_service_host | default('') | length > 0
- k3d_cluster_name not in k3d_cluster_list.stdout
- name: Create the k3d cluster - name: Create the k3d cluster
ansible.builtin.raw: >- ansible.builtin.raw: >-
{{ k3d_tool_dir }}/k3d cluster create {{ k3d_tool_dir }}/k3d cluster create
@@ -117,17 +128,47 @@
> /home/core/.kube/config && chmod 0600 /home/core/.kube/config > /home/core/.kube/config && chmod 0600 /home/core/.kube/config
become_user: "{{ k3d_user }}" become_user: "{{ k3d_user }}"
- name: Verify Kubernetes nodes are Ready - name: Verify Kubernetes nodes are Ready for the standard profile
ansible.builtin.raw: "{{ k3d_tool_dir }}/kubectl get nodes --no-headers" ansible.builtin.raw: "{{ k3d_tool_dir }}/kubectl get nodes --no-headers"
become_user: "{{ k3d_user }}" become_user: "{{ k3d_user }}"
register: k3d_nodes register: k3d_nodes
changed_when: false changed_when: false
when: not (cilium_enabled | bool)
retries: 12 retries: 12
delay: 10 delay: 10
until: until:
- k3d_nodes.rc == 0 - k3d_nodes.rc == 0
- k3d_nodes.stdout is search(' Ready ') - k3d_nodes.stdout is search(' Ready ')
- name: Verify Kubernetes API and node objects for the Cilium profile
ansible.builtin.raw: "{{ k3d_tool_dir }}/kubectl get nodes --no-headers"
become_user: "{{ k3d_user }}"
register: k3d_nodes_cilium
changed_when: false
when: cilium_enabled | bool
retries: 12
delay: 10
until:
- k3d_nodes_cilium.rc == 0
- k3d_nodes_cilium.stdout | trim | length > 0
- name: Configure reachable upstream DNS servers for CoreDNS
ansible.builtin.raw: >-
{{ k3d_tool_dir }}/kubectl -n kube-system create configmap coredns-custom
--from-literal=upstream.override='forward . {{ (k3d_dns_servers | string | from_yaml) | join(' ') }}'
--dry-run=client -o yaml |
{{ k3d_tool_dir }}/kubectl apply -f -
become_user: "{{ k3d_user }}"
when: k3d_dns_servers | length > 0
- name: Restart CoreDNS after changing upstream resolvers
ansible.builtin.raw: >-
{{ k3d_tool_dir }}/kubectl -n kube-system rollout restart deployment/coredns &&
{{ k3d_tool_dir }}/kubectl -n kube-system rollout status deployment/coredns
--timeout=120s
become_user: "{{ k3d_user }}"
when: k3d_dns_servers | length > 0
- name: Ensure local kubeconfig artifact directory exists - name: Ensure local kubeconfig artifact directory exists
ansible.builtin.file: ansible.builtin.file:
path: "{{ kubeconfig_artifact_dir }}" path: "{{ kubeconfig_artifact_dir }}"
@@ -144,6 +185,122 @@
dest: "{{ kubeconfig_artifact_dir }}/{{ inventory_hostname }}-kubeconfig" dest: "{{ kubeconfig_artifact_dir }}/{{ inventory_hostname }}-kubeconfig"
mode: pull mode: pull
archive: true archive: true
rsync_opts:
- "--checksum"
delegate_to: localhost delegate_to: localhost
become: false become: false
when: kubeconfig_fetch_enabled | bool when: kubeconfig_fetch_enabled | bool
- name: Replace the k3d wildcard API address in the local artifact
ansible.builtin.replace:
path: "{{ kubeconfig_artifact_dir }}/{{ inventory_hostname }}-kubeconfig"
regexp: '(?m)^(\s+server:\s+https://)0\.0\.0\.0(:[0-9]+\s*)$'
replace: '\g<1>{{ kubeconfig_server_host }}\g<2>'
delegate_to: localhost
become: false
when:
- kubeconfig_fetch_enabled | bool
- kubeconfig_import_enabled | bool
- name: Read the rewritten local kubeconfig server endpoint
ansible.builtin.command:
cmd: >-
kubectl config view
--kubeconfig {{ kubeconfig_artifact_dir }}/{{ inventory_hostname }}-kubeconfig
--minify -o jsonpath={.clusters[0].cluster.server}
register: rewritten_kubeconfig_server
changed_when: false
delegate_to: localhost
become: false
when:
- kubeconfig_fetch_enabled | bool
- kubeconfig_import_enabled | bool
- name: Require a reachable host address in the local kubeconfig artifact
ansible.builtin.assert:
that:
- rewritten_kubeconfig_server.stdout | trim is match('^https://' ~ (kubeconfig_server_host | regex_escape) ~ ':[0-9]+$')
- rewritten_kubeconfig_server.stdout | trim is not search('0\.0\.0\.0')
fail_msg: >-
The fetched kubeconfig still points to an unusable API address:
{{ rewritten_kubeconfig_server.stdout | trim }}
delegate_to: localhost
become: false
when:
- kubeconfig_fetch_enabled | bool
- kubeconfig_import_enabled | bool
- name: Ensure the default kubeconfig directory exists
ansible.builtin.file:
path: "{{ kubeconfig_default_path | dirname }}"
state: directory
mode: '0700'
delegate_to: localhost
become: false
run_once: true
when:
- kubeconfig_fetch_enabled | bool
- kubeconfig_import_enabled | bool
- name: Merge the fetched kubeconfig into the operator default
ansible.builtin.command:
cmd: kubectl config view --flatten
environment:
# Put the freshly fetched artifact first so it overrides an older entry
# with the same cluster and context name.
KUBECONFIG: "{{ kubeconfig_artifact_dir }}/{{ inventory_hostname }}-kubeconfig:{{ kubeconfig_default_path }}"
register: merged_kubeconfig
changed_when: false
delegate_to: localhost
become: false
no_log: true
when:
- kubeconfig_fetch_enabled | bool
- kubeconfig_import_enabled | bool
- name: Install the merged kubeconfig as the operator default
ansible.builtin.copy:
content: "{{ merged_kubeconfig.stdout }}"
dest: "{{ kubeconfig_default_path }}"
mode: '0600'
delegate_to: localhost
become: false
no_log: true
when:
- kubeconfig_fetch_enabled | bool
- kubeconfig_import_enabled | bool
- name: Select the bootstrapped cluster context
ansible.builtin.command:
cmd: kubectl config use-context {{ kubeconfig_context_name }} --kubeconfig {{ kubeconfig_default_path }}
register: selected_kubeconfig_context
changed_when: false
delegate_to: localhost
become: false
when:
- kubeconfig_fetch_enabled | bool
- kubeconfig_import_enabled | bool
- name: Verify the selected kubeconfig context
ansible.builtin.command:
cmd: kubectl config current-context --kubeconfig {{ kubeconfig_default_path }}
register: selected_kubeconfig_context_check
changed_when: false
delegate_to: localhost
become: false
when:
- kubeconfig_fetch_enabled | bool
- kubeconfig_import_enabled | bool
- name: Require the expected bootstrapped context to be current
ansible.builtin.assert:
that:
- selected_kubeconfig_context_check.stdout | trim == kubeconfig_context_name
fail_msg: >-
The default kubeconfig context was not changed to
{{ kubeconfig_context_name }}.
delegate_to: localhost
become: false
when:
- kubeconfig_fetch_enabled | bool
- kubeconfig_import_enabled | bool
+17 -3
View File
@@ -8,8 +8,23 @@ k8sServicePort: 6443
ipam: ipam:
mode: kubernetes mode: kubernetes
routingMode: tunnel # k3d nodes share a directly reachable Docker bridge. Native routing avoids
tunnelProtocol: vxlan # nested VXLAN service paths that can prevent pod access to ClusterIP services.
routingMode: native
autoDirectNodeRoutes: true
# K3s allocates pod addresses from this cluster-wide CIDR. Native routing
# requires the CIDR so Cilium can distinguish pod traffic from underlay traffic.
ipv4NativeRoutingCIDR: 10.42.0.0/16
# The k3d nodes run as nested containers on the Flatcar host. eBPF masquerade
# keeps return traffic for cross-node service backends inside the Cilium path.
bpf:
masquerade: true
# Keep socket load-balancing in the node namespace for the nested k3d profile.
# Pod traffic is handled by the eBPF service datapath instead.
socketLB:
hostNamespaceOnly: true
hubble: hubble:
enabled: true enabled: true
@@ -20,4 +35,3 @@ hubble:
operator: operator:
replicas: 1 replicas: 1
@@ -20,6 +20,14 @@ options:
- arg: --disable-kube-proxy - arg: --disable-kube-proxy
nodeFilters: nodeFilters:
- server:* - server:*
# Include both the external Flatcar address and the internal k3d server
# address so workstation and in-cluster API clients pass TLS validation.
- arg: --tls-san=K3D_EXTERNAL_API_SERVER_IP
nodeFilters:
- server:*
- arg: --tls-san=K3D_INTERNAL_API_SERVER_IP
nodeFilters:
- server:*
- arg: --disable=traefik - arg: --disable=traefik
nodeFilters: nodeFilters:
- server:* - server:*
@@ -29,4 +37,3 @@ options:
kubeconfig: kubeconfig:
updateDefaultKubeconfig: true updateDefaultKubeconfig: true
switchCurrentContext: true switchCurrentContext: true