diff --git a/apps/portfolio-namespace.yaml b/apps/portfolio-namespace.yaml new file mode 100644 index 0000000..f621f3a --- /dev/null +++ b/apps/portfolio-namespace.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: portfolio + annotations: + argocd.argoproj.io/sync-wave: "-2" + labels: + app.kubernetes.io/name: portfolio + app.kubernetes.io/part-of: portfolio-platform + pod-security.kubernetes.io/enforce: restricted + pod-security.kubernetes.io/enforce-version: v1.36 + pod-security.kubernetes.io/audit: restricted + pod-security.kubernetes.io/audit-version: v1.36 + pod-security.kubernetes.io/warn: restricted + pod-security.kubernetes.io/warn-version: v1.36 diff --git a/apps/portfolio-project.yaml b/apps/portfolio-project.yaml new file mode 100644 index 0000000..651c559 --- /dev/null +++ b/apps/portfolio-project.yaml @@ -0,0 +1,39 @@ +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: portfolio + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "-1" +spec: + description: Restricted GitOps project for the public portfolio workload + + sourceRepos: + - http://git.home.lan/jay/k8s-gitops.git + + destinations: + - namespace: portfolio + server: https://kubernetes.default.svc + + namespaceResourceWhitelist: + - group: "" + kind: ConfigMap + - group: "" + kind: LimitRange + - group: "" + kind: ResourceQuota + - group: "" + kind: Service + - group: "" + kind: ServiceAccount + - group: apps + kind: Deployment + - group: networking.k8s.io + kind: Ingress + - group: networking.k8s.io + kind: NetworkPolicy + - group: policy + kind: PodDisruptionBudget + + orphanedResources: + warn: true diff --git a/apps/portfolio.yaml b/apps/portfolio.yaml new file mode 100644 index 0000000..700d8f3 --- /dev/null +++ b/apps/portfolio.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: portfolio + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "0" + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + project: portfolio + + source: + repoURL: http://git.home.lan/jay/k8s-gitops.git + targetRevision: main + path: apps/portfolio + + destination: + server: https://kubernetes.default.svc + namespace: portfolio + + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - ApplyOutOfSyncOnly=true + - PruneLast=true + retry: + limit: 5 + backoff: + duration: 5s + factor: 2 + maxDuration: 1m diff --git a/apps/portfolio/deployment.yaml b/apps/portfolio/deployment.yaml new file mode 100644 index 0000000..e6879f1 --- /dev/null +++ b/apps/portfolio/deployment.yaml @@ -0,0 +1,123 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: portfolio + namespace: portfolio + labels: + app.kubernetes.io/name: portfolio + app.kubernetes.io/instance: portfolio + app.kubernetes.io/component: web + app.kubernetes.io/part-of: portfolio-platform +spec: + replicas: 2 + revisionHistoryLimit: 3 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + selector: + matchLabels: + app.kubernetes.io/name: portfolio + app.kubernetes.io/instance: portfolio + template: + metadata: + labels: + app.kubernetes.io/name: portfolio + app.kubernetes.io/instance: portfolio + app.kubernetes.io/component: web + app.kubernetes.io/part-of: portfolio-platform + spec: + serviceAccountName: portfolio + automountServiceAccountToken: false + enableServiceLinks: false + terminationGracePeriodSeconds: 20 + securityContext: + runAsNonRoot: true + runAsUser: 101 + runAsGroup: 101 + fsGroup: 101 + fsGroupChangePolicy: OnRootMismatch + seccompProfile: + type: RuntimeDefault + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app.kubernetes.io/name: portfolio + app.kubernetes.io/instance: portfolio + containers: + - name: portfolio + image: nginxinc/nginx-unprivileged:1.29.4-alpine + imagePullPolicy: IfNotPresent + command: + - nginx + args: + - -g + - daemon off; + ports: + - name: http + containerPort: 8080 + protocol: TCP + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + resources: + requests: + cpu: 25m + memory: 32Mi + ephemeral-storage: 16Mi + limits: + cpu: 250m + memory: 128Mi + ephemeral-storage: 128Mi + startupProbe: + httpGet: + path: /healthz + port: http + failureThreshold: 30 + periodSeconds: 2 + timeoutSeconds: 2 + readinessProbe: + httpGet: + path: /healthz + port: http + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 15 + periodSeconds: 30 + timeoutSeconds: 2 + failureThreshold: 3 + volumeMounts: + - name: site + mountPath: /usr/share/nginx/html + readOnly: true + - name: nginx-config + mountPath: /etc/nginx/conf.d/default.conf + subPath: default.conf + readOnly: true + - name: tmp + mountPath: /tmp + volumes: + - name: site + configMap: + name: portfolio-site + defaultMode: 0444 + - name: nginx-config + configMap: + name: portfolio-nginx + defaultMode: 0444 + - name: tmp + emptyDir: + medium: Memory + sizeLimit: 32Mi diff --git a/apps/portfolio/ingress.yaml b/apps/portfolio/ingress.yaml new file mode 100644 index 0000000..86a928e --- /dev/null +++ b/apps/portfolio/ingress.yaml @@ -0,0 +1,34 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: portfolio + namespace: portfolio + labels: + app.kubernetes.io/name: portfolio + app.kubernetes.io/instance: portfolio + app.kubernetes.io/component: web + app.kubernetes.io/part-of: portfolio-platform + annotations: + cert-manager.io/cluster-issuer: home-lab-ca + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/force-ssl-redirect: "true" + nginx.ingress.kubernetes.io/proxy-body-size: "1m" + nginx.ingress.kubernetes.io/limit-rps: "10" + nginx.ingress.kubernetes.io/limit-burst-multiplier: "5" +spec: + ingressClassName: nginx + tls: + - hosts: + - portfolio.home.lan + secretName: portfolio-home-lan-tls + rules: + - host: portfolio.home.lan + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: portfolio + port: + name: http diff --git a/apps/portfolio/kustomization.yaml b/apps/portfolio/kustomization.yaml new file mode 100644 index 0000000..6dc8abe --- /dev/null +++ b/apps/portfolio/kustomization.yaml @@ -0,0 +1,27 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: portfolio + +resources: + - serviceaccount.yaml + - resourcequota.yaml + - limitrange.yaml + - deployment.yaml + - service.yaml + - ingress.yaml + - networkpolicy.yaml + - poddisruptionbudget.yaml + +configMapGenerator: + - name: portfolio-site + files: + - index.html=site/index.html + - name: portfolio-nginx + files: + - default.conf=nginx/default.conf + +generatorOptions: + labels: + app.kubernetes.io/name: portfolio + app.kubernetes.io/part-of: portfolio-platform diff --git a/apps/portfolio/limitrange.yaml b/apps/portfolio/limitrange.yaml new file mode 100644 index 0000000..1d9f96e --- /dev/null +++ b/apps/portfolio/limitrange.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: LimitRange +metadata: + name: portfolio-limits + namespace: portfolio + labels: + app.kubernetes.io/name: portfolio + app.kubernetes.io/part-of: portfolio-platform +spec: + limits: + - type: Container + defaultRequest: + cpu: 25m + memory: 32Mi + ephemeral-storage: 16Mi + default: + cpu: 250m + memory: 128Mi + ephemeral-storage: 128Mi + min: + cpu: 5m + memory: 16Mi + max: + cpu: "1" + memory: 512Mi + ephemeral-storage: 512Mi diff --git a/apps/portfolio/networkpolicy.yaml b/apps/portfolio/networkpolicy.yaml new file mode 100644 index 0000000..37cdb29 --- /dev/null +++ b/apps/portfolio/networkpolicy.yaml @@ -0,0 +1,41 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny-all + namespace: portfolio + labels: + app.kubernetes.io/name: portfolio + app.kubernetes.io/part-of: portfolio-platform +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-ingress-nginx + namespace: portfolio + labels: + app.kubernetes.io/name: portfolio + app.kubernetes.io/part-of: portfolio-platform +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: portfolio + app.kubernetes.io/instance: portfolio + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: ingress-nginx + podSelector: + matchLabels: + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/component: controller + ports: + - protocol: TCP + port: 8080 diff --git a/apps/portfolio/nginx/default.conf b/apps/portfolio/nginx/default.conf new file mode 100644 index 0000000..227ef5f --- /dev/null +++ b/apps/portfolio/nginx/default.conf @@ -0,0 +1,28 @@ +server { + listen 8080 default_server; + listen [::]:8080 default_server; + server_name _; + + root /usr/share/nginx/html; + index index.html; + server_tokens off; + + access_log /dev/stdout; + error_log /dev/stderr warn; + + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "DENY" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; + add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always; + + location = /healthz { + access_log off; + default_type text/plain; + return 200 "ok\n"; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/apps/portfolio/poddisruptionbudget.yaml b/apps/portfolio/poddisruptionbudget.yaml new file mode 100644 index 0000000..01ef8eb --- /dev/null +++ b/apps/portfolio/poddisruptionbudget.yaml @@ -0,0 +1,15 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: portfolio + namespace: portfolio + labels: + app.kubernetes.io/name: portfolio + app.kubernetes.io/instance: portfolio + app.kubernetes.io/part-of: portfolio-platform +spec: + minAvailable: 1 + selector: + matchLabels: + app.kubernetes.io/name: portfolio + app.kubernetes.io/instance: portfolio diff --git a/apps/portfolio/resourcequota.yaml b/apps/portfolio/resourcequota.yaml new file mode 100644 index 0000000..d1c8a80 --- /dev/null +++ b/apps/portfolio/resourcequota.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: ResourceQuota +metadata: + name: portfolio-quota + namespace: portfolio + labels: + app.kubernetes.io/name: portfolio + app.kubernetes.io/part-of: portfolio-platform +spec: + hard: + requests.cpu: "500m" + requests.memory: 512Mi + limits.cpu: "2" + limits.memory: 1Gi + requests.ephemeral-storage: 256Mi + limits.ephemeral-storage: 1Gi + pods: "6" + services: "4" + configmaps: "10" + count/deployments.apps: "3" + count/ingresses.networking.k8s.io: "2" + count/networkpolicies.networking.k8s.io: "10" diff --git a/apps/portfolio/service.yaml b/apps/portfolio/service.yaml new file mode 100644 index 0000000..d102480 --- /dev/null +++ b/apps/portfolio/service.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + name: portfolio + namespace: portfolio + labels: + app.kubernetes.io/name: portfolio + app.kubernetes.io/instance: portfolio + app.kubernetes.io/component: web + app.kubernetes.io/part-of: portfolio-platform +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: portfolio + app.kubernetes.io/instance: portfolio + ports: + - name: http + port: 80 + targetPort: http + protocol: TCP diff --git a/apps/portfolio/serviceaccount.yaml b/apps/portfolio/serviceaccount.yaml new file mode 100644 index 0000000..a650fc5 --- /dev/null +++ b/apps/portfolio/serviceaccount.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: portfolio + namespace: portfolio + labels: + app.kubernetes.io/name: portfolio + app.kubernetes.io/instance: portfolio + app.kubernetes.io/component: web + app.kubernetes.io/part-of: portfolio-platform +automountServiceAccountToken: false diff --git a/apps/portfolio/site/index.html b/apps/portfolio/site/index.html new file mode 100644 index 0000000..15f1a89 --- /dev/null +++ b/apps/portfolio/site/index.html @@ -0,0 +1,599 @@ + + + + + + + Jay Phillips | Infrastructure Portfolio + + + +
+ +
+ +
+
+
+
Systems Administrator · Infrastructure Engineer
+

