From f12fb4d7a16086c25722ae46c67a710672cd0582 Mon Sep 17 00:00:00 2001 From: swaphb Date: Sun, 9 Aug 2026 19:52:19 -0400 Subject: [PATCH] first commit --- .gitignore | 23 +++++ README.md | 38 ++++++++ ansible/README.md | 17 ++++ ansible/bootstrap-k3s.yml | 75 ++++++++++++++++ ansible/inventory.example.yml | 9 ++ argocd/README.md | 36 ++++++++ argocd/app-of-apps.yaml | 24 +++++ argocd/apps/falco.yaml | 29 ++++++ argocd/apps/kyverno.yaml | 23 +++++ argocd/apps/policies.yaml | 24 +++++ argocd/apps/test-workloads.yaml | 23 +++++ falco/falco-values.yaml | 6 ++ local-quickstart/README.md | 21 +++++ local-quickstart/k3d-cluster-config.yaml | 33 +++++++ policies/kyverno/kustomization.yaml | 6 ++ policies/kyverno/namespace.yaml | 7 ++ policies/kyverno/phase-2-placeholder.yaml | 9 ++ terraform/cloud-cluster/README.md | 27 ++++++ terraform/cloud-cluster/main.tf | 88 +++++++++++++++++++ terraform/cloud-cluster/outputs.tf | 10 +++ .../cloud-cluster/terraform.tfvars.example | 7 ++ terraform/cloud-cluster/variables.tf | 33 +++++++ terraform/cloud-cluster/versions.tf | 15 ++++ test-workloads/kustomization.yaml | 5 ++ test-workloads/phase-2-placeholder.yaml | 9 ++ 25 files changed, 597 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 ansible/README.md create mode 100644 ansible/bootstrap-k3s.yml create mode 100644 ansible/inventory.example.yml create mode 100644 argocd/README.md create mode 100644 argocd/app-of-apps.yaml create mode 100644 argocd/apps/falco.yaml create mode 100644 argocd/apps/kyverno.yaml create mode 100644 argocd/apps/policies.yaml create mode 100644 argocd/apps/test-workloads.yaml create mode 100644 falco/falco-values.yaml create mode 100644 local-quickstart/README.md create mode 100644 local-quickstart/k3d-cluster-config.yaml create mode 100644 policies/kyverno/kustomization.yaml create mode 100644 policies/kyverno/namespace.yaml create mode 100644 policies/kyverno/phase-2-placeholder.yaml create mode 100644 terraform/cloud-cluster/README.md create mode 100644 terraform/cloud-cluster/main.tf create mode 100644 terraform/cloud-cluster/outputs.tf create mode 100644 terraform/cloud-cluster/terraform.tfvars.example create mode 100644 terraform/cloud-cluster/variables.tf create mode 100644 terraform/cloud-cluster/versions.tf create mode 100644 test-workloads/kustomization.yaml create mode 100644 test-workloads/phase-2-placeholder.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9500c86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Terraform state, plans, and provider/plugin caches +**/.terraform/ +*.tfstate +*.tfstate.* +*.tfplan +crash.log +crash.*.log +.terraform.lock.hcl + +# Ansible artifacts and local secrets +*.retry +*.vault-password +ansible/inventory/*.ini +ansible/inventory/*.yml +*.key +*.pem + +# Local tooling and generated demo output +.DS_Store +.env +tmp/ +artifacts/ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..fbb16f7 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# Kubernetes Security Baseline + +An auditable, GitOps-managed Kubernetes security baseline: local k3d for a +fast reproducible demo, optional Terraform + Ansible for a cloud K3s host, CIS +Benchmark checks in CI, Kyverno admission policies, and Falco runtime +detection. + +## Phase 1: provision a local cluster + +The primary quick-start path requires Docker, k3d, and kubectl: + +```bash +k3d cluster create --config local-quickstart/k3d-cluster-config.yaml +kubectl get nodes -o wide +``` + +Expected result: one server and two agent nodes in `Ready` state. The bundled +Traefik is disabled so later ArgoCD-managed components own add-ons explicitly. + +Delete the lab with `k3d cluster delete security-baseline`. + +## Production-style option + +`terraform/cloud-cluster/` and `ansible/bootstrap-k3s.yml` provide an optional, +documented AWS path. It requires your own AWS credentials, an existing EC2 key +pair, an Ubuntu AMI ID for the chosen region, and a tightly scoped admin CIDR. +It is not needed for the portfolio demo and is not run in CI. + +## Build status + +- [x] Phase 1: local k3d definition and optional Terraform/Ansible path +- [ ] Phase 2: ArgoCD app-of-apps deployment (scaffolded; runtime verification pending) +- [ ] Phase 3: kube-bench CI scan +- [ ] Phase 4: Kyverno policy set and CIS mapping +- [ ] Phase 5: Falco rules and webhook alerting +- [ ] Phase 6: test workloads and evidence capture +- [ ] Phase 7: architecture/design documentation +- [ ] Phase 8: final portfolio polish diff --git a/ansible/README.md b/ansible/README.md new file mode 100644 index 0000000..f651c3a --- /dev/null +++ b/ansible/README.md @@ -0,0 +1,17 @@ +# Optional Ansible bootstrap + +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. + +```bash +cp inventory.example.yml inventory.yml +# Replace the Terraform public IP and local SSH key path. +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-k3s.yml b/ansible/bootstrap-k3s.yml new file mode 100644 index 0000000..2b28d9b --- /dev/null +++ b/ansible/bootstrap-k3s.yml @@ -0,0 +1,75 @@ +--- +- name: Harden host and install K3s + hosts: k3s_servers + become: true + gather_facts: true + + vars: + k3s_version: "v1.30.6+k3s1" + k3s_install_url: "https://get.k3s.io" + + pre_tasks: + - name: Require a supported Ubuntu release + ansible.builtin.assert: + that: + - ansible_distribution == "Ubuntu" + - ansible_distribution_version is version("22.04", ">=") + fail_msg: "This playbook expects Ubuntu 22.04 or newer." + + tasks: + - name: Install host security prerequisites + ansible.builtin.apt: + name: + - apparmor + - apparmor-utils + - curl + - ca-certificates + - unattended-upgrades + state: present + update_cache: true + + - name: Ensure unattended security upgrades are enabled + ansible.builtin.service: + name: unattended-upgrades + state: started + enabled: true + + - name: Disable swap for Kubernetes node + ansible.builtin.command: swapoff -a + changed_when: false + + - name: Remove swap entries from fstab + ansible.builtin.replace: + path: /etc/fstab + regexp: '^([^#].*\sswap\s+.*)$' + replace: '# Disabled for Kubernetes: \1' + + - name: Set restrictive SSH password authentication + ansible.builtin.copy: + dest: /etc/ssh/sshd_config.d/60-k8s-baseline.conf + owner: root + group: root + mode: '0644' + content: | + PasswordAuthentication no + PermitRootLogin no + notify: Restart ssh + + - name: Install pinned K3s server + ansible.builtin.shell: | + curl -sfL {{ k3s_install_url }} | INSTALL_K3S_VERSION={{ k3s_version }} sh -s - server --write-kubeconfig-mode 600 + args: + creates: /usr/local/bin/k3s + + - name: Enable and start K3s + ansible.builtin.service: + name: k3s + state: started + enabled: true + + handlers: + - name: Restart ssh + ansible.builtin.service: + name: ssh + state: restarted + diff --git a/ansible/inventory.example.yml b/ansible/inventory.example.yml new file mode 100644 index 0000000..ca9a959 --- /dev/null +++ b/ansible/inventory.example.yml @@ -0,0 +1,9 @@ +all: + children: + k3s_servers: + hosts: + k3s-1: + ansible_host: REPLACE_WITH_TERRAFORM_PUBLIC_IP + ansible_user: ubuntu + ansible_ssh_private_key_file: ~/.ssh/REPLACE_WITH_KEY + diff --git a/argocd/README.md b/argocd/README.md new file mode 100644 index 0000000..8abf347 --- /dev/null +++ b/argocd/README.md @@ -0,0 +1,36 @@ +# ArgoCD GitOps bootstrap + +ArgoCD is installed once into the local cluster; everything after that is +declared through the root Application. The root uses the app-of-apps pattern: +it watches `argocd/apps/`, and each child Application owns one platform or +workload boundary. + +## Bootstrap + +1. Push this repository to GitHub and replace `REPLACE_WITH_GITHUB_OWNER` in + the Application manifests with the repository owner. +2. Create the local cluster from `local-quickstart/`. +3. Run: + +```bash +kubectl create namespace argocd --dry-run=client -o yaml | kubectl apply -f - +kubectl apply --server-side --force-conflicts -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml +kubectl -n argocd rollout status deployment/argocd-server --timeout=180s +kubectl apply -f argocd/app-of-apps.yaml +kubectl get applications -n argocd -w +``` + +The first command is idempotent. The official install manifest is fetched at +bootstrap time rather than vendored into this repository, keeping the repo +reviewable and making the ArgoCD version choice visible in the command. + +For local UI access: + +```bash +kubectl -n argocd port-forward svc/argocd-server 8081:443 +kubectl -n argocd get secret argocd-initial-admin-secret \ + -o jsonpath='{.data.password}' | base64 -d; echo +``` + +The initial admin secret is for local bootstrap only. A later hardening phase +should replace this with SSO/RBAC and remove the bootstrap credential. diff --git a/argocd/app-of-apps.yaml b/argocd/app-of-apps.yaml new file mode 100644 index 0000000..cb48f66 --- /dev/null +++ b/argocd/app-of-apps.yaml @@ -0,0 +1,24 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: security-baseline-root + namespace: argocd + labels: + app.kubernetes.io/part-of: kubernetes-security-baseline +spec: + project: default + source: + # Replace the owner after publishing this repository. + repoURL: https://github.com/REPLACE_WITH_GITHUB_OWNER/kubernetes-security-baseline.git + targetRevision: main + path: argocd/apps + destination: + server: https://kubernetes.default.svc + namespace: argocd + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + diff --git a/argocd/apps/falco.yaml b/argocd/apps/falco.yaml new file mode 100644 index 0000000..df00aae --- /dev/null +++ b/argocd/apps/falco.yaml @@ -0,0 +1,29 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: falco + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "-1" +spec: + project: default + sources: + - repoURL: https://falcosecurity.github.io/charts + chart: falco + targetRevision: 4.21.1 + helm: + valueFiles: + - $values/falco/falco-values.yaml + - repoURL: https://github.com/REPLACE_WITH_GITHUB_OWNER/kubernetes-security-baseline.git + targetRevision: main + ref: values + destination: + server: https://kubernetes.default.svc + namespace: falco + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + diff --git a/argocd/apps/kyverno.yaml b/argocd/apps/kyverno.yaml new file mode 100644 index 0000000..9f95c04 --- /dev/null +++ b/argocd/apps/kyverno.yaml @@ -0,0 +1,23 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: kyverno + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "-2" +spec: + project: default + source: + repoURL: https://kyverno.github.io/kyverno/ + chart: kyverno + targetRevision: 3.3.7 + destination: + server: https://kubernetes.default.svc + namespace: kyverno + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + diff --git a/argocd/apps/policies.yaml b/argocd/apps/policies.yaml new file mode 100644 index 0000000..56bd6b4 --- /dev/null +++ b/argocd/apps/policies.yaml @@ -0,0 +1,24 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: security-policies + namespace: argocd + annotations: + # Policies wait for Kyverno's CRDs/controller to exist. + argocd.argoproj.io/sync-wave: "0" +spec: + project: default + source: + repoURL: https://github.com/REPLACE_WITH_GITHUB_OWNER/kubernetes-security-baseline.git + targetRevision: main + path: policies/kyverno + destination: + server: https://kubernetes.default.svc + namespace: security-baseline + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + diff --git a/argocd/apps/test-workloads.yaml b/argocd/apps/test-workloads.yaml new file mode 100644 index 0000000..9c95fdb --- /dev/null +++ b/argocd/apps/test-workloads.yaml @@ -0,0 +1,23 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: security-test-workloads + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "1" +spec: + project: default + source: + repoURL: https://github.com/REPLACE_WITH_GITHUB_OWNER/kubernetes-security-baseline.git + targetRevision: main + path: test-workloads + destination: + server: https://kubernetes.default.svc + namespace: security-baseline + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + diff --git a/falco/falco-values.yaml b/falco/falco-values.yaml new file mode 100644 index 0000000..813bad5 --- /dev/null +++ b/falco/falco-values.yaml @@ -0,0 +1,6 @@ +# Phase 2 placeholder values. Runtime rules and webhook output are added in +# phase 5; keeping values in Git now establishes the ArgoCD ownership boundary. +falco: + json_output: true + json_include_output_property: true + diff --git a/local-quickstart/README.md b/local-quickstart/README.md new file mode 100644 index 0000000..456fcd0 --- /dev/null +++ b/local-quickstart/README.md @@ -0,0 +1,21 @@ +# Local quick start + +Prerequisites: Docker, `k3d`, and `kubectl`. + +Create the cluster and verify all nodes become Ready: + +```bash +k3d cluster create --config local-quickstart/k3d-cluster-config.yaml +kubectl config use-context k3d-security-baseline +kubectl get nodes -o wide +``` + +The config creates one server and two agents and disables the bundled Traefik +so later GitOps components own the cluster add-ons explicitly. + +To remove the local cluster: + +```bash +k3d cluster delete security-baseline +``` + diff --git a/local-quickstart/k3d-cluster-config.yaml b/local-quickstart/k3d-cluster-config.yaml new file mode 100644 index 0000000..cd07f2d --- /dev/null +++ b/local-quickstart/k3d-cluster-config.yaml @@ -0,0 +1,33 @@ +apiVersion: k3d.io/v1alpha5 +kind: Simple +metadata: + name: security-baseline + +# The default path is intentionally small enough for a laptop, while still +# exercising a server/agent topology rather than a single-node shortcut. +servers: 1 +agents: 2 + +options: + k3s: + # Keep the local cluster focused on the security stack. Ingress can be + # added later without changing the cluster definition. + extraArgs: + - arg: --disable=traefik + nodeFilters: + - server:* + k3d: + wait: true + timeout: 120s + kubeconfig: + updateDefaultKubeconfig: true + switchCurrentContext: true + +ports: + - port: 8080:80 + nodeFilters: + - loadbalancer + - port: 8443:443 + nodeFilters: + - loadbalancer + diff --git a/policies/kyverno/kustomization.yaml b/policies/kyverno/kustomization.yaml new file mode 100644 index 0000000..076d7f1 --- /dev/null +++ b/policies/kyverno/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - namespace.yaml + - phase-2-placeholder.yaml + diff --git a/policies/kyverno/namespace.yaml b/policies/kyverno/namespace.yaml new file mode 100644 index 0000000..79ce09a --- /dev/null +++ b/policies/kyverno/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: security-baseline + labels: + security-baseline/open: "true" + diff --git a/policies/kyverno/phase-2-placeholder.yaml b/policies/kyverno/phase-2-placeholder.yaml new file mode 100644 index 0000000..ea868e1 --- /dev/null +++ b/policies/kyverno/phase-2-placeholder.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: policy-baseline-status + namespace: security-baseline +data: + phase: "2" + status: "GitOps target established; Kyverno policies arrive in phase 4" + diff --git a/terraform/cloud-cluster/README.md b/terraform/cloud-cluster/README.md new file mode 100644 index 0000000..f2e27ad --- /dev/null +++ b/terraform/cloud-cluster/README.md @@ -0,0 +1,27 @@ +# Optional production-style path + +This directory provisions one encrypted Ubuntu EC2 host with a deliberately +small network surface. It is a scaffold for the documented cloud path; the +local k3d path remains the primary, cost-free portfolio demo. + +The AWS account, region, AMI, SSH key pair, and administrator CIDR are inputs, +not repository values. Do not commit a real `terraform.tfvars` file or private +keys. The security group intentionally exposes SSH and the Kubernetes API only +to `admin_cidr`; add any public application ports explicitly when needed. + +Example workflow: + +```bash +cp terraform.tfvars.example terraform.tfvars +# Edit every replace-* value and set admin_cidr to your current IP /32. +terraform init +terraform validate +terraform plan +terraform apply +terraform output -raw public_ip +``` + +Then run `ansible/bootstrap-k3s.yml` against the output IP. `terraform destroy` +removes the lab resources when finished. This path is not required for the +portfolio demo and is not invoked by CI. + diff --git a/terraform/cloud-cluster/main.tf b/terraform/cloud-cluster/main.tf new file mode 100644 index 0000000..e92f90e --- /dev/null +++ b/terraform/cloud-cluster/main.tf @@ -0,0 +1,88 @@ +data "aws_availability_zones" "available" { + state = "available" +} + +locals { + availability_zone = coalesce(var.availability_zone, data.aws_availability_zones.available.names[0]) +} + +resource "aws_vpc" "cluster" { + cidr_block = "10.42.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { Name = "k8s-security-baseline" } +} + +resource "aws_subnet" "cluster" { + vpc_id = aws_vpc.cluster.id + cidr_block = "10.42.1.0/24" + availability_zone = local.availability_zone + + tags = { Name = "k8s-security-baseline" } +} + +resource "aws_internet_gateway" "cluster" { + vpc_id = aws_vpc.cluster.id +} + +resource "aws_route_table" "cluster" { + vpc_id = aws_vpc.cluster.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.cluster.id + } +} + +resource "aws_route_table_association" "cluster" { + subnet_id = aws_subnet.cluster.id + route_table_id = aws_route_table.cluster.id +} + +resource "aws_security_group" "cluster" { + name = "k8s-security-baseline" + description = "Minimal access for the optional single-node K3s lab host" + vpc_id = aws_vpc.cluster.id + + ingress { + description = "SSH from the administrator CIDR" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = [var.admin_cidr] + } + + ingress { + description = "Kubernetes API from the administrator CIDR" + from_port = 6443 + to_port = 6443 + protocol = "tcp" + cidr_blocks = [var.admin_cidr] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_instance" "k3s" { + ami = var.ami_id + instance_type = var.instance_type + subnet_id = aws_subnet.cluster.id + vpc_security_group_ids = [aws_security_group.cluster.id] + key_name = var.ssh_key_name + associate_public_ip_address = true + + root_block_device { + volume_size = 30 + volume_type = "gp3" + encrypted = true + } + + tags = { Name = "k8s-security-baseline" } +} + diff --git a/terraform/cloud-cluster/outputs.tf b/terraform/cloud-cluster/outputs.tf new file mode 100644 index 0000000..6966d68 --- /dev/null +++ b/terraform/cloud-cluster/outputs.tf @@ -0,0 +1,10 @@ +output "public_ip" { + description = "Public IP to place in the Ansible inventory." + value = aws_instance.k3s.public_ip +} + +output "ssh_command" { + description = "SSH command for initial connectivity testing." + value = "ssh ubuntu@${aws_instance.k3s.public_ip}" +} + diff --git a/terraform/cloud-cluster/terraform.tfvars.example b/terraform/cloud-cluster/terraform.tfvars.example new file mode 100644 index 0000000..7522a9c --- /dev/null +++ b/terraform/cloud-cluster/terraform.tfvars.example @@ -0,0 +1,7 @@ +aws_region = "us-east-1" +availability_zone = null +instance_type = "t3.medium" +ssh_key_name = "replace-with-existing-key-pair" +admin_cidr = "203.0.113.10/32" +ami_id = "replace-with-ubuntu-22.04-ami-for-your-region" + diff --git a/terraform/cloud-cluster/variables.tf b/terraform/cloud-cluster/variables.tf new file mode 100644 index 0000000..3f7bae1 --- /dev/null +++ b/terraform/cloud-cluster/variables.tf @@ -0,0 +1,33 @@ +variable "aws_region" { + description = "AWS region for the optional production-style K3s host." + type = string + default = "us-east-1" +} + +variable "availability_zone" { + description = "Availability zone for the subnet. Leave null to use the first AZ." + type = string + default = null +} + +variable "instance_type" { + description = "EC2 instance type for the lab node." + type = string + default = "t3.medium" +} + +variable "ssh_key_name" { + description = "Existing EC2 key pair name used for Ansible bootstrap." + type = string +} + +variable "admin_cidr" { + description = "CIDR allowed to SSH to the node; restrict this to your admin IP." + type = string +} + +variable "ami_id" { + description = "Ubuntu 22.04 LTS AMI ID for the selected region." + type = string +} + diff --git a/terraform/cloud-cluster/versions.tf b/terraform/cloud-cluster/versions.tf new file mode 100644 index 0000000..4745610 --- /dev/null +++ b/terraform/cloud-cluster/versions.tf @@ -0,0 +1,15 @@ +terraform { + required_version = ">= 1.6.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +provider "aws" { + region = var.aws_region +} + diff --git a/test-workloads/kustomization.yaml b/test-workloads/kustomization.yaml new file mode 100644 index 0000000..3692bef --- /dev/null +++ b/test-workloads/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - phase-2-placeholder.yaml + diff --git a/test-workloads/phase-2-placeholder.yaml b/test-workloads/phase-2-placeholder.yaml new file mode 100644 index 0000000..dc40885 --- /dev/null +++ b/test-workloads/phase-2-placeholder.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-workloads-status + namespace: security-baseline +data: + phase: "2" + status: "GitOps target established; security test workloads arrive in phase 6" +