ЁЯПл The SchoolтА║ЁЯЫдя╕П Platform EngineeringтА║ЁЯФС рдзрдбрд╛ 08 тАФ Secrets рдЖрдгрд┐ config as a service: рдХрд┐рд▓реНрд▓реНрдпрд╛рдВрдЪреЗ рдХрдкрд╛рдЯ
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯФС рдзрдбрд╛ 08 тАФ Secrets рдЖрдгрд┐ config as a service: рдХрд┐рд▓реНрд▓реНрдпрд╛рдВрдЪреЗ рдХрдкрд╛рдЯ

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 08 ┬╖ рдорд╛рдЧреАрд▓: lesson-07-paved-pipelines ┬╖ рдкреБрдвреАрд▓: lesson-09-policy-guardrails


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

рдзрдбреЗ 01тАУ07, рдЖрдгрд┐ secrets рдЖрдгрд┐ configuration as a service. Secrets рдПрдХрд╛рдЪ store рдордзреНрдпреЗ, рдкреНрд░рддреНрдпреЗрдХ team рдЪреНрдпрд╛ рд╕реНрд╡рддрдВрддреНрд░ path рдЦрд╛рд▓реА рд░рд╛рд╣рддрд╛рдд; team рдлрдХреНрдд рд╕реНрд╡рддрдГрдЪреЗрдЪ рд╡рд╛рдЪреВ рд╢рдХрддреЗ; рдкреНрд░рддреНрдпреЗрдХ рдмрджрд▓рд╛рддреВрди рдирд╡реЗ version рдмрдирддреЗ; rotation рдирдВрддрд░ store рджрд╛рдЦрд╡рддреЗ рдХреА рдХреЛрдгрддреЗ consumers рдЕрдЬреВрди рдЬреБрдиреЗ version рд╡рд╛рдкрд░рдд рдЖрд╣реЗрдд. Configuration layers рдордзреНрдпреЗ рдпреЗрддреЗ тАФ рдЖрдзреА defaults, рдордЧ environment, рдордЧ service тАФ рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ рдЕрдВрддрд┐рдо value рд╕рд╛рдВрдЧрддреЗ рдХреА рддреЗ рдХреЛрдгрддреНрдпрд╛ layer рдиреЗ рдард░рд╡рд▓реЗ. SecretStore рдЖрдгрд┐ layered_config() idp/selfserve.py рдордзреНрдпреЗ, secrets() idp/demo.py рдордзреНрдпреЗ.

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

рд╕реБрд╡рд┐рдзрд╛ рдХрд╛рд░реНрдпрд╛рд▓рдпрд╛рдд рдПрдХ рдХрд┐рд▓реНрд▓реНрдпрд╛рдВрдЪреЗ рдХрдкрд╛рдЯ рдЖрд╣реЗ. ЁЯФС рдкреНрд░рддреНрдпреЗрдХ рд╡рд┐рднрд╛рдЧрд╛рдЪрд╛ рд╕реНрд╡рддрдГрдЪрд╛ рдЦреБрдВрдЯрд╛ рдЖрд╣реЗ: science рдЪреНрдпрд╛ рдХрд┐рд▓реНрд▓реНрдпрд╛ science рдЪреНрдпрд╛ рдЦреБрдВрдЯреНрдпрд╛рд╡рд░, maths рдЪреНрдпрд╛ рдХрд┐рд▓реНрд▓реНрдпрд╛ maths рдЪреНрдпрд╛ рдЦреБрдВрдЯреНрдпрд╛рд╡рд░.