Building resilient infrastructure.

+

+ I design, automate, secure, and operate enterprise and home-lab platforms across + Windows Server, VMware, Linux, Kubernetes, GitOps, and Microsoft systems management. +

+ +
+ + +
+ +
+
+

Core capabilities

+

+ Hands-on administration supported by automation, security controls, observability, + documentation, and repeatable deployment practices. +

+
+
+
+
01 / CLOUD NATIVE
+

Kubernetes & GitOps

+

Multi-node clusters, Argo CD, ingress, certificates, persistent storage, RBAC, policies, and application lifecycle management.

+
KubernetesArgo CDKustomizeLonghorn
+
+
+
02 / MICROSOFT
+

Windows Infrastructure

+

Windows Server, Active Directory, Group Policy, Configuration Manager, Operations Manager, SharePoint, and PowerShell.

+
Windows ServerSCCMSCOMSharePoint
+
+
+
03 / AUTOMATION
+

Automation & IaC

+

Repeatable infrastructure operations using PowerShell, Bash, Git, Terraform, Ansible, APIs, and declarative configuration.

+
PowerShellTerraformAnsibleGit
+
+
+
04 / PLATFORMS
+

Virtualization & Linux

+

VMware vSphere administration, Linux server operations, networking, storage, patching, troubleshooting, and capacity planning.

