first commit

This commit is contained in:
2026-08-09 19:52:19 -04:00
commit f12fb4d7a1
25 changed files with 597 additions and 0 deletions
+23
View File
@@ -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/
+38
View File
@@ -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
+17
View File
@@ -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 organizations OS baseline, firewall model, HA topology, and secret
management. RKE2 can replace K3s here if the target environment requires it.
+75
View File
@@ -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
+9
View File
@@ -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
+36
View File
@@ -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.
+24
View File
@@ -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
+29
View File
@@ -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
+23
View File
@@ -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
+24
View File
@@ -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
+23
View File
@@ -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
+6
View File
@@ -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
+21
View File
@@ -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
```
+33
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- phase-2-placeholder.yaml
+7
View File
@@ -0,0 +1,7 @@
apiVersion: v1
kind: Namespace
metadata:
name: security-baseline
labels:
security-baseline/open: "true"
@@ -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"
+27
View File
@@ -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.
+88
View File
@@ -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" }
}
+10
View File
@@ -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}"
}
@@ -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"
+33
View File
@@ -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
}
+15
View File
@@ -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
}
+5
View File
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- phase-2-placeholder.yaml
+9
View File
@@ -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"