Runbook: Create Project Namespace and Baseline Resources
Metadata
| Field | Value |
|---|---|
| Runbook ID | RB-K8S-CREATE-PROJECT-NS-001 |
| Operation | create |
| Resource type | Kubernetes namespace and baseline policy resources |
| Owner | Platform Engineering |
| On-call | #platform-oncall |
| Risk | Medium |
| Environments | dev, staging, prod |
| Required approval | Platform Engineering lead for prod; team lead for dev/staging |
| Source of truth | platform-infra/k8s/project-namespace |
| Review cadence | Quarterly |
| Last reviewed | 2026-10-22 |
Purpose
Create an isolated project namespace with baseline resource governance, access control, workload security policy, and network isolation.
This runbook creates:
NamespaceResourceQuotaLimitRange- Namespace-scoped
RoleandRoleBinding - Pod Security Admission labels
- Default-deny
NetworkPolicy - Standard ownership and cost-allocation labels
A namespace is not a complete security boundary by itself; isolation requires namespace-scoped RBAC, network policy, admission controls, and resource governance. kubernetes
Preconditions
Before starting, confirm all items:
- Approved request exists in Jira/ServiceNow.
- Request includes project ID, owning team, environment, cost center, data classification, and resource requirements.
- Operator is authenticated to the correct cluster.
- Operator has
platform-provisionerpermissions in the target environment. - Namespace name is available and follows naming convention.
- Resource quota values are approved by the requesting team and Platform Engineering.
- For
prod, change approval is recorded. - No existing namespace uses the requested name.
Inputs
| Variable | Required | Example | Validation |
|---|---|---|---|
PROJECT_ID |
Yes | payments |
Lowercase DNS label: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$, max 63 characters |
ENVIRONMENT |
Yes | prod |
dev, staging, or prod |
TEAM |
Yes | payments-team |
Existing team/group name |
COST_CENTER |
Yes | cc-4821 |
Valid finance cost center |
DATA_CLASSIFICATION |
Yes | confidential |
internal, confidential, or restricted |
CPU_REQUEST |
Yes | 4 |
Positive Kubernetes CPU quantity |
CPU_LIMIT |
Yes | 8 |
Greater than or equal to CPU_REQUEST |
MEMORY_REQUEST |
Yes | 8Gi |
Positive Kubernetes memory quantity |
MEMORY_LIMIT |
Yes | 16Gi |
Greater than or equal to MEMORY_REQUEST |
MAX_PODS |
Yes | 40 |
Positive integer |
NAMESPACE |
Generated | payments-prod |
$PROJECT_ID-$ENVIRONMENT |
Naming and labeling standards
Namespace name:
<project-id>-<environment>
Required namespace labels:
app.kubernetes.io/part-of: <PROJECT_ID>
environment: <ENVIRONMENT>
owner: <TEAM>
cost-center: <COST_CENTER>
data-classification: <DATA_CLASSIFICATION>
managed-by: platform-engineering
network-policy: default-deny
Use consistent labels for ownership, environment, cost allocation, and policy selection. github
Procedure
1. Verify target cluster
kubectl config current-context
kubectl auth can-i create namespaces
kubectl auth can-i create resourcequotas
kubectl auth can-i create limitranges
kubectl auth can-i create roles
kubectl auth can-i create rolebindings
kubectl auth can-i create networkpolicies
Verify: The current context is the approved cluster, and every permission check returns yes.
If failed: Stop. Request access through Platform Engineering. Do not use a broader identity or cluster-admin.
2. Confirm namespace does not exist
NAMESPACE="<PROJECT_ID>-<ENVIRONMENT>"
kubectl get namespace "${NAMESPACE}" \
-o name 2>/dev/null || echo "NAMESPACE_AVAILABLE"
Verify: Output is NAMESPACE_AVAILABLE.
If failed: Stop. Investigate ownership, labels, and workloads before proceeding. Do not reuse or modify an existing namespace without a separate modification runbook.
3. Create the namespace
Save as namespace.yaml:
apiVersion: v1
kind: Namespace
metadata:
name: <PROJECT_ID>-<ENVIRONMENT>
labels:
app.kubernetes.io/part-of: <PROJECT_ID>
environment: <ENVIRONMENT>
owner: <TEAM>
cost-center: <COST_CENTER>
data-classification: <DATA_CLASSIFICATION>
managed-by: platform-engineering
network-policy: default-deny
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
Apply it:
kubectl apply -f namespace.yaml
Verify:
kubectl get namespace "${NAMESPACE}" \
--show-labels
Verify:
- Namespace exists.
- All required labels are present.
- Pod Security Admission labels are
restrictedforprod;baselinemay be used in non-production only with a documented exception.
Pod Security Admission enforces the restricted standard through the pod-security.kubernetes.io/* namespace labels. kubernetes
4. Create resource quota
Save as resource-quota.yaml:
apiVersion: v1
kind: ResourceQuota
metadata:
name: project-baseline-quota
namespace: <PROJECT_ID>-<ENVIRONMENT>
spec:
hard:
requests.cpu: "<CPU_REQUEST>"
requests.memory: "<MEMORY_REQUEST>"
limits.cpu: "<CPU_LIMIT>"
limits.memory: "<MEMORY_LIMIT>"
pods: "<MAX_PODS>"
persistentvolumeclaims: "10"
services.loadbalancers: "1"
Apply it:
kubectl apply -f resource-quota.yaml
Verify:
kubectl describe resourcequota project-baseline-quota \
--namespace "${NAMESPACE}"
Verify:
- CPU, memory, pod, PVC, and LoadBalancer limits match the approved request.
Usedvalues are zero or expected.- No warning indicates the quota was rejected.
ResourceQuota constrains aggregate namespace consumption and can also cap object counts such as pods and services. kubernetes
5. Create limit range
Save as limit-range.yaml:
apiVersion: v1
kind: LimitRange
metadata:
name: project-baseline-limits
namespace: <PROJECT_ID>-<ENVIRONMENT>
spec:
limits:
- type: Container
defaultRequest:
cpu: 100m
memory: 128Mi
default:
cpu: 500m
memory: 512Mi
min:
cpu: 50m
memory: 64Mi
max:
cpu: "2"
memory: 2Gi
- type: PersistentVolumeClaim
min:
storage: 1Gi
max:
storage: 50Gi
Apply it:
kubectl apply -f limit-range.yaml
Verify:
kubectl describe limitrange project-baseline-limits \
--namespace "${NAMESPACE}"
Verify:
- Default requests and limits are present.
- Maximum per-container values are within the approved project quota.
- PVC bounds match storage policy.
LimitRange supplies defaults for containers that omit requests or limits and enforces minimum and maximum resource values. deepwiki
6. Create namespace-scoped RBAC
Save as rbac.yaml:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: project-developer
namespace: <PROJECT_ID>-<ENVIRONMENT>
rules:
- apiGroups: ["", "apps", "batch"]
resources:
- pods
- pods/log
- services
- configmaps
- deployments
- replicasets
- statefulsets
- jobs
- cronjobs
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups: [""]
resources:
- secrets
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: project-developer-binding
namespace: <PROJECT_ID>-<ENVIRONMENT>
subjects:
- kind: Group
name: <TEAM>
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: project-developer
apiGroup: rbac.authorization.k8s.io
Apply it:
kubectl apply -f rbac.yaml
Verify:
kubectl auth can-i list pods \
--as=system:group:<TEAM> \
--namespace "${NAMESPACE}"
kubectl auth can-i delete namespaces \
--as=system:group:<TEAM> \
--namespace "${NAMESPACE}"
Verify:
- First command returns
yes. - Second command returns
no. - No
ClusterRoleBindingis created for the project team.
Use namespace-scoped Role and RoleBinding, and avoid broad or cluster-admin access for project teams. nirgeier.github
7. Create default-deny network policy
Save as network-policy.yaml:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: <PROJECT_ID>-<ENVIRONMENT>
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
Apply it:
kubectl apply -f network-policy.yaml
Verify:
kubectl get networkpolicy default-deny-all \
--namespace "${NAMESPACE}" \
-o yaml
Verify:
policyTypescontains bothIngressandEgress.- No ingress or egress rules are present in the default-deny policy.
- Any required exceptions are added through separate, reviewed network policies.
A default-deny policy blocks ingress and egress unless explicitly allowed by another policy. atmosly
8. Validate baseline resources
kubectl get namespace,resourcequota,limitrange,role,rolebinding,networkpolicy \
--namespace "${NAMESPACE}"
Expected result:
NAME STATUS AGE
namespace/<PROJECT_ID>-<ENVIRONMENT> Active ...
NAME AGE
resourcequota/project-baseline-quota ...
NAME CREATED AT
limitrange/project-baseline-limits ...
NAME CREATED AT
role/project-developer ...
NAME ROLE AGE
rolebinding/project-developer-binding Role/project-developer ...
NAME POD-SELECTOR AGE
networkpolicy/default-deny-all <none> ...
Run an end-to-end smoke test:
kubectl run baseline-smoke-test \
--namespace "${NAMESPACE}" \
--image=registry.k8s.io/pause:3.10 \
--restart=Never
kubectl get pod baseline-smoke-test \
--namespace "${NAMESPACE}" \
-o jsonpath='{.spec.containers.resources}'
kubectl delete pod baseline-smoke-test \
--namespace "${NAMESPACE}"
Verify:
- Test pod is created.
- It receives default CPU and memory requests/limits from the
LimitRange. - It is deleted after validation.
9. Record completion
Update the original request with:
- Namespace name.
- Cluster and environment.
- Approved quota values.
- Owner group.
- Cost center.
- Data classification.
- Links to applied manifests or Git commit.
- Operator name and completion timestamp.
Notify the requesting team:
Project namespace <NAMESPACE> has been created in <CLUSTER>.
Owner: <TEAM>
Resource quota: CPU request <CPU_REQUEST>, CPU limit <CPU_LIMIT>,
memory request <MEMORY_REQUEST>, memory limit <MEMORY_LIMIT>, max pods <MAX_PODS>.
Network access is default-deny; submit a network-policy change request for required exceptions.
Rollback
Use this only if creation failed, was approved in error, or the namespace contains no workloads or persistent data.
NAMESPACE="<PROJECT_ID>-<ENVIRONMENT>"
kubectl get all,pvc,secret,configmap \
--namespace "${NAMESPACE}"
kubectl delete namespace "${NAMESPACE}"
Verify:
kubectl get namespace "${NAMESPACE}" 2>/dev/null \
|| echo "NAMESPACE_REMOVED"
Warning: Deleting the namespace deletes all resources inside it, including workloads, configuration, secrets, PVCs, and namespace-scoped RBAC. Do not run rollback if the namespace contains production data or active workloads.
If resources must be retained, remove only the newly created baseline objects instead:
kubectl delete resourcequota project-baseline-quota --namespace "${NAMESPACE}"
kubectl delete limitrange project-baseline-limits --namespace "${NAMESPACE}"
kubectl delete role project-developer --namespace "${NAMESPACE}"
kubectl delete rolebinding project-developer-binding --namespace "${NAMESPACE}"
kubectl delete networkpolicy default-deny-all --namespace "${NAMESPACE}"
Failure handling
| Symptom | Likely cause | Action | Escalation |
|---|---|---|---|
kubectl auth can-i returns no |
Missing RBAC permissions | Stop; request platform-provisioner access |
Platform on-call |
| Namespace already exists | Name collision or duplicate request | Identify owner and existing workload | Platform Engineering lead |
ResourceQuota rejected |
Invalid quantity or exceeds cluster policy | Correct approved values; reapply | Capacity owner |
| Pod cannot schedule | Quota, node capacity, taints, or admission policy | Inspect events and quota usage | Platform on-call |
| Pod fails admission | Pod violates restricted Pod Security Standard | Remediate workload manifest or document exception | Security Engineering |
| Required external access fails | Default-deny network policy | Add narrowly scoped ingress/egress policy through review | Network/Security team |
| Team cannot access namespace | Incorrect group name or role binding | Verify identity provider group and RoleBinding |
Identity/Platform team |
Change control
- Any change to quota values requires a new approved request.
- Any relaxation of Pod Security, RBAC, or network policy requires security review.
- Do not grant namespace teams permission to modify or delete
ResourceQuota,LimitRange, or baseline network policies. - Protect baseline objects through admission policy or GitOps-controlled manifests; Kubernetes recommends restricting update/delete access to quotas to preserve enforcement. kubernetes