diff --git a/network-security/index.ts b/network-security/index.ts new file mode 100644 index 0000000..0f63593 --- /dev/null +++ b/network-security/index.ts @@ -0,0 +1,83 @@ +import { DataKubernetesNamespaceV1 } from "@cdktf/provider-kubernetes/lib/data-kubernetes-namespace-v1"; +import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; +import { DataTerraformRemoteStateS3, TerraformStack } from "cdktf"; +import { Construct } from "constructs"; + +import { + RateLimitMiddleware, + IpAllowListMiddleware, + IpAllowListMiddlewareTCP, +} from "./traefik"; +import { ValkeyCluster } from "./valkey"; + +export class NetworkSecurity extends TerraformStack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const kubernetes = new KubernetesProvider(this, "kubernetes", { + configPath: "~/.kube/config", + }); + + const r2Endpoint = `${process.env.ACCOUNT_ID!}.r2.cloudflarestorage.com`; + + const coreServicesState = new DataTerraformRemoteStateS3( + this, + "core-services-state", + { + usePathStyle: true, + skipRegionValidation: true, + skipCredentialsValidation: true, + skipRequestingAccountId: true, + skipS3Checksum: true, + encrypt: true, + bucket: "terraform-state", + key: "core-services/terraform.tfstate", + endpoints: { + s3: `https://${r2Endpoint}`, + }, + region: "auto", + accessKey: process.env.ACCESS_KEY, + secretKey: process.env.SECRET_KEY, + }, + ); + + const namespaceName = coreServicesState.getString("namespace-output"); + const namespaceResource = new DataKubernetesNamespaceV1( + this, + "homelab-namespace", + { + provider: kubernetes, + metadata: { + name: namespaceName, + }, + }, + ); + const namespace = namespaceResource.metadata.name; + + new ValkeyCluster(this, "valkey-cluster", { + provider: kubernetes, + name: "valkey", + namespace, + }); + + new RateLimitMiddleware(this, "rate-limit", { + provider: kubernetes, + namespace, + name: "rate-limit", + }); + + new IpAllowListMiddleware(this, "internal-ip-allow-list", { + provider: kubernetes, + namespace, + name: "ip-allow-list", + sourceRanges: ["192.168.18.0/24", "10.43.0.0/16"], + }); + + new IpAllowListMiddlewareTCP(this, "tcp-internal-ip-allow-list", { + provider: kubernetes, + namespace, + name: "tcp-ip-allow-list", + sourceRanges: ["192.168.18.0/24", "10.42.0.0/16"], + }); + } +} diff --git a/network-security/traefik/index.ts b/network-security/traefik/index.ts new file mode 100644 index 0000000..0c29d72 --- /dev/null +++ b/network-security/traefik/index.ts @@ -0,0 +1,2 @@ +export { RateLimitMiddleware } from "./rateLimit"; +export { IpAllowListMiddleware, IpAllowListMiddlewareTCP } from "./ipAllowList"; diff --git a/network-security/traefik/ipAllowList.ts b/network-security/traefik/ipAllowList.ts new file mode 100644 index 0000000..f56cf7f --- /dev/null +++ b/network-security/traefik/ipAllowList.ts @@ -0,0 +1,64 @@ +import { Construct } from "constructs"; +import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest"; +import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; + +type IpAllowListMiddlewareOptions = { + provider: KubernetesProvider; + namespace: string; + name: string; + sourceRanges: string[]; +}; + +export class IpAllowListMiddleware extends Construct { + constructor( + scope: Construct, + id: string, + opts: IpAllowListMiddlewareOptions, + ) { + super(scope, id); + + new Manifest(this, opts.name, { + provider: opts.provider, + manifest: { + apiVersion: "traefik.io/v1alpha1", + kind: "Middleware", + metadata: { + name: opts.name, + namespace: opts.namespace, + }, + spec: { + ipAllowList: { + sourceRange: opts.sourceRanges, + }, + }, + }, + }); + } +} + +export class IpAllowListMiddlewareTCP extends Construct { + constructor( + scope: Construct, + id: string, + opts: IpAllowListMiddlewareOptions, + ) { + super(scope, id); + + new Manifest(this, opts.name, { + provider: opts.provider, + manifest: { + apiVersion: "traefik.io/v1alpha1", + kind: "MiddlewareTCP", + metadata: { + name: opts.name, + namespace: opts.namespace, + }, + spec: { + ipAllowList: { + sourceRange: opts.sourceRanges, + }, + }, + }, + }); + } +} diff --git a/network-security/traefik/rateLimit.ts b/network-security/traefik/rateLimit.ts new file mode 100644 index 0000000..a8032d1 --- /dev/null +++ b/network-security/traefik/rateLimit.ts @@ -0,0 +1,51 @@ +import { Construct } from "constructs"; +import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest"; +import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; + +type RateLimitMiddlewareOptions = { + provider: KubernetesProvider; + namespace: string; + name: string; + + average?: number; // default 10 + burst?: number; // default 50 + period?: string; // default "1s" +}; + +export class RateLimitMiddleware extends Construct { + public readonly ref: string; + + constructor(scope: Construct, id: string, opts: RateLimitMiddlewareOptions) { + super(scope, id); + + const average = opts.average ?? 10; + const burst = opts.burst ?? 50; + const period = opts.period ?? "1s"; + + this.ref = `${opts.namespace}/${opts.name}`; + + new Manifest(this, opts.name, { + provider: opts.provider, + manifest: { + apiVersion: "traefik.io/v1alpha1", + kind: "Middleware", + metadata: { + name: opts.name, + namespace: opts.namespace, + }, + spec: { + rateLimit: { + average, + burst, + period, + redis: { + endpoints: [`valkey.${opts.namespace}.svc.cluster.local:6379`], + secret: "valkey", + db: 5, + }, + }, + }, + }, + }); + } +} diff --git a/network-security/valkey/index.ts b/network-security/valkey/index.ts new file mode 100644 index 0000000..97df849 --- /dev/null +++ b/network-security/valkey/index.ts @@ -0,0 +1,124 @@ +import { DeploymentV1 } from "@cdktf/provider-kubernetes/lib/deployment-v1"; +import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; +import { ServiceV1 } from "@cdktf/provider-kubernetes/lib/service-v1"; +import { Construct } from "constructs"; +import { OnePasswordSecret } from "../../utils"; + +type ValkeyClusterOptions = { + provider: KubernetesProvider; + name: string; + namespace: string; +}; + +export class ValkeyCluster extends Construct { + constructor(scope: Construct, id: string, options: ValkeyClusterOptions) { + super(scope, id); + + // Labels used by both Deployment and Service + const labels = { app: "valkey" }; + const { provider, name, namespace } = options; + + new OnePasswordSecret(this, "secret", { + provider, + name: "valkey", + namespace, + itemPath: "vaults/Lab/items/valkey", + }); + + new DeploymentV1(this, "deployment", { + provider, + metadata: { + name, + namespace, + labels, + }, + spec: { + replicas: "1", + strategy: { + type: "RollingUpdate", + rollingUpdate: { + maxSurge: "1", + maxUnavailable: "0", + }, + }, + selector: { matchLabels: labels }, + template: { + metadata: { labels }, + spec: { + container: [ + { + name: "valkey", + image: "docker.io/valkey/valkey:8.1.3", + port: [{ name: "client", containerPort: 6379 }], + env: [ + { + name: "PASSWORD", + valueFrom: { + secretKeyRef: { + name: "valkey", + key: "password", + }, + }, + }, + ], + command: ["/bin/sh", "-c"], + args: ['exec valkey-server --requirepass "$PASSWORD"'], + readinessProbe: { + tcpSocket: [ + { + port: "6379", + }, + ], + initialDelaySeconds: 5, + periodSeconds: 5, + timeoutSeconds: 3, + failureThreshold: 5, + }, + livenessProbe: { + tcpSocket: [ + { + port: "6379", + }, + ], + initialDelaySeconds: 20, + periodSeconds: 10, + timeoutSeconds: 5, + failureThreshold: 5, + }, + resources: { + requests: { + cpu: "100m", + memory: "128Mi", + }, + limits: { + memory: "512Mi", + }, + }, + }, + ], + }, + }, + }, + }); + + new ServiceV1(this, "valkey-service", { + provider, + metadata: { + name, + namespace, + labels, + }, + spec: { + type: "ClusterIP", + selector: labels, + port: [ + { + name: "client", + port: 6379, + targetPort: "client", + }, + ], + }, + }); + } +}