ЁЯПл The SchoolтА║ЁЯЫбя╕П SecurityтА║ЁЯХ╕я╕П рдзрдбрд╛ 11 тАФ Kubernetes secrets рдЖрдгрд┐ network policies: рдЖрд░рд╢рд╛рддрд▓реЗ рдЙрд▓рдЯреЗ рдЕрдХреНрд╖рд░ рдореНрд╣рдгрдЬреЗ рдХреБрд▓реВрдк рдирд╛рд╣реА
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯХ╕я╕П рдзрдбрд╛ 11 тАФ Kubernetes secrets рдЖрдгрд┐ network policies: рдЖрд░рд╢рд╛рддрд▓реЗ рдЙрд▓рдЯреЗ рдЕрдХреНрд╖рд░ рдореНрд╣рдгрдЬреЗ рдХреБрд▓реВрдк рдирд╛рд╣реА

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 16 рдкреИрдХреА рдзрдбрд╛ 11 ┬╖ рдорд╛рдЧреАрд▓: lesson-10-k8s-rbac ┬╖ рдкреБрдвреАрд▓: lesson-12-admission


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ10, рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ рдирд╡реНрдпрд╛ cluster рдордзреНрдпреЗ рдЕрд╕рдгрд╛рд░реА рджреЛрди рдЖрд╢реНрдЪрд░реНрдпреЗ. рдкрд╣рд┐рд▓реЗ: Kubernetes Secret рдлрдХреНрдд base64 рдЕрд╕рддреЗ тАФ рдПрдХ encoding, encryption рдирд╛рд╣реА тАФ рдЬреЛрдкрд░реНрдпрдВрдд рддреБрдореНрд╣реА encryption at rest рдЪрд╛рд▓реВ рдХрд░рдд рдирд╛рд╣реА рдХрд┐рдВрд╡рд╛ рдЦрд░реЗ рдЧреБрдкрд┐рдд external store рдордзреНрдпреЗ рдареЗрд╡рдд рдирд╛рд╣реА. рджреБрд╕рд░реЗ: рдкреНрд░рддреНрдпреЗрдХ pod рдЗрддрд░ рдкреНрд░рддреНрдпреЗрдХ pod рд╢реА рдмреЛрд▓реВ рд╢рдХрддреЛ, рдЬреЛрдкрд░реНрдпрдВрдд рдПрдЦрд╛рджреА NetworkPolicy рддреНрдпрд╛рд▓рд╛ select рдХрд░рдд рдирд╛рд╣реА тАФ рдЖрдгрд┐ рддреЗрд╣реА рдлрдХреНрдд cluster рдЪрд╛ network plugin (CNI) policies рд▓рд╛рдЧреВ рдХрд░рдд рдЕрд╕реЗрд▓ рддрд░рдЪ. рддреБрдореНрд╣реА рджреЛрдиреНрд╣реА рд╢рд┐рдХрддрд╛, рдЖрдгрд┐ default-deny pattern рд╕реБрджреНрдзрд╛.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдЧреБрдкреНрдд рдкрд╛рдХреАрдЯ. рдРрд╢реНрд╡рд░реНрдпрд╛ database рдЪрд╛ password "SECRET" рдЕрд╕реЗ рд▓рд┐рд╣рд┐рд▓реЗрд▓реНрдпрд╛ рдкрд╛рдХрд┐рдЯрд╛рдд рдареЗрд╡рддреЗ. рдкрдг рдЖрдд password рдХреБрд▓реВрдкрдмрдВрдж рдирд╛рд╣реА тАФ рддреЛ рдлрдХреНрдд рдЖрд░рд╢рд╛рддрд▓реНрдпрд╛ рдЙрд▓рдЯреНрдпрд╛ рдЕрдХреНрд╖рд░рд╛рдВрдд рд▓рд┐рд╣рд┐рд▓реЗрд▓рд╛ рдЖрд╣реЗ. рдЬреНрдпрд╛рдЪреНрдпрд╛рдХрдбреЗ рдЖрд░рд╕рд╛ рдЖрд╣реЗ (рдЖрдгрд┐ рдЖрд░рд╕рд╛ рд╕рдЧрд│реНрдпрд╛рдВрдХрдбреЗрдЪ рдЕрд╕рддреЛ) рддреЛ рд╡рд╛рдЪреВ рд╢рдХрддреЛ. рд╣реЗрдЪ base64. рддреЗ рдЦрд░реЛрдЦрд░ рдЧреБрдкреНрдд рдХрд░рд╛рдпрдЪреЗ рдЕрд╕реЗрд▓, рддрд░ рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рдиреЗ рдЪрд╛рд╡реА-рддрд┐рдЬреЛрд░реАрддрд▓реНрдпрд╛ рдЪрд╛рд╡реАрдиреЗ рдкрд╛рдХрд┐рдЯрд╛рд▓рд╛ рдХреБрд▓реВрдк рд▓рд╛рд╡рд╛рдпрд▓рд╛ рд╣рд╡реЗ (KMS рд╕рд╣ encryption at rest), рдХрд┐рдВрд╡рд╛ password рдЪрд╛рд╡реА-рддрд┐рдЬреЛрд░реАрддрдЪ рдареЗрд╡реВрди рдЧрд░рдЬ рдЕрд╕реЗрд▓ рддреЗрд╡реНрд╣рд╛рдЪ рдЖрдгрд╛рдпрд▓рд╛ рд╣рд╡рд╛ (external secret store).