рдХрддрд░рд┐рдирд╛ science lab рдЪреА рдХрд┐рд▓реНрд▓реА рдорд╛рдЧрддреЗ: тЬЕ "рд╣реА рдШреНрдпрд╛ тАФ рдХрд┐рд▓реНрд▓реА version 1." рддреА maths рдЪреНрдпрд╛ рднрд╛рдВрдбрд╛рд░рдЦреЛрд▓реАрдЪреА рдХрд┐рд▓реНрд▓реА рдорд╛рдЧрддреЗ: тЭМ "рддреА maths рдЪреНрдпрд╛ рдЦреБрдВрдЯреНрдпрд╛рд╡рд░ рдЖрд╣реЗ. maths рд▓рд╛ рддреА рджреНрдпрд╛рдпрд▓рд╛ рд╕рд╛рдВрдЧрд╛."

рдЕрдзреВрдирдордзреВрди рджреАрдкрд┐рдХрд╛ рдХреБрд▓реВрдк рдмрджрд▓рддреЗ (рдореНрд╣рдгрдЬреЗ rotation) рдЖрдгрд┐ рдХрд┐рд▓реНрд▓реА version 2 рдЯрд╛рдВрдЧрддреЗ. рдЬреБрдиреА рдХрд┐рд▓реНрд▓реА рдХреЛрдгреА рдХреЛрдгреА рдиреЗрд▓реА рдпрд╛рдЪреА рдпрд╛рджреА рддреА рдареЗрд╡рддреЗ. Exam-results рдЪрд╛ рдХрд╛рд░рдХреВрди рдирд╡реА рдХрд┐рд▓реНрд▓реА рдШреНрдпрд╛рдпрд▓рд╛ рдкрд░рдд рдпреЗрддреЛ тЬЕ. Lab worker рдЕрдЬреВрди рдкрд░рдд рдЖрд▓реЗрд▓рд╛ рдирд╛рд╣реА тЪая╕П. рдореНрд╣рдгреВрди lab worker рдХрдбреЗрд╣реА рдирд╡реА рдХрд┐рд▓реНрд▓реА рдпреЗрдИрдкрд░реНрдпрдВрдд рджреАрдкрд┐рдХрд╛ рдЬреБрдиреЗ рдХреБрд▓реВрдк рдлреЗрдХреВрди рджреЗрдд рдирд╛рд╣реА.

рдЖрдгрд┐ рдХрд╛рд░реНрдпрд╛рд▓рдп рдкреНрд░рддреНрдпреЗрдХ рдЦреЛрд▓реАрд╕рд╛рдареА рдПрдХ settings рдкрддреНрд░рдХ рдареЗрд╡рддреЗ: рдЖрдзреА рд╢рд╛рд│реЗрдЪрд╛ default ("рджрд┐рд╡реЗ рдиреЗрд╣рдореАрд╕рд╛рд░рдЦреЗ"), рдордЧ рдЗрдорд╛рд░рддреАрдЪрд╛ рдирд┐рдпрдо ("рдкрд░реАрдХреНрд╖рд╛ рд╣реЙрд▓рдордзреНрдпреЗ 2 рдирд╡реНрд╣реЗ, 3 рд╕рдлрд╛рдИ рдХрд░реНрдордЪрд╛рд░реА"), рдордЧ рдЦреЛрд▓реАрдЪреА рд╕реНрд╡рддрдГрдЪреА рдЗрдЪреНрдЫрд╛ ("рдирд╡рд╛ grades board: рдЪрд╛рд▓реВ"). рдХреЛрдгреАрд╣реА рдкрддреНрд░рдХ рд╡рд╛рдЪреВрди рдкреНрд░рддреНрдпреЗрдХ рдУрд│ рдХреЛрдгрддреНрдпрд╛ рдирд┐рдпрдорд╛рдиреЗ рдард░рд╡рд▓реА рддреЗ рдкрд╛рд╣реВ рд╢рдХрддреЛ.

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

