init: 폴더구조 설계 및 인프라 설계
This commit is contained in:
@@ -0,0 +1,528 @@
|
||||
# security hardening 예시
|
||||
|
||||
이 문서의 모든 YAML은 `kubectl apply --dry-run=server -f -` 기준 clean을 목표로 한다. 1000+ 서비스 운영 클러스터의 auth-server namespace를 기준 예시로 사용한다.
|
||||
|
||||
---
|
||||
|
||||
## 좋은 예시 1: Namespace에 Pod Security Admission 라벨 enforce
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: auth-prod
|
||||
labels:
|
||||
app.kubernetes.io/part-of: identity-platform
|
||||
pod-security.kubernetes.io/enforce: restricted
|
||||
pod-security.kubernetes.io/enforce-version: v1.29
|
||||
pod-security.kubernetes.io/audit: restricted
|
||||
pod-security.kubernetes.io/audit-version: v1.29
|
||||
pod-security.kubernetes.io/warn: restricted
|
||||
pod-security.kubernetes.io/warn-version: v1.29
|
||||
annotations:
|
||||
platform.example.com/owner: identity-team
|
||||
platform.example.com/adr: ADR-0017-psa-restricted-baseline
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- 운영 namespace의 PSA 기본값을 `restricted`로 enforce. violation Pod는 API server 단에서 reject된다.
|
||||
- version을 pin해 Kubernetes 업그레이드 시 silent behavior drift를 방지한다.
|
||||
- audit/warn을 함께 붙여 위반을 audit log와 kubectl warning으로 수집한다.
|
||||
|
||||
---
|
||||
|
||||
## 좋은 예시 2: Restricted 프로파일을 완전히 만족하는 Deployment
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: auth-server
|
||||
namespace: auth-prod
|
||||
labels:
|
||||
app.kubernetes.io/name: auth-server
|
||||
app.kubernetes.io/instance: auth-server
|
||||
app.kubernetes.io/component: api
|
||||
app.kubernetes.io/part-of: identity-platform
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
automountServiceAccountToken: false
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: auth-server
|
||||
namespace: auth-prod
|
||||
labels:
|
||||
app.kubernetes.io/name: auth-server
|
||||
app.kubernetes.io/instance: auth-server
|
||||
app.kubernetes.io/component: api
|
||||
app.kubernetes.io/part-of: identity-platform
|
||||
app.kubernetes.io/version: 1.42.0
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
spec:
|
||||
replicas: 6
|
||||
revisionHistoryLimit: 5
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 25%
|
||||
maxUnavailable: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: auth-server
|
||||
app.kubernetes.io/instance: auth-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: auth-server
|
||||
app.kubernetes.io/instance: auth-server
|
||||
app.kubernetes.io/component: api
|
||||
app.kubernetes.io/part-of: identity-platform
|
||||
app.kubernetes.io/version: 1.42.0
|
||||
app.kubernetes.io/managed-by: argocd
|
||||
spec:
|
||||
serviceAccountName: auth-server
|
||||
automountServiceAccountToken: false
|
||||
terminationGracePeriodSeconds: 30
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 10001
|
||||
runAsGroup: 10001
|
||||
fsGroup: 10001
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
topologySpreadConstraints:
|
||||
- maxSkew: 1
|
||||
topologyKey: topology.kubernetes.io/zone
|
||||
whenUnsatisfiable: ScheduleAnyway
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: auth-server
|
||||
containers:
|
||||
- name: auth-server
|
||||
image: registry.example.com/identity/auth-server@sha256:8f3c0a8c6b3a2a7a0f1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
- name: metrics
|
||||
containerPort: 9090
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: JAVA_TOOL_OPTIONS
|
||||
value: "-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError"
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: auth-server-db
|
||||
resources:
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: 1Gi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /actuator/health/readiness
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
failureThreshold: 3
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /actuator/health/liveness
|
||||
port: http
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
failureThreshold: 5
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /actuator/health/liveness
|
||||
port: http
|
||||
failureThreshold: 30
|
||||
periodSeconds: 5
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 10001
|
||||
runAsGroup: 10001
|
||||
allowPrivilegeEscalation: false
|
||||
privileged: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
volumeMounts:
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: workdir
|
||||
mountPath: /workspace
|
||||
volumes:
|
||||
- name: tmp
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 64Mi
|
||||
- name: workdir
|
||||
emptyDir:
|
||||
sizeLimit: 256Mi
|
||||
imagePullSecrets:
|
||||
- name: registry-example-com
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- `restricted` 프로파일의 전 필드(runAsNonRoot, numeric UID/GID, fsGroup, seccompProfile, allowPrivilegeEscalation, readOnlyRootFilesystem, drop ALL capabilities)를 Pod+컨테이너 양쪽에 일관 명시한다.
|
||||
- image는 digest pin. mutable tag에 의존하지 않는다.
|
||||
- ServiceAccount는 전용 SA + `automountServiceAccountToken: false`.
|
||||
- writable 경로는 `emptyDir`로 분리해 root FS는 read-only 유지.
|
||||
|
||||
---
|
||||
|
||||
## 나쁜 예시 1: Restricted 프로파일 위반 Pod
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: auth-server
|
||||
namespace: auth-prod
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: auth-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: auth-server
|
||||
spec:
|
||||
containers:
|
||||
- name: auth-server
|
||||
image: auth-server:latest
|
||||
securityContext:
|
||||
privileged: true
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- `privileged: true`는 baseline조차 위반. PSA enforce=restricted namespace에서는 API server가 reject한다.
|
||||
- `runAsNonRoot`, `allowPrivilegeEscalation`, `capabilities.drop`, `seccompProfile`, `readOnlyRootFilesystem` 전부 누락.
|
||||
- image tag `latest`는 digest 고정 없이 rolling silently breaks.
|
||||
- SA 미지정 → `default` SA가 토큰 자동 마운트.
|
||||
|
||||
---
|
||||
|
||||
## 좋은 예시 3: Default-deny + DNS + Ingress + DB + Prometheus allow NetworkPolicy 세트
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: default-deny-all
|
||||
namespace: auth-prod
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-dns-egress
|
||||
namespace: auth-prod
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes:
|
||||
- Egress
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
podSelector:
|
||||
matchLabels:
|
||||
k8s-app: kube-dns
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
- protocol: TCP
|
||||
port: 53
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-from-ingress-traefik
|
||||
namespace: auth-prod
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: auth-server
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: ingress-traefik
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: traefik
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-to-postgres
|
||||
namespace: auth-prod
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: auth-server
|
||||
policyTypes:
|
||||
- Egress
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: data-prod
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: postgres
|
||||
app.kubernetes.io/instance: identity-postgres
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5432
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-metrics-scrape-from-prometheus
|
||||
namespace: auth-prod
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: auth-server
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: monitoring
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: prometheus
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 9090
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- `namespaceSelector`와 `podSelector`가 **동일 `from` 엔트리** 안에 있으므로 AND(교집합): monitoring namespace 안의 Prometheus Pod만 9090 scrape 허용된다.
|
||||
- default-deny + minimum allow 세트로 ingress/egress 모두 통제.
|
||||
- DNS는 `kube-system`의 `k8s-app=kube-dns` Pod로 한정, egress 전체를 열지 않음.
|
||||
|
||||
---
|
||||
|
||||
## 나쁜 예시 2: NetworkPolicy AND/OR 혼동
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: broken-scrape
|
||||
namespace: auth-prod
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: auth-server
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: monitoring
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: prometheus
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 9090
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- `namespaceSelector`와 `podSelector`가 **별도 엔트리**(두 개의 `-`) → OR로 해석된다.
|
||||
- 결과: ① monitoring namespace의 **모든 Pod**가 허용되고, ② `auth-prod` namespace의 label `app.kubernetes.io/name=prometheus`를 가진 **아무 Pod**도 허용된다.
|
||||
- 의도했던 "monitoring의 Prometheus만 허용"이 아니라 훨씬 넓은 경로가 열린다. 실제 클러스터에서 NetworkPolicy 버그의 1순위.
|
||||
|
||||
---
|
||||
|
||||
## 좋은 예시 4: Namespace-scoped RBAC (Role + RoleBinding)
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: auth-server-secret-rotator
|
||||
namespace: auth-prod
|
||||
automountServiceAccountToken: true
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: auth-server-secret-reader
|
||||
namespace: auth-prod
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
resourceNames:
|
||||
- auth-server-db
|
||||
- auth-server-oidc-client
|
||||
verbs: ["get"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: auth-server-secret-reader
|
||||
namespace: auth-prod
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: auth-server-secret-rotator
|
||||
namespace: auth-prod
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: auth-server-secret-reader
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- namespace 경계 안에서 특정 Secret 이름 2개만 `get`. `list`/`watch` 미부여.
|
||||
- SA/Role/RoleBinding 모두 같은 namespace에 명시. `---`로 분리된 다중 리소스 문서.
|
||||
- `system:masters`나 `cluster-admin` 같은 전능 role과 무관.
|
||||
|
||||
---
|
||||
|
||||
## 나쁜 예시 3: cluster-admin ClusterRoleBinding 남용
|
||||
|
||||
```yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: auth-server-admin
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: auth-server
|
||||
namespace: auth-prod
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- 단일 SA가 모든 namespace의 모든 리소스(Secret, Node, CRD)를 수정할 수 있다.
|
||||
- 앱 노드 1개가 compromise되면 전체 클러스터가 compromise된다.
|
||||
- least privilege 원칙의 정반대.
|
||||
|
||||
---
|
||||
|
||||
## 좋은 예시 5: Private registry ImagePullSecret
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: registry-example-com
|
||||
namespace: auth-prod
|
||||
type: kubernetes.io/dockerconfigjson
|
||||
data:
|
||||
.dockerconfigjson: eyJhdXRocyI6eyJyZWdpc3RyeS5leGFtcGxlLmNvbSI6eyJ1c2VybmFtZSI6ImNpLWJvdCIsInBhc3N3b3JkIjoiPFJFREFDVEVEPiIsImF1dGgiOiI8UkVEQUNURUQ+In19fQ==
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: auth-server
|
||||
namespace: auth-prod
|
||||
automountServiceAccountToken: false
|
||||
imagePullSecrets:
|
||||
- name: registry-example-com
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- type이 `kubernetes.io/dockerconfigjson`으로 정확. kubelet이 이 포맷만 pull credential로 인식한다.
|
||||
- SA에 `imagePullSecrets`를 묶어 Deployment 마다 반복 선언 불필요.
|
||||
- 실제 운영에서는 이 Secret 자체도 VSO로 Vault → K8s로 sync(config-and-secrets 문서 참고).
|
||||
|
||||
---
|
||||
|
||||
## 나쁜 예시 4: 정책 없는 운영 namespace
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: auth-prod
|
||||
```
|
||||
|
||||
**문제:**
|
||||
|
||||
- PSA 라벨 없음 → `privileged` Pod도 통과.
|
||||
- NetworkPolicy 없음 → ingress/egress 모두 allow-all. 침해 시 lateral movement 자유.
|
||||
- ResourceQuota/LimitRange 없음 → 한 Deployment가 namespace CPU/memory 전부 점유 가능.
|
||||
- 1000-서비스 운영에서 이런 namespace는 허용되지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 좋은 예시 6: ResourceQuota + LimitRange 묶음
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ResourceQuota
|
||||
metadata:
|
||||
name: auth-prod-quota
|
||||
namespace: auth-prod
|
||||
spec:
|
||||
hard:
|
||||
requests.cpu: "50"
|
||||
requests.memory: 100Gi
|
||||
limits.cpu: "100"
|
||||
limits.memory: 200Gi
|
||||
pods: "200"
|
||||
services.loadbalancers: "0"
|
||||
services.nodeports: "0"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: LimitRange
|
||||
metadata:
|
||||
name: auth-prod-defaults
|
||||
namespace: auth-prod
|
||||
spec:
|
||||
limits:
|
||||
- type: Container
|
||||
default:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
defaultRequest:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
max:
|
||||
cpu: "4"
|
||||
memory: 4Gi
|
||||
```
|
||||
|
||||
**왜 좋은가:**
|
||||
|
||||
- `services.loadbalancers=0`, `services.nodeports=0`으로 namespace 내 외부 노출 Service 생성을 금지(ingress 경유 강제).
|
||||
- LimitRange로 컨테이너별 default request/limit을 보장해 limit 누락 Pod를 예방.
|
||||
Reference in New Issue
Block a user