+
VMwareUbuntuRocky LinuxNFS
+
+
+
05 / OBSERVABILITY
+

Monitoring & Reliability

+

Metrics, logging, alerting, dashboards, service health, failure analysis, and operational visibility across infrastructure layers.

+
PrometheusGrafanaLokiAlertmanager
+
+
+
06 / SECURITY
+

Secure Operations

+

Least privilege, service identities, network segmentation, certificate management, secret handling, backups, and recovery planning.

+
RBACTLSNetworkPolicyGitOps
+
+
+
+ +
+
+

Featured work

+

+ The full case studies will document the problem, architecture, implementation, + troubleshooting decisions, security controls, and operational results. +

+
+
+
+
CASE STUDY / 01
+

Production-style Kubernetes home lab

+

A four-node Kubernetes platform with load balancing, persistent storage, certificate automation, ingress, backups, and segmented applications.

+
4 nodesCalicoMetalLBLonghorn
+
+
+
CASE STUDY / 02
+

GitOps application delivery

+

Declarative application deployment and reconciliation through Argo CD, Git-based change control, Kustomize, and automated image workflows.

+
Argo CDGitKustomizeSelf-healing
+
+
+
CASE STUDY / 03
+

Enterprise monitoring stack

+

Centralized metrics, logs, dashboards, uptime monitoring, and email alerting for cluster and application health.

+
PrometheusGrafanaLokiUptime Kuma
+
+
+
+ +
+
+

This site is part of the demonstration.

+

+ The portfolio is deployed as an isolated, policy-controlled workload rather than a + conventional static hosting account. The architecture itself is a portfolio artifact. +

+
+
+
+

Delivery path

+
    +
  1. Git repository stores the desired state
  2. +
  3. Argo CD reconciles a restricted AppProject
  4. +
  5. Kustomize renders application resources
  6. +
  7. ingress-nginx routes HTTPS traffic
  8. +
  9. cert-manager issues the internal TLS certificate
  10. +
+
+
+

Security controls

+
    +
  • Restricted Pod Security
  • +
  • Non-root container
  • +
  • Read-only root filesystem
  • +
  • All capabilities dropped
  • +
  • Default-deny networking
  • +
  • No outbound network access
  • +
  • No API token mounted
  • +
  • Resource quotas and limits
  • +
+
+
+
+ +
+
+
+

Foundation complete. Case studies next.

+

This internal staging release will grow into a public portfolio, short résumé, architecture library, and safe interactive demonstration environment.

+
+ Back to top +
+
+
+ + + +