flowchart LR
    subgraph store["ЁЯФС secret store"]
      s1["teams/science/results-db-password<br/>v1 тЖТ v2 (rotated)"]
      s2["teams/maths/timetable-api-key"]
    end
    sci["ЁЯзк science"] -->|"тЬЕ own path"| s1
    sci -.->|"тЭМ denied"| s2
    s1 --> er["exam-results: reloaded v2"]
    s1 --> lw["lab-worker: still on v1 тЪая╕П"]
    subgraph cfg["ЁЯУД config layers"]
      d["defaults: replicas 2"] --> p["production: replicas 3"] --> sv["exam-results: feature_new_grades on"]
    end

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рд╣рд╛рддрд╛рдиреЗ рд╕рд╛рдВрднрд╛рд│рд▓реЗрд▓реЗ secrets leak рд╣реЛрддрд╛рдд: chat рдордзреНрдпреЗ paste рдХреЗрд▓реЗ рдЬрд╛рддрд╛рдд, "рдереЛрдбреНрдпрд╛ рд╡реЗрд│рд╛рд╕рд╛рдареА" commit рдХреЗрд▓реЗ рдЬрд╛рддрд╛рдд, laptop рд╡рд░ copy рдХрд░реВрди рд╡рд┐рд╕рд░рд▓реЗ рдЬрд╛рддрд╛рдд. рдЖрдгрд┐ рд╣рд╛рддрд╛рдиреЗ рдХреЗрд▓реЗрд▓реЗ rotation рдХреНрд╡рдЪрд┐рддрдЪ рд╣реЛрддреЗ, рдХрд╛рд░рдг рддреЗ рднреАрддреАрджрд╛рдпрдХ рдЕрд╕рддреЗ тАФ рдЬреБрдиреЗ value рдХреЛрдг рд╡рд╛рдкрд░рддреЗ рд╣реЗ рдХреЛрдгрд╛рд▓рд╛рдЪ рдорд╛рд╣реАрдд рдирд╕рддреЗ. рдкреНрд░рддреНрдпреЗрдХ team рд╕рд╛рдареА store, versions рдЖрдгрд┐ consumers рдЪреА рдпрд╛рджреА рдпрд╛рдВрдореБрд│реЗ rotation рдиреЗрд╣рдореАрдЪреЗ рдХрд╛рдо рдмрдирддреЗ. "рдХреЛрдгреА рдард░рд╡рд▓реЗ" рд╣реЗ рдЙрддреНрддрд░ рджреЗрдгрд╛рд▒реНрдпрд╛ layered config рдореБрд│реЗ "production рдордзреНрдпреЗ рд╣реЗ 3 рдХрд╛ рдЖрд╣реЗ?" рд╣рд╛ рдкреНрд░рд╢реНрди рддрдкрд╛рд╕рд╛рдЪрд╛ рд╡рд┐рд╖рдп рди рд░рд╛рд╣рддрд╛ рдлрдХреНрдд рдкрд╛рд╣рдгреНрдпрд╛рдЪрд╛ рд╡рд┐рд╖рдп рд╣реЛрддреЛ.

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

idp/selfserve.py рдордзрд▓реЗ SecretStore рдкреНрд░рддреНрдпреЗрдХ path рд╕рд╛рдареА values рдЪреА рдпрд╛рджреА рдареЗрд╡рддреЗ. get(team, path, consumer) teams/<team>/ рдЪреНрдпрд╛ рдмрд╛рд╣реЗрд░рдЪрд╛ рдХреЛрдгрддрд╛рд╣реА path рдирд╛рдХрд╛рд░рддреЗ, рдлрдХреНрдд version рдЖрдгрд┐ length рдкрд░рдд рджреЗрддреЗ (value рдХрдзреАрдЪ рдЫрд╛рдкрд▓реЗ рдЬрд╛рдд рдирд╛рд╣реА), рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ consumer рдХрдбреЗ рдХреЛрдгрддреЗ version рдЖрд╣реЗ рдпрд╛рдЪреА рдиреЛрдВрдж рдареЗрд╡рддреЗ. rotate() рдирд╡реЗ version рдЬреЛрдбрддреЗ; stale() рд╕рд░реНрд╡рд╛рдд рдирд╡реНрдпрд╛ version рдкреЗрдХреНрд╖рд╛ рдЬреБрдиреЗ version рдзрд░реВрди рдареЗрд╡рд▓реЗрд▓реНрдпрд╛ consumers рдЪреА рдпрд╛рджреА рджреЗрддреЗ. layered_config(*layers) (name, dict) layers рдбрд╛рд╡реАрдХрдбреВрди рдЙрдЬрд╡реАрдХрдбреЗ merge рдХрд░рддреЗ рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ key рд╕рд╛рдареА value рдЖрдгрд┐ рддреЗ рдард░рд╡рдгрд╛рд░рд╛ layer рдЬрдкреВрди рдареЗрд╡рддреЗ.

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