рд╡рд▒реНрд╣рд╛рдВрдбреЗ. рд╢рд╛рд│реЗрдЪреНрдпрд╛ рдирд╡реНрдпрд╛ рдЗрдорд╛рд░рддреАрдд рдкреНрд░рддреНрдпреЗрдХ рджрд╛рд░ рдЙрдШрдбреЗ рдЕрд╕рддреЗ. рдХреЛрдгрддрд╛рд╣реА рд╡рд┐рджреНрдпрд╛рд░реНрдереА рд╢рд┐рдХреНрд╖рдХ-рдЦреЛрд▓реАрдд, server room рдордзреНрдпреЗ, рд╕реНрд╡рдпрдВрдкрд╛рдХрдШрд░рд╛рдд рдЬрд╛рдК рд╢рдХрддреЛ. рдХрд╛рд░реНрдпрд╛рд▓рдп рдПрдЦрд╛рджреНрдпрд╛ рджрд╛рд░рд╛рд╡рд░ рдпрд╛рджреА рдШреЗрдКрди рдкрд╣рд╛рд░реЗрдХрд░реА рдареЗрд╡реВ рд╢рдХрддреЗ: "рдлрдХреНрдд рдирд┐рдХрд╛рд▓-рдорджрддрдиреАрд╕, рдЖрдгрд┐ рдлрдХреНрдд 5432 рдХреНрд░рдорд╛рдВрдХрд╛рдЪреНрдпрд╛ рдмрд╛рдЬреВрдЪреНрдпрд╛ рджрд╛рд░рд╛рддреВрди."

