Initial commit.

This commit is contained in:
2025-04-27 14:57:00 -07:00
commit 84376fb3d5
63 changed files with 5645 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
# Infrastructure setup scripts
Creates a fully functional personal cloud infrastructure on a bare metal Kubernetes (k3s) cluster that provides:
1. **External access** to services via configured domain names (using ${DOMAIN})
2. **Internal-only access** to admin interfaces (via internal.${DOMAIN} subdomains)
3. **Secure traffic routing** with automatic TLS
4. **Reliable networking** with proper load balancing
## Architecture
```
Internet → External DNS → MetalLB LoadBalancer → Traefik → Kubernetes Services
Internal DNS
Internal Network
```
## Key Components
- **MetalLB** - Provides load balancing for bare metal clusters
- **Traefik** - Handles ingress traffic, TLS termination, and routing
- **cert-manager** - Manages TLS certificates
- **CoreDNS** - Provides DNS resolution for services
- **Kubernetes Dashboard** - Web UI for cluster management (accessible via https://dashboard.internal.${DOMAIN})
## Configuration Approach
All infrastructure components use a consistent configuration approach:
1. **Environment Variables** - All configuration settings are managed using environment variables loaded by running `source load-env.sh`
2. **Template Files** - Configuration files use templates with `${VARIABLE}` syntax
3. **Setup Scripts** - Each component has a dedicated script in `infrastructure_setup/` for installation and configuration
## Idempotent Design
All setup scripts are designed to be idempotent:
- Scripts can be run multiple times without causing harm
- Each script checks for existing resources before creating new ones
- Configuration updates are applied cleanly without duplication
- Failed or interrupted setups can be safely retried
- Changes to configuration will be properly applied on subsequent runs
This idempotent approach ensures consistent, reliable infrastructure setup and allows for incremental changes without requiring a complete teardown and rebuild.

View File

@@ -0,0 +1,19 @@
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-internal-sovereign-cloud
namespace: internal
spec:
secretName: wildcard-internal-sovereign-cloud-tls
dnsNames:
- "*.internal.${DOMAIN}"
- "internal.${DOMAIN}"
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
duration: 2160h # 90 days
renewBefore: 360h # 15 days
privateKey:
algorithm: RSA
size: 2048

View File

@@ -0,0 +1,26 @@
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: ${EMAIL}
privateKeySecretRef:
name: letsencrypt-prod
server: https://acme-v02.api.letsencrypt.org/directory
solvers:
# DNS-01 solver for wildcard certificates
- dns01:
cloudflare:
email: ${EMAIL}
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
selector:
dnsZones:
- "${CLOUDFLARE_DOMAIN}" # This will cover all subdomains
# Keep the HTTP-01 solver for non-wildcard certificates
- http01:
ingress:
class: traefik

View File

@@ -0,0 +1,26 @@
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
email: ${EMAIL}
privateKeySecretRef:
name: letsencrypt-staging
server: https://acme-staging-v02.api.letsencrypt.org/directory
solvers:
# DNS-01 solver for wildcard certificates
- dns01:
cloudflare:
email: ${EMAIL}
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
selector:
dnsZones:
- "${DOMAIN}" # This will cover all subdomains
# Keep the HTTP-01 solver for non-wildcard certificates
- http01:
ingress:
class: traefik

View File

@@ -0,0 +1,19 @@
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-sovereign-cloud
namespace: default
spec:
secretName: wildcard-sovereign-cloud-tls
dnsNames:
- "*.${DOMAIN}"
- "${DOMAIN}"
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
duration: 2160h # 90 days
renewBefore: 360h # 15 days
privateKey:
algorithm: RSA
size: 2048

View File

@@ -0,0 +1,48 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
errors
health
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
hosts {
192.168.8.218 box-01
192.168.8.222 civil
192.168.8.240 traefik.${DOMAIN}
192.168.8.241 dns.internal.${DOMAIN}
# Test records
192.168.8.240 test.${DOMAIN}
192.168.8.240 example-app.${DOMAIN}
192.168.8.240 civilsociety.${DOMAIN}
192.168.8.241 test.internal.${DOMAIN}
192.168.8.240 example-admin.internal.${DOMAIN}
192.168.8.240 dashboard.internal.${DOMAIN}
192.168.8.240 kubernetes-dashboard.internal.${DOMAIN}
ttl 60
reload 15s
fallthrough
}
prometheus :9153
forward . 8.8.8.8 8.8.4.4 {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
import /etc/coredns/custom/*.override
}
import /etc/coredns/custom/*.server
NodeHosts: |
# This field needs to remain for compatibility, even if empty
# Host entries are now in the Corefile hosts section

View File

@@ -0,0 +1,25 @@
---
apiVersion: v1
kind: Service
metadata:
name: coredns-lb
namespace: kube-system
annotations:
metallb.universe.tf/loadBalancerIPs: "192.168.8.241"
spec:
type: LoadBalancer
ports:
- name: dns
port: 53
protocol: UDP
targetPort: 53
- name: dns-tcp
port: 53
protocol: TCP
targetPort: 53
- name: metrics
port: 9153
protocol: TCP
targetPort: 9153
selector:
k8s-app: kube-dns

View File

@@ -0,0 +1,41 @@
---
# Split-horizon DNS configuration for CoreDNS
# This allows different DNS responses for internal vs external domains
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns-custom
namespace: kube-system
data:
internal-zones.server: |
# Internal zone configuration for *.internal.${DOMAIN}
internal.${DOMAIN} {
errors
log
hosts {
192.168.8.240 example-admin.internal.${DOMAIN}
192.168.8.240 dashboard.internal.${DOMAIN}
192.168.8.241 test.internal.${DOMAIN}
fallthrough
}
cache 30
# Use kubernetes service discovery for internal services
kubernetes cluster.local {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
# Forward to Google DNS if not found locally
forward . 8.8.8.8 8.8.4.4
}
external-zones.server: |
# External zone configuration for *.${DOMAIN}
${DOMAIN} {
errors
log
cache 30
# For external services, forward to Cloudflare for correct public resolution
forward . 1.1.1.1 8.8.8.8 {
max_concurrent 1000
}
}

View File

@@ -0,0 +1,69 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
namespace: externaldns
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services", "endpoints", "pods"]
verbs: ["get", "watch", "list"]
- apiGroups: ["extensions", "networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "watch", "list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: externaldns
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
namespace: externaldns
spec:
selector:
matchLabels:
app: external-dns
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.k8s.io/external-dns/external-dns:v0.13.4
args:
- --source=service
- --source=ingress
- --provider=cloudflare
- --txt-owner-id=${CLUSTER_ID}
- --log-level=debug
- --publish-internal-services # Also publish internal services
- --no-cloudflare-proxied
env:
- name: CF_API_TOKEN
valueFrom:
secretKeyRef:
name: cloudflare-api-token
key: api-token

347
infrastructure_setup/get_helm.sh Executable file
View File

@@ -0,0 +1,347 @@
#!/usr/bin/env bash
# Copyright The Helm Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# The install script is based off of the MIT-licensed script from glide,
# the package manager for Go: https://github.com/Masterminds/glide.sh/blob/master/get
: ${BINARY_NAME:="helm"}
: ${USE_SUDO:="true"}
: ${DEBUG:="false"}
: ${VERIFY_CHECKSUM:="true"}
: ${VERIFY_SIGNATURES:="false"}
: ${HELM_INSTALL_DIR:="/usr/local/bin"}
: ${GPG_PUBRING:="pubring.kbx"}
HAS_CURL="$(type "curl" &> /dev/null && echo true || echo false)"
HAS_WGET="$(type "wget" &> /dev/null && echo true || echo false)"
HAS_OPENSSL="$(type "openssl" &> /dev/null && echo true || echo false)"
HAS_GPG="$(type "gpg" &> /dev/null && echo true || echo false)"
HAS_GIT="$(type "git" &> /dev/null && echo true || echo false)"
HAS_TAR="$(type "tar" &> /dev/null && echo true || echo false)"
# initArch discovers the architecture for this system.
initArch() {
ARCH=$(uname -m)
case $ARCH in
armv5*) ARCH="armv5";;
armv6*) ARCH="armv6";;
armv7*) ARCH="arm";;
aarch64) ARCH="arm64";;
x86) ARCH="386";;
x86_64) ARCH="amd64";;
i686) ARCH="386";;
i386) ARCH="386";;
esac
}
# initOS discovers the operating system for this system.
initOS() {
OS=$(echo `uname`|tr '[:upper:]' '[:lower:]')
case "$OS" in
# Minimalist GNU for Windows
mingw*|cygwin*) OS='windows';;
esac
}
# runs the given command as root (detects if we are root already)
runAsRoot() {
if [ $EUID -ne 0 -a "$USE_SUDO" = "true" ]; then
sudo "${@}"
else
"${@}"
fi
}
# verifySupported checks that the os/arch combination is supported for
# binary builds, as well whether or not necessary tools are present.
verifySupported() {
local supported="darwin-amd64\ndarwin-arm64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-ppc64le\nlinux-s390x\nlinux-riscv64\nwindows-amd64\nwindows-arm64"
if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then
echo "No prebuilt binary for ${OS}-${ARCH}."
echo "To build from source, go to https://github.com/helm/helm"
exit 1
fi
if [ "${HAS_CURL}" != "true" ] && [ "${HAS_WGET}" != "true" ]; then
echo "Either curl or wget is required"
exit 1
fi
if [ "${VERIFY_CHECKSUM}" == "true" ] && [ "${HAS_OPENSSL}" != "true" ]; then
echo "In order to verify checksum, openssl must first be installed."
echo "Please install openssl or set VERIFY_CHECKSUM=false in your environment."
exit 1
fi
if [ "${VERIFY_SIGNATURES}" == "true" ]; then
if [ "${HAS_GPG}" != "true" ]; then
echo "In order to verify signatures, gpg must first be installed."
echo "Please install gpg or set VERIFY_SIGNATURES=false in your environment."
exit 1
fi
if [ "${OS}" != "linux" ]; then
echo "Signature verification is currently only supported on Linux."
echo "Please set VERIFY_SIGNATURES=false or verify the signatures manually."
exit 1
fi
fi
if [ "${HAS_GIT}" != "true" ]; then
echo "[WARNING] Could not find git. It is required for plugin installation."
fi
if [ "${HAS_TAR}" != "true" ]; then
echo "[ERROR] Could not find tar. It is required to extract the helm binary archive."
exit 1
fi
}
# checkDesiredVersion checks if the desired version is available.
checkDesiredVersion() {
if [ "x$DESIRED_VERSION" == "x" ]; then
# Get tag from release URL
local latest_release_url="https://get.helm.sh/helm-latest-version"
local latest_release_response=""
if [ "${HAS_CURL}" == "true" ]; then
latest_release_response=$( curl -L --silent --show-error --fail "$latest_release_url" 2>&1 || true )
elif [ "${HAS_WGET}" == "true" ]; then
latest_release_response=$( wget "$latest_release_url" -q -O - 2>&1 || true )
fi
TAG=$( echo "$latest_release_response" | grep '^v[0-9]' )
if [ "x$TAG" == "x" ]; then
printf "Could not retrieve the latest release tag information from %s: %s\n" "${latest_release_url}" "${latest_release_response}"
exit 1
fi
else
TAG=$DESIRED_VERSION
fi
}
# checkHelmInstalledVersion checks which version of helm is installed and
# if it needs to be changed.
checkHelmInstalledVersion() {
if [[ -f "${HELM_INSTALL_DIR}/${BINARY_NAME}" ]]; then
local version=$("${HELM_INSTALL_DIR}/${BINARY_NAME}" version --template="{{ .Version }}")
if [[ "$version" == "$TAG" ]]; then
echo "Helm ${version} is already ${DESIRED_VERSION:-latest}"
return 0
else
echo "Helm ${TAG} is available. Changing from version ${version}."
return 1
fi
else
return 1
fi
}
# downloadFile downloads the latest binary package and also the checksum
# for that binary.
downloadFile() {
HELM_DIST="helm-$TAG-$OS-$ARCH.tar.gz"
DOWNLOAD_URL="https://get.helm.sh/$HELM_DIST"
CHECKSUM_URL="$DOWNLOAD_URL.sha256"
HELM_TMP_ROOT="$(mktemp -dt helm-installer-XXXXXX)"
HELM_TMP_FILE="$HELM_TMP_ROOT/$HELM_DIST"
HELM_SUM_FILE="$HELM_TMP_ROOT/$HELM_DIST.sha256"
echo "Downloading $DOWNLOAD_URL"
if [ "${HAS_CURL}" == "true" ]; then
curl -SsL "$CHECKSUM_URL" -o "$HELM_SUM_FILE"
curl -SsL "$DOWNLOAD_URL" -o "$HELM_TMP_FILE"
elif [ "${HAS_WGET}" == "true" ]; then
wget -q -O "$HELM_SUM_FILE" "$CHECKSUM_URL"
wget -q -O "$HELM_TMP_FILE" "$DOWNLOAD_URL"
fi
}
# verifyFile verifies the SHA256 checksum of the binary package
# and the GPG signatures for both the package and checksum file
# (depending on settings in environment).
verifyFile() {
if [ "${VERIFY_CHECKSUM}" == "true" ]; then
verifyChecksum
fi
if [ "${VERIFY_SIGNATURES}" == "true" ]; then
verifySignatures
fi
}
# installFile installs the Helm binary.
installFile() {
HELM_TMP="$HELM_TMP_ROOT/$BINARY_NAME"
mkdir -p "$HELM_TMP"
tar xf "$HELM_TMP_FILE" -C "$HELM_TMP"
HELM_TMP_BIN="$HELM_TMP/$OS-$ARCH/helm"
echo "Preparing to install $BINARY_NAME into ${HELM_INSTALL_DIR}"
runAsRoot cp "$HELM_TMP_BIN" "$HELM_INSTALL_DIR/$BINARY_NAME"
echo "$BINARY_NAME installed into $HELM_INSTALL_DIR/$BINARY_NAME"
}
# verifyChecksum verifies the SHA256 checksum of the binary package.
verifyChecksum() {
printf "Verifying checksum... "
local sum=$(openssl sha1 -sha256 ${HELM_TMP_FILE} | awk '{print $2}')
local expected_sum=$(cat ${HELM_SUM_FILE})
if [ "$sum" != "$expected_sum" ]; then
echo "SHA sum of ${HELM_TMP_FILE} does not match. Aborting."
exit 1
fi
echo "Done."
}
# verifySignatures obtains the latest KEYS file from GitHub main branch
# as well as the signature .asc files from the specific GitHub release,
# then verifies that the release artifacts were signed by a maintainer's key.
verifySignatures() {
printf "Verifying signatures... "
local keys_filename="KEYS"
local github_keys_url="https://raw.githubusercontent.com/helm/helm/main/${keys_filename}"
if [ "${HAS_CURL}" == "true" ]; then
curl -SsL "${github_keys_url}" -o "${HELM_TMP_ROOT}/${keys_filename}"
elif [ "${HAS_WGET}" == "true" ]; then
wget -q -O "${HELM_TMP_ROOT}/${keys_filename}" "${github_keys_url}"
fi
local gpg_keyring="${HELM_TMP_ROOT}/keyring.gpg"
local gpg_homedir="${HELM_TMP_ROOT}/gnupg"
mkdir -p -m 0700 "${gpg_homedir}"
local gpg_stderr_device="/dev/null"
if [ "${DEBUG}" == "true" ]; then
gpg_stderr_device="/dev/stderr"
fi
gpg --batch --quiet --homedir="${gpg_homedir}" --import "${HELM_TMP_ROOT}/${keys_filename}" 2> "${gpg_stderr_device}"
gpg --batch --no-default-keyring --keyring "${gpg_homedir}/${GPG_PUBRING}" --export > "${gpg_keyring}"
local github_release_url="https://github.com/helm/helm/releases/download/${TAG}"
if [ "${HAS_CURL}" == "true" ]; then
curl -SsL "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc" -o "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc"
curl -SsL "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc" -o "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc"
elif [ "${HAS_WGET}" == "true" ]; then
wget -q -O "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc" "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc"
wget -q -O "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc" "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc"
fi
local error_text="If you think this might be a potential security issue,"
error_text="${error_text}\nplease see here: https://github.com/helm/community/blob/master/SECURITY.md"
local num_goodlines_sha=$(gpg --verify --keyring="${gpg_keyring}" --status-fd=1 "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc" 2> "${gpg_stderr_device}" | grep -c -E '^\[GNUPG:\] (GOODSIG|VALIDSIG)')
if [[ ${num_goodlines_sha} -lt 2 ]]; then
echo "Unable to verify the signature of helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256!"
echo -e "${error_text}"
exit 1
fi
local num_goodlines_tar=$(gpg --verify --keyring="${gpg_keyring}" --status-fd=1 "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc" 2> "${gpg_stderr_device}" | grep -c -E '^\[GNUPG:\] (GOODSIG|VALIDSIG)')
if [[ ${num_goodlines_tar} -lt 2 ]]; then
echo "Unable to verify the signature of helm-${TAG}-${OS}-${ARCH}.tar.gz!"
echo -e "${error_text}"
exit 1
fi
echo "Done."
}
# fail_trap is executed if an error occurs.
fail_trap() {
result=$?
if [ "$result" != "0" ]; then
if [[ -n "$INPUT_ARGUMENTS" ]]; then
echo "Failed to install $BINARY_NAME with the arguments provided: $INPUT_ARGUMENTS"
help
else
echo "Failed to install $BINARY_NAME"
fi
echo -e "\tFor support, go to https://github.com/helm/helm."
fi
cleanup
exit $result
}
# testVersion tests the installed client to make sure it is working.
testVersion() {
set +e
HELM="$(command -v $BINARY_NAME)"
if [ "$?" = "1" ]; then
echo "$BINARY_NAME not found. Is $HELM_INSTALL_DIR on your "'$PATH?'
exit 1
fi
set -e
}
# help provides possible cli installation arguments
help () {
echo "Accepted cli arguments are:"
echo -e "\t[--help|-h ] ->> prints this help"
echo -e "\t[--version|-v <desired_version>] . When not defined it fetches the latest release tag from the Helm CDN"
echo -e "\te.g. --version v3.0.0 or -v canary"
echo -e "\t[--no-sudo] ->> install without sudo"
}
# cleanup temporary files to avoid https://github.com/helm/helm/issues/2977
cleanup() {
if [[ -d "${HELM_TMP_ROOT:-}" ]]; then
rm -rf "$HELM_TMP_ROOT"
fi
}
# Execution
#Stop execution on any error
trap "fail_trap" EXIT
set -e
# Set debug if desired
if [ "${DEBUG}" == "true" ]; then
set -x
fi
# Parsing input arguments (if any)
export INPUT_ARGUMENTS="${@}"
set -u
while [[ $# -gt 0 ]]; do
case $1 in
'--version'|-v)
shift
if [[ $# -ne 0 ]]; then
export DESIRED_VERSION="${1}"
if [[ "$1" != "v"* ]]; then
echo "Expected version arg ('${DESIRED_VERSION}') to begin with 'v', fixing..."
export DESIRED_VERSION="v${1}"
fi
else
echo -e "Please provide the desired version. e.g. --version v3.0.0 or -v canary"
exit 0
fi
;;
'--no-sudo')
USE_SUDO="false"
;;
'--help'|-h)
help
exit 0
;;
*) exit 1
;;
esac
shift
done
set +u
initArch
initOS
verifySupported
checkDesiredVersion
if ! checkHelmInstalledVersion; then
downloadFile
verifyFile
installFile
fi
testVersion
cleanup

View File

@@ -0,0 +1,103 @@
---
# Certificate for the dashboard
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: kubernetes-dashboard-tls
namespace: kubernetes-dashboard
spec:
secretName: kubernetes-dashboard-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- "dashboard.internal.${DOMAIN}"
duration: 2160h # 90 days
renewBefore: 360h # 15 days
privateKey:
algorithm: RSA
size: 2048
---
# Internal-only middleware
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: internal-only
namespace: kubernetes-dashboard
spec:
ipWhiteList:
# Restrict to local private network ranges
sourceRange:
- 127.0.0.1/32 # localhost
- 10.0.0.0/8 # Private network
- 172.16.0.0/12 # Private network
- 192.168.0.0/16 # Private network
---
# HTTPS redirect middleware
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: dashboard-redirect-scheme
namespace: kubernetes-dashboard
spec:
redirectScheme:
scheme: https
permanent: true
---
# IngressRoute for Dashboard
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: kubernetes-dashboard-https
namespace: kubernetes-dashboard
spec:
entryPoints:
- websecure
routes:
- match: Host(`dashboard.internal.${DOMAIN}`)
kind: Rule
middlewares:
- name: internal-only
namespace: kubernetes-dashboard
services:
- name: kubernetes-dashboard
port: 443
serversTransport: dashboard-transport
tls:
secretName: kubernetes-dashboard-tls
---
# HTTP to HTTPS redirect
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: kubernetes-dashboard-http
namespace: kubernetes-dashboard
spec:
entryPoints:
- web
routes:
- match: Host(`dashboard.internal.${DOMAIN}`)
kind: Rule
middlewares:
- name: dashboard-redirect-scheme
namespace: kubernetes-dashboard
services:
- name: kubernetes-dashboard
port: 443
serversTransport: dashboard-transport
---
# ServersTransport for HTTPS backend with skip verify
apiVersion: traefik.containo.us/v1alpha1
kind: ServersTransport
metadata:
name: dashboard-transport
namespace: kubernetes-dashboard
spec:
insecureSkipVerify: true
serverName: dashboard.internal.${DOMAIN}

View File

@@ -0,0 +1,21 @@
---
# Define IP address pool for MetalLB
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: production
namespace: metallb-system
spec:
addresses:
- ${CLUSTER_LOAD_BALANCER_RANGE}
---
# Define Layer 2 advertisement for the IP pool
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: l2-advertisement
namespace: metallb-system
spec:
ipAddressPools:
- production

View File

@@ -0,0 +1,16 @@
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: metallb
namespace: kube-system
spec:
valuesContent: |-
# The new configuration format for MetalLB v0.13.0+
apiVersion: v1
# We'll use IPAddressPool and L2Advertisement CRs instead of the deprecated configInline
# Need to install the CRDs separately
crds:
enabled: true
# Disable controller.configInline since it's deprecated
controller:
configInline: null

View File

@@ -0,0 +1,21 @@
---
# Define IP address pool for MetalLB using the new format
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: production
namespace: metallb-system
spec:
addresses:
- 192.168.8.240-192.168.8.250
---
# Define Layer 2 advertisement for the IP pool using the new format
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: l2-advertisement
namespace: metallb-system
spec:
ipAddressPools:
- production

View File

@@ -0,0 +1,46 @@
#!/bin/bash
set -e
# Navigate to script directory
SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")"
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
cd "$SCRIPT_DIR"
echo "Setting up infrastructure components for k3s..."
# Make all script files executable
chmod +x *.sh
# Utils
./setup-utils.sh
# Setup MetalLB (must be first for IP allocation)
./setup-metallb.sh
# Setup Traefik
./setup-traefik.sh
# Setup CoreDNS
./setup-coredns.sh
# Setup cert-manager
./setup-cert-manager.sh
# Setup ExternalDNS
./setup-externaldns.sh
# Setup Kubernetes Dashboard
./setup-dashboard.sh
echo "Infrastructure setup complete!"
echo
echo "Next steps:"
echo "1. Install Helm charts for non-infrastructure components"
echo "2. Access the dashboard at: https://dashboard.internal.${DOMAIN}"
echo "3. Get the dashboard token with: ./bin/dashboard-token"
echo
echo "To verify components, run:"
echo "- kubectl get pods -n cert-manager"
echo "- kubectl get pods -n externaldns"
echo "- kubectl get pods -n kubernetes-dashboard"
echo "- kubectl get clusterissuers"

View File

@@ -0,0 +1,102 @@
#!/bin/bash
set -e
# Navigate to script directory
SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")"
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
cd "$SCRIPT_DIR"
# Source environment variables
if [[ -f "../load-env.sh" ]]; then
source ../load-env.sh
fi
echo "Setting up cert-manager..."
# Create cert-manager namespace
kubectl create namespace cert-manager --dry-run=client -o yaml | kubectl apply -f -
# Install cert-manager using the official installation method
# This installs CRDs, controllers, and webhook components
echo "Installing cert-manager components..."
# Using stable URL for cert-manager installation
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.1/cert-manager.yaml || \
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.13.1/cert-manager.yaml
# Wait for cert-manager to be ready
echo "Waiting for cert-manager to be ready..."
kubectl wait --for=condition=Available deployment/cert-manager -n cert-manager --timeout=120s
kubectl wait --for=condition=Available deployment/cert-manager-cainjector -n cert-manager --timeout=120s
kubectl wait --for=condition=Available deployment/cert-manager-webhook -n cert-manager --timeout=120s
# Add delay to allow webhook to be fully ready
echo "Waiting additional time for cert-manager webhook to be fully operational..."
sleep 30
# Setup Cloudflare API token for DNS01 challenges
if [[ -n "${CLOUDFLARE_API_TOKEN}" ]]; then
echo "Creating Cloudflare API token secret in cert-manager namespace..."
kubectl create secret generic cloudflare-api-token \
--namespace cert-manager \
--from-literal=api-token="${CLOUDFLARE_API_TOKEN}" \
--dry-run=client -o yaml | kubectl apply -f -
# Create internal namespace if it doesn't exist
echo "Creating internal namespace if it doesn't exist..."
kubectl create namespace internal --dry-run=client -o yaml | kubectl apply -f -
# Create the same secret in the internal namespace
echo "Creating Cloudflare API token secret in internal namespace..."
kubectl create secret generic cloudflare-api-token \
--namespace internal \
--from-literal=api-token="${CLOUDFLARE_API_TOKEN}" \
--dry-run=client -o yaml | kubectl apply -f -
else
echo "Warning: CLOUDFLARE_API_TOKEN not set. DNS01 challenges will not work."
fi
# Apply Let's Encrypt issuers
echo "Creating Let's Encrypt issuers..."
cat ${SCRIPT_DIR}/cert-manager/letsencrypt-staging-dns01.yaml | envsubst | kubectl apply -f -
cat ${SCRIPT_DIR}/cert-manager/letsencrypt-prod-dns01.yaml | envsubst | kubectl apply -f -
# Wait for issuers to be ready
echo "Waiting for Let's Encrypt issuers to be ready..."
sleep 10
# Apply wildcard certificates
echo "Creating wildcard certificates..."
cat ${SCRIPT_DIR}/cert-manager/internal-wildcard-certificate.yaml | envsubst | kubectl apply -f -
cat ${SCRIPT_DIR}/cert-manager/wildcard-certificate.yaml | envsubst | kubectl apply -f -
echo "Wildcard certificate creation initiated. This may take some time to complete depending on DNS propagation."
# Wait for the certificates to be issued (with a timeout)
echo "Waiting for wildcard certificates to be ready (this may take several minutes)..."
kubectl wait --for=condition=Ready certificate wildcard-soverign-cloud -n default --timeout=300s || true
kubectl wait --for=condition=Ready certificate wildcard-internal-sovereign-cloud -n internal --timeout=300s || true
# Copy the internal wildcard certificate to example-admin namespace
echo "Copying internal wildcard certificate to example-admin namespace..."
if kubectl get namespace example-admin &>/dev/null; then
# Create example-admin namespace if it doesn't exist
kubectl create namespace example-admin --dry-run=client -o yaml | kubectl apply -f -
# Get the internal wildcard certificate secret and copy it to example-admin namespace
if kubectl get secret wildcard-internal-sovereign-cloud-tls -n internal &>/dev/null; then
kubectl get secret wildcard-internal-sovereign-cloud-tls -n internal -o yaml | \
sed 's/namespace: internal/namespace: example-admin/' | \
kubectl apply -f -
echo "Certificate copied to example-admin namespace"
else
echo "Internal wildcard certificate not ready yet. Please manually copy it later with:"
echo " kubectl get secret wildcard-internal-sovereign-cloud-tls -n internal -o yaml | \\"
echo " sed 's/namespace: internal/namespace: example-admin/' | \\"
echo " kubectl apply -f -"
fi
fi
echo "cert-manager setup complete!"
echo ""
echo "To verify the installation:"
echo " kubectl get pods -n cert-manager"
echo " kubectl get clusterissuers"

View File

@@ -0,0 +1,35 @@
#!/bin/bash
set -e
SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")"
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
cd "$SCRIPT_DIR"
# Source environment variables
if [[ -f "../load-env.sh" ]]; then
source ../load-env.sh
fi
echo "Setting up CoreDNS for k3s..."
echo "Script directory: ${SCRIPT_DIR}"
echo "Current directory: $(pwd)"
# Apply the custom config for the k3s-provided CoreDNS
echo "Applying CoreDNS configuration..."
echo "Looking for file: ${SCRIPT_DIR}/coredns/coredns-config.yaml"
# Simply use envsubst for variable expansion and apply
cat "${SCRIPT_DIR}/coredns/coredns-config.yaml" | envsubst | kubectl apply -f -
# Apply the split-horizon configuration
echo "Applying split-horizon DNS configuration..."
cat "${SCRIPT_DIR}/coredns/split-horizon.yaml" | envsubst | kubectl apply -f -
# Apply the LoadBalancer service for external access to CoreDNS
echo "Applying CoreDNS service configuration..."
cat "${SCRIPT_DIR}/coredns/coredns-service.yaml" | envsubst | kubectl apply -f -
# Restart CoreDNS pods to apply the changes
echo "Restarting CoreDNS pods to apply changes..."
kubectl delete pod -n kube-system -l k8s-app=kube-dns
echo "CoreDNS setup complete!"

View File

@@ -0,0 +1,94 @@
#!/bin/bash
set -e
# Store the script directory path for later use
SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")"
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
cd "$SCRIPT_DIR"
# Source environment variables
if [[ -f "../load-env.sh" ]]; then
source ../load-env.sh
fi
echo "Setting up Kubernetes Dashboard..."
# Apply the official dashboard installation
echo "Installing Kubernetes Dashboard core components..."
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml
# Create admin service account and token
cat << EOF | kubectl apply -f -
---
# Service Account and RBAC
apiVersion: v1
kind: ServiceAccount
metadata:
name: dashboard-admin
namespace: kubernetes-dashboard
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: dashboard-admin
subjects:
- kind: ServiceAccount
name: dashboard-admin
namespace: kubernetes-dashboard
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
---
# Token for dashboard-admin
apiVersion: v1
kind: Secret
metadata:
name: dashboard-admin-token
namespace: kubernetes-dashboard
annotations:
kubernetes.io/service-account.name: dashboard-admin
type: kubernetes.io/service-account-token
EOF
# Clean up any existing IngressRoute resources that might conflict
echo "Cleaning up any existing dashboard resources to prevent conflicts..."
# Clean up all IngressRoutes related to dashboard in both namespaces
kubectl delete ingressroute -n kubernetes-dashboard --all --ignore-not-found
kubectl delete ingressroute -n kube-system kubernetes-dashboard --ignore-not-found
kubectl delete ingressroute -n kube-system kubernetes-dashboard-alt --ignore-not-found
kubectl delete ingressroute -n kube-system kubernetes-dashboard-http --ignore-not-found
kubectl delete ingressroute -n kube-system kubernetes-dashboard-alt-http --ignore-not-found
# Clean up middleware in both namespaces
kubectl delete middleware -n kubernetes-dashboard --all --ignore-not-found
kubectl delete middleware -n kube-system dashboard-internal-only --ignore-not-found
kubectl delete middleware -n kube-system dashboard-redirect-scheme --ignore-not-found
# Clean up ServersTransport in both namespaces
kubectl delete serverstransport -n kubernetes-dashboard dashboard-transport --ignore-not-found
kubectl delete serverstransport -n kube-system dashboard-transport --ignore-not-found
# Apply the dashboard configuration
echo "Applying dashboard configuration in kube-system namespace..."
# Use just the kube-system version since it works better with Traefik
cat "${SCRIPT_DIR}/kubernetes-dashboard/dashboard-kube-system.yaml" | envsubst | kubectl apply -f -
# No need to manually update the CoreDNS ConfigMap anymore
# The setup-coredns.sh script now handles variable substitution correctly
# Restart CoreDNS to pick up the changes
kubectl delete pods -n kube-system -l k8s-app=kube-dns
echo "Restarted CoreDNS to pick up DNS changes"
# Wait for dashboard to be ready
echo "Waiting for Kubernetes Dashboard to be ready..."
kubectl rollout status deployment/kubernetes-dashboard -n kubernetes-dashboard --timeout=60s
echo "Kubernetes Dashboard setup complete!"
echo "Access the dashboard at: https://dashboard.internal.${DOMAIN}"
echo ""
echo "To get the authentication token, run:"
echo "./bin/dashboard-token"

View File

@@ -0,0 +1,55 @@
#!/bin/bash
set -e
# Navigate to script directory
SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")"
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
cd "$SCRIPT_DIR"
# Source environment variables
if [[ -f "../load-env.sh" ]]; then
source ../load-env.sh
fi
echo "Setting up ExternalDNS..."
# Create externaldns namespace
kubectl create namespace externaldns --dry-run=client -o yaml | kubectl apply -f -
# Setup Cloudflare API token secret for ExternalDNS
if [[ -n "${CLOUDFLARE_API_TOKEN}" ]]; then
echo "Creating Cloudflare API token secret..."
kubectl create secret generic cloudflare-api-token \
--namespace externaldns \
--from-literal=api-token="${CLOUDFLARE_API_TOKEN}" \
--dry-run=client -o yaml | kubectl apply -f -
else
echo "Error: CLOUDFLARE_API_TOKEN not set. ExternalDNS will not work correctly."
exit 1
fi
# Apply ExternalDNS manifests with environment variables
echo "Deploying ExternalDNS..."
cat ${SCRIPT_DIR}/externaldns/externaldns.yaml | envsubst | kubectl apply -f -
# Wait for ExternalDNS to be ready
echo "Waiting for ExternalDNS to be ready..."
kubectl rollout status deployment/external-dns -n externaldns --timeout=60s
# Deploy test services if --test flag is provided
if [[ "$1" == "--test" ]]; then
echo "Deploying test services to verify ExternalDNS..."
cat ${SCRIPT_DIR}/externaldns/test-service.yaml | envsubst | kubectl apply -f -
cat ${SCRIPT_DIR}/externaldns/test-cname-service.yaml | envsubst | kubectl apply -f -
echo "Test services deployed at:"
echo "- test.${DOMAIN}"
echo "- test-cname.${DOMAIN} (CNAME record)"
echo "DNS records should be automatically created in Cloudflare within a few minutes."
fi
echo "ExternalDNS setup complete!"
echo ""
echo "To verify the installation:"
echo " kubectl get pods -n externaldns"
echo " kubectl logs -n externaldns -l app=external-dns -f"

View File

@@ -0,0 +1,36 @@
#!/bin/bash
set -e
SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")"
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
cd "$SCRIPT_DIR"
# Source environment variables
if [[ -f "../load-env.sh" ]]; then
source ../load-env.sh
fi
echo "Setting up MetalLB..."
# TODO: Remove the helm config in preference to a native config.
echo "Deploying MetalLB..."
cat ${SCRIPT_DIR}/metallb/metallb-helm-config.yaml | envsubst | kubectl apply -f -
echo "Waiting for MetalLB to be deployed..."
kubectl wait --for=condition=complete job -l helm.sh/chart=metallb -n kube-system --timeout=120s || echo "Warning: Timeout waiting for MetalLB Helm job"
echo "Waiting for MetalLB controller to be ready..."
kubectl get namespace metallb-system &>/dev/null || (echo "Waiting for metallb-system namespace to be created..." && sleep 30)
kubectl wait --for=condition=Available deployment -l app.kubernetes.io/instance=metallb -n metallb-system --timeout=60s || echo "Warning: Timeout waiting for controller deployment"
echo "Configuring MetalLB IP address pool..."
kubectl get namespace metallb-system &>/dev/null && \
kubectl apply -f "${SCRIPT_DIR}/metallb/metallb-pool.yaml" || \
echo "Warning: metallb-system namespace not ready yet. Pool configuration will be skipped. Run this script again in a few minutes."
echo "✅ MetalLB installed and configured"
echo ""
echo "To verify the installation:"
echo " kubectl get pods -n metallb-system"
echo " kubectl get ipaddresspools.metallb.io -n metallb-system"

View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -e
SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")"
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
cd "$SCRIPT_DIR"
# Source environment variables
if [[ -f "../load-env.sh" ]]; then
source ../load-env.sh
fi
echo "Setting up Traefik service and middleware for k3s..."
cat ${SCRIPT_DIR}/traefik/traefik-service.yaml | envsubst | kubectl apply -f -
cat ${SCRIPT_DIR}/traefik/internal-middleware.yaml | envsubst | kubectl apply -f -
echo "Traefik setup complete!"

View File

@@ -0,0 +1,15 @@
#!/bin/bash
set -e
SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")"
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
cd "$SCRIPT_DIR"
# Install gomplate
if command -v gomplate &> /dev/null; then
echo "gomplate is already installed."
exit 0
fi
curl -sSL https://github.com/hairyhenderson/gomplate/releases/latest/download/gomplate_linux-amd64 -o $HOME/.local/bin/gomplate
chmod +x $HOME/.local/bin/gomplate
echo "gomplate installed successfully."

View File

@@ -0,0 +1,13 @@
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: internal-only
namespace: kube-system
spec:
ipWhiteList:
# Restrict to local private network ranges - adjust these to match your network
sourceRange:
- 127.0.0.1/32 # localhost
- 10.0.0.0/8 # Private network
- 172.16.0.0/12 # Private network
- 192.168.0.0/16 # Private network

View File

@@ -0,0 +1,27 @@
---
# Traefik service configuration with static LoadBalancer IP
apiVersion: v1
kind: Service
metadata:
name: traefik
namespace: kube-system
annotations:
metallb.universe.tf/address-pool: production
metallb.universe.tf/allow-shared-ip: traefik-lb
labels:
app.kubernetes.io/instance: traefik-kube-system
app.kubernetes.io/name: traefik
spec:
type: LoadBalancer
loadBalancerIP: 192.168.8.240
selector:
app.kubernetes.io/instance: traefik-kube-system
app.kubernetes.io/name: traefik
ports:
- name: web
port: 80
targetPort: web
- name: websecure
port: 443
targetPort: websecure
externalTrafficPolicy: Local

File diff suppressed because it is too large Load Diff