python3 idp/demo.py secrets
python3 - <<'EOF'
import sys; sys.path.insert(0, "idp"); from selfserve import SecretStore, layered_config
st = SecretStore()
st.put("teams/library/search-token", "tok-one-for-the-search")
print(st.get("library", "teams/library/search-token", consumer="library-search"))
print(st.get("library", "teams/library/missing"))
print(st.get("maths", "teams/library/search-token"))
st.rotate("teams/library/search-token", "tok-two"); st.rotate("teams/library/search-token", "tok-three")
print("after two rotations, stale:", st.stale(), "┬╖", st.get("library", "teams/library/search-token", consumer="library-search")[1])
cfg = layered_config(("defaults", {"log_level": "info", "replicas": 2}), ("preview", {"replicas": 1, "log_level": "debug"}),
                     ("library-search", {"log_level": "warning"}))
for k, (v, src) in cfg.items(): print(f"{k} = {v} (from {src})")
EOF

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

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

   science reads teams/science/results-db-password тЖТ ok ┬╖ teams/science/results-db-password v1 (22 characters, value hidden)
   science reads teams/maths/timetable-api-key     тЖТ denied ┬╖ science may read teams/science/* only тАФ ask the owner of teams/maths/timetable-api-key to share it
тФАтФА rotation: the password is now v2; consumers still on the old version: ['exam-results', 'lab-worker']
   exam-results reloads тЖТ still stale: ['lab-worker'] ┬╖ keep v1 valid until nobody uses it, then revoke
   replicas            = 3     (from production)

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

('ok', 'teams/library/search-token v1 (22 characters, value hidden)')
('missing', 'no secret at teams/library/missing')
('denied', 'maths may read teams/maths/* only тАФ ask the owner of teams/library/search-token to share it')
after two rotations, stale: ['library-search'] ┬╖ teams/library/search-token v3 (9 characters, value hidden)
log_level = warning (from library-search)
replicas = 1 (from preview)

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

Store рдиреЗ рддреАрди рд╡реЗрдЧрд╡реЗрдЧрд│реА рдЙрддреНрддрд░реЗ рджрд┐рд▓реА тАФ ok, missing, denied тАФ рдЖрдгрд┐ рдХрдзреАрдЪ value рдЫрд╛рдкрд▓реЗ рдирд╛рд╣реА. рджреЛрди rotations рдореБрд│реЗ v3 рдмрдирд▓реЗ; library-search рдкреБрдиреНрд╣рд╛ рд╡рд╛рдЪреЗрдкрд░реНрдпрдВрдд stale рд╣реЛрддреЗ, рдЖрдгрд┐ v3 рдЖрдгрдгрд╛рд▒реНрдпрд╛ рддреНрдпрд╛рдЪ call рдиреЗ рддреЗ рд╕рд╛рдл рдХреЗрд▓реЗ. Config рдордзреНрдпреЗ log_level рддреАрди рд╡реЗрд│рд╛ рдард░рд╡рд▓реЗ рдЧреЗрд▓реЗ рдЖрдгрд┐ рд╢реЗрд╡рдЯрдЪрд╛ layer рдЬрд┐рдВрдХрд▓рд╛; replicas preview рдордзреВрди рдЖрд▓реЗ, рдЖрдгрд┐ output рддрд╕реЗ рд╕рд╛рдВрдЧрддреЛ. "Stale consumers" рдЪреА рдпрд╛рджреАрдЪ рдЬреБрдиреЗ value revoke рдХрд░рдгреЗ рд╕реБрд░рдХреНрд╖рд┐рдд рдмрдирд╡рддреЗ.

ЁЯУП рд╢рд┐рдХрд╡рдгреНрдпрд╛рд╕рд╛рдареАрдЪрд╛ рдирдореБрдирд╛: рдЦрд░рд╛ store values encrypt рдХрд░рддреЛ, рдкреНрд░рддреНрдпреЗрдХ read рдЪреА рдиреЛрдВрдж рдареЗрд╡рддреЛ рдЖрдгрд┐ рд╢рдХреНрдп рддрд┐рдереЗ рдереЛрдбреНрдпрд╛ рдХрд╛рд│рд╛рдЪреЗ credentials рджреЗрддреЛ. рд╣рд╛ lab рдлрдХреНрдд paths, versions рдЖрдгрд┐ рдХреЛрдгрд╛рдХрдбреЗ рдХреЛрдгрддреЗ version рдЖрд╣реЗ рдПрд╡рдвреЗрдЪ рджрд╛рдЦрд╡рддреЛ.

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

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

HashiCorp Vault (KV version 2), рдЦрд▒реНрдпрд╛ server рд╡рд░:

vault kv put secret/teams/science/results-db-password value='тАж'
vault kv get -version=1 secret/teams/science/results-db-password     # an older version

Science рд▓рд╛ рдлрдХреНрдд рддрд┐рдЪрд╛рдЪ subtree рд╡рд╛рдЪреВ рджреЗрдгрд╛рд░реА policy (KV v2 API paths рдордзреНрдпреЗ рдЬреЛрдбрддреЛ рддреЛ data/ рднрд╛рдЧ рд▓рдХреНрд╖рд╛рдд рдШреНрдпрд╛):

path "secret/data/teams/science/*" {
  capabilities = ["read"]
}

External Secrets Operator Kubernetes Secret рд▓рд╛ store рд╢реА sync рдареЗрд╡рддреЛ (рдЬреБрдиреНрдпрд╛ releases рдордзреНрдпреЗ apiVersion: external-secrets.io/v1beta1 рд╡рд╛рдкрд░рддрд╛рдд):

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: results-db
  namespace: science
spec:
  refreshInterval: 1h
  secretStoreRef: {name: vault-science, kind: SecretStore}
  target: {name: results-db}
  data:
    - secretKey: password
      remoteRef: {key: teams/science/results-db-password, property: value}

AWS Secrets Manager rotation function рд╡рд╛рдкрд░реВрди рдХрд╛рд╣реА database secrets рдард░рд▓реЗрд▓реНрдпрд╛ рд╡реЗрд│рд╛рдкрддреНрд░рдХрд╛рдиреЗ rotate рдХрд░реВ рд╢рдХрддреЛ, рдпрд╛рдд "alternating users" strategy рдкрдг рдЖрд╣реЗ рдЬреА рдПрдХ credentials рд╕рдВрдЪ рдЪрд╛рд▓реВ рдареЗрд╡рддреЗ рдЖрдгрд┐ рджреБрд╕рд░рд╛ рдмрджрд▓рддреЗ.

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдЧрд░рдЬ рдкрдбрдгреНрдпрд╛рдЖрдзреАрдЪ rotation рдЪрд╛ рд╕рд░рд╛рд╡ рдХрд░рд╛. Secret рдХреЛрдг рд╡рд╛рдкрд░рддреЗ рд╣реЗ рдХреЛрдгрд╛рд▓рд╛рдЪ рд╕рд╛рдВрдЧрддрд╛ рдЖрд▓реЗ рдирд╛рд╣реА, рддрд░ рдкрд╣рд┐рд▓рд╛ рдЦрд░рд╛ leak рд╣рд╛ outage рдкрдг рдард░рддреЛ.

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

рднрд╛рдЧ 2 рдкреВрд░реНрдг рдЭрд╛рд▓рд╛: teams рд╕реНрд╡рддрдГрдЪреА рдорджрдд рд╕реНрд╡рддрдГ рдХрд░реВ рд╢рдХрддрд╛рдд. рднрд╛рдЧ 3 рд╡рд┐рдЪрд╛рд░рддреЛ рдХреА 'рдирд╛рд╣реА' рдЪреА рднрд┐рдВрдд рди рдмрдирддрд╛ рд╣реЗ рд╕реБрд░рдХреНрд╖рд┐рдд рдХрд╕реЗ рдареЗрд╡рд╛рдпрдЪреЗ тАФ рджреБрд░реБрд╕реНрддреА рд╕рдордЬрд╛рд╡реВрди рд╕рд╛рдВрдЧрдгрд╛рд░реЗ guardrails.

git checkout lesson-09-policy-guardrails

ЁЯФС Lesson 08 тАФ Secrets & config as a service: the key cabinet

ЁЯУН You are here: Lesson 08 of 12 ┬╖ Previous: lesson-07-paved-pipelines ┬╖ Next: lesson-09-policy-guardrails


ЁЯУж What's in this branch

Lessons 01тАУ07, plus secrets and configuration as a service. Secrets live in one store, under a path per team; a team can read only its own; every change makes a new version; after a rotation the store shows which consumers still hold the old one. Configuration comes in layers тАФ defaults, then environment, then service тАФ and every final value says which layer set it. SecretStore and layered_config() in idp/selfserve.py, secrets() in idp/demo.py.

ЁЯзТ Explain like I'm 5

The facilities office has a key cabinet. ЁЯФС Each department has its own hook: science keys on the science hook, maths keys on the maths hook.

Katrina asks for the science lab key: тЬЕ "Here тАФ key version 1." She asks for the maths store-room key: тЭМ "That is on the maths hook. Ask maths to share it."

Now and then Dipika changes a lock (a rotation) and hangs up key version 2. She keeps a list of who took the old key. The exam-results clerk comes back for the new one тЬЕ. The lab worker has not come back yet тЪая╕П. So Dipika does not throw the old lock away until the lab worker has the new key too.

And the office keeps one settings sheet per room: the school's default ("lights at normal"), then the building's rule ("the exam hall has 3 cleaners, not 2"), then the room's own wish ("new grades board: on"). Anyone can read the sheet and see which rule set each line.

ЁЯЧ║я╕П Diagram

flowchart LR
    subgraph store["ЁЯФС secret store"]
      s1["teams/science/results-db-password<br/>v1 тЖТ v2 (rotated)"]
      s2["teams/maths/timetable-api-key"]
    end
    sci["ЁЯзк science"] -->|"тЬЕ own path"| s1
    sci -.->|"тЭМ denied"| s2
    s1 --> er["exam-results: reloaded v2"]
    s1 --> lw["lab-worker: still on v1 тЪая╕П"]
    subgraph cfg["ЁЯУД config layers"]
      d["defaults: replicas 2"] --> p["production: replicas 3"] --> sv["exam-results: feature_new_grades on"]
    end

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

тЭУ What

ЁЯдФ Why

Because secrets handled by hand leak: pasted into chat, committed "for a moment", copied to a laptop and forgotten. And rotation done by hand is done rarely, because it is scary тАФ nobody knows who uses the old value. A store per team, with versions and a list of consumers, makes rotation routine. Layered config with a "set by" answer makes the question "why is this 3 in production?" a lookup instead of an investigation.

ЁЯФз How (in this repo)

SecretStore in idp/selfserve.py keeps a list of values per path. get(team, path, consumer) refuses any path outside teams/<team>/, returns only the version and length (the value is never printed), and records which version each consumer holds. rotate() appends a new version; stale() lists consumers holding an older version than the newest. layered_config(*layers) merges (name, dict) layers left to right and keeps, for every key, the value and the layer that set it.

ЁЯзк Try it

python3 idp/demo.py secrets
python3 - <<'EOF'
import sys; sys.path.insert(0, "idp"); from selfserve import SecretStore, layered_config
st = SecretStore()
st.put("teams/library/search-token", "tok-one-for-the-search")
print(st.get("library", "teams/library/search-token", consumer="library-search"))
print(st.get("library", "teams/library/missing"))
print(st.get("maths", "teams/library/search-token"))
st.rotate("teams/library/search-token", "tok-two"); st.rotate("teams/library/search-token", "tok-three")
print("after two rotations, stale:", st.stale(), "┬╖", st.get("library", "teams/library/search-token", consumer="library-search")[1])
cfg = layered_config(("defaults", {"log_level": "info", "replicas": 2}), ("preview", {"replicas": 1, "log_level": "debug"}),
                     ("library-search", {"log_level": "warning"}))
for k, (v, src) in cfg.items(): print(f"{k} = {v} (from {src})")
EOF

тЬЕ Verify тАФ what you should see

secrets prints:

   science reads teams/science/results-db-password тЖТ ok ┬╖ teams/science/results-db-password v1 (22 characters, value hidden)
   science reads teams/maths/timetable-api-key     тЖТ denied ┬╖ science may read teams/science/* only тАФ ask the owner of teams/maths/timetable-api-key to share it
тФАтФА rotation: the password is now v2; consumers still on the old version: ['exam-results', 'lab-worker']
   exam-results reloads тЖТ still stale: ['lab-worker'] ┬╖ keep v1 valid until nobody uses it, then revoke
   replicas            = 3     (from production)

Your snippet prints:

('ok', 'teams/library/search-token v1 (22 characters, value hidden)')
('missing', 'no secret at teams/library/missing')
('denied', 'maths may read teams/maths/* only тАФ ask the owner of teams/library/search-token to share it')
after two rotations, stale: ['library-search'] ┬╖ teams/library/search-token v3 (9 characters, value hidden)
log_level = warning (from library-search)
replicas = 1 (from preview)

ЁЯПБ What you just proved

The store answered three different ways тАФ ok, missing, denied тАФ and never printed a value. Two rotations made v3; library-search was stale until it read again, and the same call that fetched v3 cleared it. In the config, log_level was set three times and the last layer won; replicas came from preview, and the output says so. The "stale consumers" list is what makes it safe to revoke the old value.

ЁЯУП Teaching model: a real store encrypts values, logs every read and hands out short-lived credentials where it can. This lab only models paths, versions and who holds which version.

тЪая╕П Common mistakes

ЁЯПн In production

HashiCorp Vault (KV version 2), on a real server:

vault kv put secret/teams/science/results-db-password value='тАж'
vault kv get -version=1 secret/teams/science/results-db-password     # an older version

A policy that lets science read only its own subtree (note the data/ segment KV v2 adds to API paths):

path "secret/data/teams/science/*" {
  capabilities = ["read"]
}

The External Secrets Operator keeps a Kubernetes Secret in sync with the store (older releases use apiVersion: external-secrets.io/v1beta1):

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: results-db
  namespace: science
spec:
  refreshInterval: 1h
  secretStoreRef: {name: vault-science, kind: SecretStore}
  target: {name: results-db}
  data:
    - secretKey: password
      remoteRef: {key: teams/science/results-db-password, property: value}

AWS Secrets Manager can rotate some database secrets on a schedule with a rotation function, including an "alternating users" strategy that keeps one set of credentials valid while the other is changed.

ЁЯПн Why this matters in production: run a rotation drill before you need one. If nobody can tell who uses a secret, the first real leak becomes an outage too.

тПня╕П Next

Part 2 is done: teams can help themselves. Part 3 asks how to keep that safe without becoming a wall of "no" тАФ guardrails that explain the fix.

git checkout lesson-09-policy-guardrails
тЖР Previouspaved pipelinesNext тЖТpolicy guardrails

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