рдкрд╣рд╛рд░реЗрдХрд▒реНрдпрд╛рдВрдмрджреНрджрд▓ рджреЛрди рдирд┐рдпрдо:

  1. рдкрд╣рд╛рд░реЗрдХрд░реА рдирд╕рд▓реЗрд▓реЗ рджрд╛рд░ рд╕рдЧрд│реНрдпрд╛рдВрд╕рд╛рдареА рдЙрдШрдбреЗрдЪ рд░рд╛рд╣рддреЗ. рдореНрд╣рдгреВрди рдХрд╛рд░реНрдпрд╛рд▓рдп рд╕реБрд░реБрд╡рд╛рддреАрд▓рд╛ рдкреНрд░рддреНрдпреЗрдХ рджрд╛рд░рд╛рд╡рд░ рд░рд┐рдХрд╛рдореА рдпрд╛рджреА тАФ "рдХреЛрдгреАрдЪ рдирд╛рд╣реА" тАФ рдШреЗрдКрди рдкрд╣рд╛рд░реЗрдХрд░реА рдареЗрд╡рддреЗ, рдЖрдгрд┐ рдордЧ рдирд╛рд╡реЗ рдЬреЛрдбрддреЗ. рд╣реЗрдЪ default-deny.
  2. рдкрд╣рд╛рд░реЗрдХрд▒реНрдпрд╛рдЪреА рдпрд╛рджреА рдлрдХреНрдд рдХрд╛рдЧрдж рдЖрд╣реЗ. рдЗрдорд╛рд░рддреАрдд рдкрд╣рд╛рд░реЗрдХрд░реА рд╕реЗрд╡рд╛рдЪ рдирд╕реЗрд▓ (policies рд▓рд╛рдЧреВ рдХрд░рдгрд╛рд░рд╛ CNI), рддрд░ рддреЛ рдХрд╛рдЧрдж рдХрд╛рд╣реАрдЪ рдХрд░рдд рдирд╛рд╣реА, рдЖрдгрд┐ рдХреЛрдгреА рддреБрдореНрд╣рд╛рд▓рд╛ рд╕рд╛рдВрдЧрддрд╣реА рдирд╛рд╣реА.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    r["ЁЯН│ results-api"] -->|"5432 тЬУ"| pg["ЁЯУТ postgres<br/>NetworkPolicy: from app=results-api, port 5432"]
    n["ЁЯУв notices-api"] -. "5432 тЬЧ" .-> pg
    r -. "22 тЬЧ" .-> pg
    w["ЁЯМР web (no policy selects it)"]
    any["any pod"] -->|"тЬУ everything allowed<br/>until default-deny"| w
    s["ЁЯФС Secret password: U3VtbWVyтАж<br/>base64 -d тЖТ readable"] --> k["ЁЯЧЭя╕П encryption at rest (KMS)<br/>or External Secrets"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрдХреГрддреА + рдПрдХ lab: https://school-edh.pages.dev/security/lesson-diagrams.html#l11

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рджреЛрдиреНрд╣реА defaults security рдЪреНрдпрд╛ рджреГрд╖реНрдЯреАрдиреЗ рдЪреБрдХреАрдЪреНрдпрд╛ рджрд┐рд╢реЗрд▓рд╛ рдЖрд╣реЗрдд. рдПрдХрд╛ pod рдордзреНрдпреЗ shell рдорд┐рд│рд╡рдгрд╛рд░рд╛ attacker (рдзрдбрд╛ 01 рдордзрд▓реЗ injection, рдзрдбрд╛ 13 рдордзрд▓реА рдЕрд╕реБрд░рдХреНрд╖рд┐рдд library) рдордЧ database, cache, metrics endpoints рдЖрдгрд┐ рдЗрддрд░ teams рдЪреНрдпрд╛ services рдкрд░реНрдпрдВрдд рдкреЛрд╣реЛрдЪреВ рд╢рдХрддреЛ тАФ рдЬреЛрдкрд░реНрдпрдВрдд рдПрдЦрд╛рджреА policy рддреНрдпрд╛рд▓рд╛ рдерд╛рдВрдмрд╡рдд рдирд╛рд╣реА. рдЖрдгрд┐ рд╕реБрд░рдХреНрд╖рд┐рдд "рджрд┐рд╕рдгрд╛рд░реЗ" Secret backups, CI logs рдЖрдгрд┐ kubectl get -o yaml output рдордзреНрдпреЗ рдЕрд╢рд╛ рд╕реНрд╡рд░реВрдкрд╛рдд copy рд╣реЛрддреЗ рдЬреЗ рдХреЛрдгреАрд╣реА decode рдХрд░реВ рд╢рдХрддреЛ. Default-deny рдЖрдгрд┐ encrypted, рдмрд╛рд╣реЗрд░реВрди managed secrets рдореБрд│реЗ рдПрдХ рдмрд┐рдШрдбрд▓реЗрд▓рд╛ pod рдлрдХреНрдд рдПрдХ рдмрд┐рдШрдбрд▓реЗрд▓рд╛ pod рд░рд╛рд╣рддреЛ тАФ рдкреВрд░реНрдг рд╢рд╛рд│рд╛ рдирд╛рд╣реА.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

sec/cloud.py рдордзрд▓реЗ netpol_allows(policies, src_labels, dst_labels, port) pod рдХрдбреЗ рдпреЗрдгрд╛рд▒реНрдпрд╛ ingress рдЪреЗ model рдХрд░рддреЗ:

  1. рдЬреНрдпрд╛ policies рдЪреЗ рд╕рдЧрд│реЗ pod labels destination pod рд╢реА рдЬреБрд│рддрд╛рдд рддреНрдпрд╛ рд╢реЛрдзрд╛;
  2. рдХреЛрдгрддреАрдЪ рддреНрдпрд╛рд▓рд╛ select рдХрд░рдд рдирд╕реЗрд▓ тЖТ True (рд╕рдЧрд│реНрдпрд╛рдВрд╕рд╛рдареА рдЙрдШрдбрд╛);
  3. рдирд╛рд╣реАрддрд░ тЖТ рддреЗрд╡реНрд╣рд╛рдЪ allowed рдЬреЗрд╡реНрд╣рд╛ select рдХрд░рдгрд╛рд▒реНрдпрд╛ рдПрдЦрд╛рджреНрдпрд╛ policy рдЪреЗ рд╕рдЧрд│реЗ from labels source рд╢реА рдЬреБрд│рддрд╛рдд рдЖрдгрд┐ port рддреНрдпрд╛рдЪреНрдпрд╛ ports рдордзреНрдпреЗ рдЕрд╕рддреЛ.

рд░рд┐рдХрд╛рдорд╛ pod={} рдкреНрд░рддреНрдпреЗрдХ pod select рдХрд░рддреЛ, рдЖрдгрд┐ ports=[] рдХрд╛рд╣реАрдЪ allow рдХрд░рдд рдирд╛рд╣реА тАФ рджреЛрдиреНрд╣реА рдорд┐рд│реВрди default-deny. sec/demo.py рдордзрд▓реЗ k8s_network() postgres pods рд╡рд░ рдПрдХ policy рд▓рд╛рд╡рддреЗ. Snippet рд╣реЗ рд╕реБрджреНрдзрд╛ рджрд╛рдЦрд╡рддреЛ рдХреА Secret рдЪреА value base64 рдордзреНрдпреЗ рдХрд╢реА рджрд┐рд╕рддреЗ тАФ рдзрдбрд╛ 07 рдЪреНрдпрд╛ scan рдордзрд▓рд╛ рдХрд╛рд▓реНрдкрдирд┐рдХ password рд╡рд╛рдкрд░реВрди.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 sec/demo.py k8s_network
python3 - <<'EOF'
import sys, base64; sys.path.insert(0, "sec"); from cloud import netpol_allows
deny_all = dict(pod={}, ports=[], **{"from": {}})                                  # selects every pod, allows nothing
db_rule  = dict(pod={"app": "postgres"}, ports=[5432], **{"from": {"app": "results-api"}})
web_rule = dict(pod={"app": "web"}, ports=[8080], **{"from": {"app": "ingress"}})
tries = (({"app": "notices-api"}, {"app": "web"}, 8080), ({"app": "ingress"}, {"app": "web"}, 8080),
         ({"app": "results-api"}, {"app": "postgres"}, 5432), ({"app": "web"}, {"app": "postgres"}, 5432))
for name, pols in (("no policies", []), ("default-deny only", [deny_all]), ("default-deny + 2 allows", [deny_all, db_rule, web_rule])):
    print(f"тФАтФА {name}")
    for src, dst, port in tries:
        print(f"   {src['app']:<12} тЖТ {dst['app']}:{port:<5} {'allowed' if netpol_allows(pols, src, dst, port) else 'blocked'}")
stored = base64.b64encode(b"Summer2026!school").decode()
print(f"тФАтФА a Secret as the API server stores it: password: {stored}")
print(f"   anyone who can read it runs base64 -d тЖТ {base64.b64decode(stored).decode()}")
EOF
python3 sec/test_sec.py

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

k8s_network рд╣реЗ рдЫрд╛рдкрддреЗ:

тХРтХРтХР k8s_network тХРтХРтХР
тФАтФА NetworkPolicy on the database pods: only results-api, only 5432
   results-api  тЖТ postgres:5432  allowed
   notices-api  тЖТ postgres:5432  blocked
   results-api  тЖТ postgres:22    blocked
   pods with no policy selecting them accept everything тЖТ True тАФ start every namespace with default-deny
   Kubernetes Secrets are only base64 тАФ enable encryption at rest, or use an external secret store

тЬЕ done тАФ every door checked

рддреБрдордЪрд╛ snippet рд╣реЗ рдЫрд╛рдкрддреЛ:

тФАтФА no policies
   notices-api  тЖТ web:8080  allowed
   ingress      тЖТ web:8080  allowed
   results-api  тЖТ postgres:5432  allowed
   web          тЖТ postgres:5432  allowed
тФАтФА default-deny only
   notices-api  тЖТ web:8080  blocked
   ingress      тЖТ web:8080  blocked
   results-api  тЖТ postgres:5432  blocked
   web          тЖТ postgres:5432  blocked
тФАтФА default-deny + 2 allows
   notices-api  тЖТ web:8080  blocked
   ingress      тЖТ web:8080  allowed
   results-api  тЖТ postgres:5432  allowed
   web          тЖТ postgres:5432  blocked
тФАтФА a Secret as the API server stores it: password: U3VtbWVyMjAyNiFzY2hvb2w=
   anyone who can read it runs base64 -d тЖТ Summer2026!school

Tests рдордзреНрдпреЗ тЬЕ L11 a NetworkPolicy lets only the named source in рдЖрд╣реЗ рдЖрдгрд┐ рд╢реЗрд╡рдЯ 16/16 passed рдиреЗ рд╣реЛрддреЛ.

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рдХреЛрдгрддреАрд╣реА policy рдирд╕рддрд╛рдирд╛ рдЪрд╛рд░рд╣реА connections рдЪрд╛рд▓рддрд╛рдд тАФ рдЕрдЧрджреА web тЖТ postgres рд╕реБрджреНрдзрд╛, рдЬреЗ рдХреЛрдгрд╛рд▓рд╛рдЪ рдирдХреЛ рд╣реЛрддреЗ. рдлрдХреНрдд default-deny рд╕рдЧрд│реЗрдЪ рдмрдВрдж рдХрд░рддреЗ (рд╕реБрд░рдХреНрд╖рд┐рдд, рдкрдг app рдмрдВрдж рдкрдбрддреЗ). Default-deny рдЖрдгрд┐ рджреЛрди рдЫреЛрдЯреЗ allow рдирд┐рдпрдо рд╢рд╛рд│реЗрд▓рд╛ рд╣рд╡реЗ рдЕрд╕рд▓реЗрд▓реЗ рдиреЗрдордХреЗ рджреЛрди рдорд╛рд░реНрдЧ рджреЗрддрд╛рдд, рдЖрдгрд┐ рдмрд╛рдХреАрдЪреЗ рджреЛрди рдмрдВрдж рд░рд╛рд╣рддрд╛рдд. Policies рдмреЗрд░реАрдЬ рд╣реЛрддрд╛рдд: allows рдирд╛ deny "рдХрд╛рдвреВрди рдЯрд╛рдХрд╛рд╡рд╛" рд▓рд╛рдЧрд▓рд╛ рдирд╛рд╣реА. рдЖрдгрд┐ "рдЧреБрдкреНрдд" password рдПрдХрд╛ рдУрд│реАрдд, рдХреЛрдгрддреНрдпрд╛рд╣реА key рд╢рд┐рд╡рд╛рдп, base64 рдордзреВрди рдкрд░рдд рдЖрд▓рд╛.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ namespace рд╕рд╛рдареА default-deny, рдПрдХ DNS allow, рдЖрдгрд┐ рд╢рд╛рд│реЗрд▓рд╛ рд╣рд╡реЗ рдЕрд╕рд▓реЗрд▓реЗ рджреЛрди рдорд╛рд░реНрдЧ:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny, namespace: school }
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: allow-dns, namespace: school }
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: postgres-from-results-api, namespace: school }
spec:
  podSelector: { matchLabels: { app: postgres } }
  policyTypes: [Ingress]
  ingress:
    - from: [{ podSelector: { matchLabels: { app: results-api } } }]
      ports: [{ protocol: TCP, port: 5432 }]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: results-api-to-postgres, namespace: school }
spec:
  podSelector: { matchLabels: { app: results-api } }
  policyTypes: [Egress]
  egress:
    - to: [{ podSelector: { matchLabels: { app: postgres } } }]
      ports: [{ protocol: TCP, port: 5432 }]

CNI рддреА рд▓рд╛рдЧреВ рдХрд░рддреЛ рд╣реЗ рд╕рд┐рджреНрдз рдХрд░рд╛ тАФ рдЪреБрдХреАрдЪреЗ label рдЕрд╕рд▓реЗрд▓рд╛ рдПрдХ рддрд╛рддреНрдкреБрд░рддрд╛ pod fail рд╡реНрд╣рд╛рдпрд▓рд╛ рд╣рд╡рд╛, рдЖрдгрд┐ рдпреЛрдЧреНрдп label рдЕрд╕рд▓реЗрд▓рд╛ connect рд╡реНрд╣рд╛рдпрд▓рд╛ рд╣рд╡рд╛:

kubectl -n school run np-test --rm -it --restart=Never --image=busybox:1.36 \
    --labels app=notices-api -- nc -zv -w 2 postgres 5432      # expect: timed out
kubectl -n school run np-test --rm -it --restart=Never --image=busybox:1.36 \
    --labels app=results-api -- nc -zv -w 2 postgres 5432      # expect: open

Amazon VPC CNI рдЕрд╕рд▓реЗрд▓реНрдпрд╛ EKS рд╡рд░, add-on configuration рдордзреНрдпреЗ policy enforcement рдЪрд╛рд▓реВ рдХрд░рд╛ ({"enableNetworkPolicy": "true"}), рдХрд┐рдВрд╡рд╛ Calico рдХрд┐рдВрд╡рд╛ Cilium рдЪрд╛рд▓рд╡рд╛.

Secrets рд╕рд╛рдареА encryption at rest тАФ рддреБрдореНрд╣реА рд╕реНрд╡рддрдГ рдЪрд╛рд▓рд╡рдд рдЕрд╕рд▓реЗрд▓реНрдпрд╛ cluster рд╡рд░, --encryption-provider-config рдиреЗ API server рд▓рд╛ рджрд┐рд▓реЗрд▓реА EncryptionConfiguration:

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources: ["secrets"]
    providers:
      - kms:
          apiVersion: v2
          name: school-kms
          endpoint: unix:///var/run/kmsplugin/socket.sock
          timeout: 3s
      - identity: {}        # only so Secrets written before the change can still be read

рдордЧ рдкреНрд░рддреНрдпреЗрдХ Secret рдкреБрдиреНрд╣рд╛ рд▓рд┐рд╣рд╛, рдореНрд╣рдгрдЬреЗ рддреА encrypted рд╕реНрд╡рд░реВрдкрд╛рдд рд╕рд╛рдард╡рд▓реА рдЬрд╛рддреАрд▓:

kubectl get secrets --all-namespaces -o json | kubectl replace -f -

EKS рд╡рд░, Secrets рд╕рд╛рдареА рддреБрдордЪреА рд╕реНрд╡рддрдГрдЪреА KMS key рдирд┐рд╡рдбрд╛:

aws eks associate-encryption-config --cluster-name school \
    --encryption-config '[{"resources":["secrets"],"provider":{"keyArn":"arn:aws:kms:ap-south-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"}}]'

External Secrets Operator тАФ password AWS Secrets Manager рдордзреНрдпреЗ рд░рд╛рд╣рддреЛ; operator Kubernetes Secret results-db рддрдпрд╛рд░ рдХрд░рддреЛ рдЖрдгрд┐ refresh рдХрд░рддреЛ:

apiVersion: external-secrets.io/v1
kind: SecretStore
metadata: { name: aws-secrets, namespace: school }
spec:
  provider:
    aws:
      service: SecretsManager
      region: ap-south-1
      auth:
        jwt:
          serviceAccountRef: { name: external-secrets-reader }
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: results-db, namespace: school }
spec:
  refreshInterval: 1h
  secretStoreRef: { name: aws-secrets, kind: SecretStore }
  target: { name: results-db }
  data:
    - secretKey: password
      remoteRef: { key: school/results-db, property: password }

(рдЬреБрдиреНрдпрд╛ operator releases рдордзреНрдпреЗ external-secrets.io/v1beta1 рд╡рд╛рдкрд░рд▓реЗ рдЬрд╛рддреЗ; рддреБрдореНрд╣реА рдЪрд╛рд▓рд╡рдд рдЕрд╕рд▓реЗрд▓реЗ version рддрдкрд╛рд╕рд╛.)

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: namespace рдордзреНрдпреЗ default-deny policy рдирд╕реЗрд▓ рддреЗрд╡реНрд╣рд╛ fail рд╣реЛрдгрд╛рд░рд╛ CI check рдЬреЛрдбрд╛, рдЖрдгрд┐ Git рдордзреНрдпреЗ data: рд╕рд╣ kind: Secret рд╕рд╛рдкрдбрд▓реНрдпрд╛рд╡рд░ fail рд╣реЛрдгрд╛рд░рд╛ secret scanner (рдзрдбрд╛ 07) рдЬреЛрдбрд╛. рджреЛрдиреНрд╣реА рдЪреБрдХрд╛ рдХрд░рд╛рдпрд▓рд╛ рд╕реЛрдкреНрдпрд╛ рдЖрд╣реЗрдд рдЖрдгрд┐ рдХреЛрдгреА рддреНрдпрд╛рдВрдЪрд╛ рд╡рд╛рдкрд░ рдХрд░реЗрдкрд░реНрдпрдВрдд рд╢рд╛рдВрдд рд░рд╛рд╣рддрд╛рдд.

тПня╕П рдкреБрдвреЗ

Pod рдХреЛрдг рддрдпрд╛рд░ рдХрд░реВ рд╢рдХрддреЛ рд╣реЗ RBAC рдард░рд╡рддреЗ. рддреНрдпрд╛рдЪреНрдпрд╛рдкрд░реНрдпрдВрдд рдХреЛрдг рдкреЛрд╣реЛрдЪреВ рд╢рдХрддреЛ рд╣реЗ NetworkPolicy рдард░рд╡рддреЗ. рдкрдг рдХреЛрдгрддреНрдпрд╛ рдкреНрд░рдХрд╛рд░рдЪрд╛ pod рдореБрд│рд╛рдд рдЪрд╛рд▓реВ рд╢рдХрддреЛ тАФ root рдореНрд╣рдгреВрди? privileged? host рдЪреА disk mount рдХрд░реВрди? рдкреБрдвреЗ: pod рдЪрд╛рд▓рдгреНрдпрд╛рдкреВрд░реНрд╡реАрдЪреЗ рджрд╛рд░.

git checkout lesson-12-admission

ЁЯХ╕я╕П Lesson 11 тАФ Kubernetes secrets & network policies: mirror writing is not a lock

ЁЯУН You are here: Lesson 11 of 16 ┬╖ Previous: lesson-10-k8s-rbac ┬╖ Next: lesson-12-admission


ЁЯУж What's in this branch

Lessons 01тАУ10, plus two surprises that every new cluster has. First: a Kubernetes Secret is only base64 тАФ an encoding, not encryption тАФ unless you turn on encryption at rest or keep the real secret in an external store. Second: every pod can talk to every other pod, until a NetworkPolicy selects it тАФ and only if the cluster's network plugin (CNI) enforces policies. You learn both, and the default-deny pattern.

ЁЯзТ Explain like I'm 5

The secret envelope. Aishwarya puts the database password in an envelope marked "SECRET". But inside, the password is not locked тАФ it is only written in mirror writing. Anyone who holds a mirror (and everyone has one) can read it. That is base64. To make it really secret, the office must lock the envelope with a key from the key safe (encryption at rest with KMS), or keep the password in the key safe itself and bring it only when needed (an external secret store).

The corridors. In a new school building, every door is open. Any pupil can walk into the staff room, the server room, the kitchen. The office can put a guard on a door with a list: "Only the results helper, and only through the side door number 5432."

Two rules about guards:

  1. A door without a guard stays open to everyone. So the office starts by putting a guard on every door with an empty list тАФ "nobody" тАФ and then adds names. That is default-deny.
  2. The guard list is only paper. If the building has no guard service (a CNI that enforces policies), the paper does nothing, and nobody tells you.

ЁЯЧ║я╕П Diagram

flowchart LR
    r["ЁЯН│ results-api"] -->|"5432 тЬУ"| pg["ЁЯУТ postgres<br/>NetworkPolicy: from app=results-api, port 5432"]
    n["ЁЯУв notices-api"] -. "5432 тЬЧ" .-> pg
    r -. "22 тЬЧ" .-> pg
    w["ЁЯМР web (no policy selects it)"]
    any["any pod"] -->|"тЬУ everything allowed<br/>until default-deny"| w
    s["ЁЯФС Secret password: U3VtbWVyтАж<br/>base64 -d тЖТ readable"] --> k["ЁЯЧЭя╕П encryption at rest (KMS)<br/>or External Secrets"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/security/lesson-diagrams.html#l11

тЭУ What

ЁЯдФ Why

Because the two defaults point the wrong way for security. An attacker who gets a shell in one pod (lesson 01's injection, a vulnerable library from lesson 13) can then reach the database, the cache, the metrics endpoints and other teams' services тАФ unless a policy stops them. And a Secret that "looks" protected is copied into backups, CI logs and kubectl get -o yaml output in a form anybody can decode. Default-deny plus encrypted, externally managed secrets turn one broken pod into one broken pod тАФ not the whole school.

ЁЯФз How (in this repo)

netpol_allows(policies, src_labels, dst_labels, port) in sec/cloud.py models ingress to a pod:

  1. find the policies whose pod labels all match the destination pod;
  2. if none select it тЖТ True (open to everyone);
  3. otherwise тЖТ allowed only if some selecting policy's from labels all match the source and the port is in its ports.

An empty pod={} selects every pod, and ports=[] allows nothing тАФ together, a default-deny. k8s_network() in sec/demo.py puts one policy on the postgres pods. The snippet also shows what a Secret value looks like in base64 тАФ using the made-up password from lesson 07's scan.

ЁЯзк Try it

python3 sec/demo.py k8s_network
python3 - <<'EOF'
import sys, base64; sys.path.insert(0, "sec"); from cloud import netpol_allows
deny_all = dict(pod={}, ports=[], **{"from": {}})                                  # selects every pod, allows nothing
db_rule  = dict(pod={"app": "postgres"}, ports=[5432], **{"from": {"app": "results-api"}})
web_rule = dict(pod={"app": "web"}, ports=[8080], **{"from": {"app": "ingress"}})
tries = (({"app": "notices-api"}, {"app": "web"}, 8080), ({"app": "ingress"}, {"app": "web"}, 8080),
         ({"app": "results-api"}, {"app": "postgres"}, 5432), ({"app": "web"}, {"app": "postgres"}, 5432))
for name, pols in (("no policies", []), ("default-deny only", [deny_all]), ("default-deny + 2 allows", [deny_all, db_rule, web_rule])):
    print(f"тФАтФА {name}")
    for src, dst, port in tries:
        print(f"   {src['app']:<12} тЖТ {dst['app']}:{port:<5} {'allowed' if netpol_allows(pols, src, dst, port) else 'blocked'}")
stored = base64.b64encode(b"Summer2026!school").decode()
print(f"тФАтФА a Secret as the API server stores it: password: {stored}")
print(f"   anyone who can read it runs base64 -d тЖТ {base64.b64decode(stored).decode()}")
EOF
python3 sec/test_sec.py

тЬЕ Verify тАФ what you should see

k8s_network prints:

тХРтХРтХР k8s_network тХРтХРтХР
тФАтФА NetworkPolicy on the database pods: only results-api, only 5432
   results-api  тЖТ postgres:5432  allowed
   notices-api  тЖТ postgres:5432  blocked
   results-api  тЖТ postgres:22    blocked
   pods with no policy selecting them accept everything тЖТ True тАФ start every namespace with default-deny
   Kubernetes Secrets are only base64 тАФ enable encryption at rest, or use an external secret store

тЬЕ done тАФ every door checked

Your snippet prints:

тФАтФА no policies
   notices-api  тЖТ web:8080  allowed
   ingress      тЖТ web:8080  allowed
   results-api  тЖТ postgres:5432  allowed
   web          тЖТ postgres:5432  allowed
тФАтФА default-deny only
   notices-api  тЖТ web:8080  blocked
   ingress      тЖТ web:8080  blocked
   results-api  тЖТ postgres:5432  blocked
   web          тЖТ postgres:5432  blocked
тФАтФА default-deny + 2 allows
   notices-api  тЖТ web:8080  blocked
   ingress      тЖТ web:8080  allowed
   results-api  тЖТ postgres:5432  allowed
   web          тЖТ postgres:5432  blocked
тФАтФА a Secret as the API server stores it: password: U3VtbWVyMjAyNiFzY2hvb2w=
   anyone who can read it runs base64 -d тЖТ Summer2026!school

The tests include тЬЕ L11 a NetworkPolicy lets only the named source in and end with 16/16 passed.

ЁЯПБ What you just proved

With no policies, all four connections work тАФ even web тЖТ postgres, which nobody wanted. Default-deny alone closes everything (safe, but the app is down). Default-deny plus two small allow rules gives exactly the two paths the school needs, and the other two stay closed. The policies add up: the allows did not have to "remove" the deny. And the "secret" password came back from base64 in one line, with no key.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ default-deny for the namespace, a DNS allow, and the two paths the school needs:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny, namespace: school }
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: allow-dns, namespace: school }
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: postgres-from-results-api, namespace: school }
spec:
  podSelector: { matchLabels: { app: postgres } }
  policyTypes: [Ingress]
  ingress:
    - from: [{ podSelector: { matchLabels: { app: results-api } } }]
      ports: [{ protocol: TCP, port: 5432 }]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: results-api-to-postgres, namespace: school }
spec:
  podSelector: { matchLabels: { app: results-api } }
  policyTypes: [Egress]
  egress:
    - to: [{ podSelector: { matchLabels: { app: postgres } } }]
      ports: [{ protocol: TCP, port: 5432 }]

Prove the CNI enforces it тАФ a throwaway pod with the wrong label must fail, one with the right label must connect:

kubectl -n school run np-test --rm -it --restart=Never --image=busybox:1.36 \
    --labels app=notices-api -- nc -zv -w 2 postgres 5432      # expect: timed out
kubectl -n school run np-test --rm -it --restart=Never --image=busybox:1.36 \
    --labels app=results-api -- nc -zv -w 2 postgres 5432      # expect: open

On EKS with the Amazon VPC CNI, turn policy enforcement on in the add-on configuration ({"enableNetworkPolicy": "true"}), or run Calico or Cilium.

Encryption at rest for Secrets тАФ on a cluster you run yourself, an EncryptionConfiguration passed to the API server with --encryption-provider-config:

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources: ["secrets"]
    providers:
      - kms:
          apiVersion: v2
          name: school-kms
          endpoint: unix:///var/run/kmsplugin/socket.sock
          timeout: 3s
      - identity: {}        # only so Secrets written before the change can still be read

Then write every Secret again, so they are stored encrypted:

kubectl get secrets --all-namespaces -o json | kubectl replace -f -

On EKS, choose your own KMS key for Secrets:

aws eks associate-encryption-config --cluster-name school \
    --encryption-config '[{"resources":["secrets"],"provider":{"keyArn":"arn:aws:kms:ap-south-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"}}]'

External Secrets Operator тАФ the password lives in AWS Secrets Manager; the operator creates and refreshes the Kubernetes Secret results-db:

apiVersion: external-secrets.io/v1
kind: SecretStore
metadata: { name: aws-secrets, namespace: school }
spec:
  provider:
    aws:
      service: SecretsManager
      region: ap-south-1
      auth:
        jwt:
          serviceAccountRef: { name: external-secrets-reader }
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: results-db, namespace: school }
spec:
  refreshInterval: 1h
  secretStoreRef: { name: aws-secrets, kind: SecretStore }
  target: { name: results-db }
  data:
    - secretKey: password
      remoteRef: { key: school/results-db, property: password }

(Older operator releases use external-secrets.io/v1beta1; check the version you run.)

ЁЯПн Why this matters in production: add a CI check that fails when a namespace has no default-deny policy, and a secret scanner (lesson 07) that fails on kind: Secret with data: in Git. Both mistakes are easy to make and silent until someone uses them.

тПня╕П Next

RBAC decides who may create a pod. NetworkPolicy decides who may reach it. But what kind of pod may run at all тАФ as root? privileged? with the host's disk mounted? Next: the gate before a pod runs.

git checkout lesson-12-admission
тЖР Previousk8s rbacNext тЖТadmission

This page is the lesson's README from the lesson-11-k8s-secrets-network branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.