~/blog / sirlar-git-tarixidaback

Parol git tarixida qoladi — .env, env_file va compose secrets

9 daqiqa o'qish

Code review'da hamkasbingiz compose.yaml da ochiq turgan POSTGRES_PASSWORD: admin123 ni ko'rsatdi. Siz qiymatni ${POSTGRES_PASSWORD} ga almashtirdingiz, commit qildingiz, fayl toza. Ish yopildimi? Repozitoriy tarixiga qarang:

console
$ grep -n PASSWORD compose.yaml
6:      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
$ git log -S admin123 --oneline
68e623f parol fayldan olib tashlandi
ccc189b db servisi qo'shildi
$ git grep admin123 $(git rev-list --all)
ccc189b9246bb43c8604cfce0c4e5f70dbc2786a:compose.yaml:      POSTGRES_PASSWORD: admin123

git log -S fayllarda admin123 necha marta uchrashi o'zgargan commitlarni topadi: biri parolni qo'shgan, ikkinchisi olib tashlagan. git grep esa har bir commitdagi fayllar ichidan qidiradi va parolni butun qatori bilan qaytaradi. Repozitoriy nusxasi bor har kim — hamkasb, CI runner, fork egasi — buni bir soniyada qila oladi.

Quyida: nega tarix unutmaydi, Compose sirni qayerdan oladi va u yana qayerlarda ko'rinib qoladi, sizib chiqqan sirni qanday almashtirish va keyingisini commitdan oldin qanday ushlash. Chiqishlar shu maqola uchun sinov muhitida olingan (Compose v2.40, postgres:16-alpine, gitleaks v8.30.1; sinov loyihasining nomi blogdemosec).

Nega "o'chirdim" yetarli emas#

Git har commitda fayllarning to'liq holatini saqlaydi. Yangi commit faylning yangi holatini qo'shadi, eskisini o'chirmaydi: ccc189b ichidagi compose.yaml doim o'sha ko'rinishda qoladi. git push qilingan bo'lsa, bu tarix endi faqat sizniki emas:

  • Har bir git clone butun tarixni oladi: hamkasblarning noutbuklari, CI runner'lar, zaxira nusxalar.
  • Tarixni qayta yozsangiz ham, GitHub'ning o'z qo'llanmasiga (yangi oynada) ko'ra, sir fork'larda, boshqalarning clone'larida, keshlangan sahifalarda va unga havola qilgan Pull Request'larda qolishi mumkin.
  • Sir git'dan tashqariga ham chiqadi: docker compose config yoki env chiqargan CI logi, image qatlamlari, chatga nusxalangan docker inspect.

Shuning uchun o'sha qo'llanma ham birinchi qadam qilib tarixni tozalashni emas, sirni bekor qilish yoki almashtirishni qo'yadi.

Compose qiymatni qayerdan oladi#

Chalkashlikning yarmi shundan: Compose'da qiymat uch yo'l bilan keladi va ular har xil ishlaydi.

ManbaKim o'qiydiKonteynerga tushadimi
.env (loyiha katalogida)Compose, ${VAR} ni to'ldirish uchunYo'q, faqat ${VAR} yozilgan joyga
env_file: api.envCompose, konteyner muhiti uchunHa, fayldagi hamma kalit
environment:ComposeHa; bir xil kalitda env_file dan ustun

.env Compose'ning o'zi uchun: faylni o'qiyotganda u ${VAR} larni shu fayldan va shell muhitidan to'ldiradi, lekin .env ning o'zi konteynerga bormaydi. Tekshirib ko'ring:

yaml
services:
  app:
    image: alpine:latest
    command: ["sleep", "3600"]
    environment:
      DB_PASSWORD: ${POSTGRES_PASSWORD}
console
$ cat .env
POSTGRES_PASSWORD=admin123
$ docker compose run --rm app printenv DB_PASSWORD
admin123
$ docker compose run --rm app printenv POSTGRES_PASSWORD
$ echo $?
1

Konteynerga faqat environment: da nomi yozilgan DB_PASSWORD yetib keldi. Ikkinchi qoida: shell muhitidagi o'zgaruvchi .env dagidan ustun. POSTGRES_PASSWORD=shelldan docker compose config qilsangiz, chiqishda DB_PASSWORD: shelldan turadi. CI'da shu nomli o'zgaruvchi bo'lsa, .env indamay chetda qoladi.

O'zgaruvchi umuman bo'lmasa, oddiy ${POSTGRES_PASSWORD} bilan Compose faqat ogohlantiradi — The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string. — va bo'sh qator bilan davom etadi. Deploy o'tadi, ilova esa bo'sh parol bilan ulanmoqchi bo'ladi. Qiymat majburiy bo'lsa, buni faylning o'zida yozing:

yaml
    environment:
      DB_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD .env da yo'q}
console
$ docker compose config
error while interpolating services.app.environment.DB_PASSWORD: required variable POSTGRES_PASSWORD is missing a value: POSTGRES_PASSWORD .env da yo'q

:? o'zgaruvchi yo'q bo'lsa ham, bo'sh bo'lsa ham to'xtatadi va noldan farqli kod qaytaradi, deploy skripti ham shu yerda to'xtaydi.

env_file: esa konteyner uchun: fayldagi har bir KALIT=qiymat muhitga tushadi. Sinovda api.env da LOG_LEVEL=info, environment: da esa LOG_LEVEL: warning edi — konteynerga warning keldi. api va worker bir xil sirni olsa, ikkalasiga bitta env_file bering, aks holda bir kun ikki joydagi qiymatlar farq qilib qoladi.

Qiymat yana qayerda ko'rinadi#

Sirni fayldan chiqardingiz, lekin Docker uni boshqa joylarda ko'rsatishda davom etadi.

docker compose config o'zgaruvchilarni to'ldirib chiqaradi — .env dagini ham, env_file dagini ham. Faylni tekshirishga qulay, ammo bu chiqish CI logiga tushsa, sir ham tushadi. CI'da faqat to'g'rilikni tekshirish kerak bo'lsa, docker compose config --quiet ishlating: u hech narsa chiqarmaydi, faqat kod qaytaradi.

docker inspect konteyner muhitini ochiq saqlaydi:

console
$ docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' blogdemosec-api-1 | grep DATABASE
DATABASE_URL=postgresql://app:61aefe1cbcaa140850a971f40be04d63@db:5432/app

Docker API'ga kira oladigan har kim buni ko'radi, docker guruhi a'zolari ham. Buyruq argumentlari ham xuddi shunday ochiq: ps da ham, docker inspect da ham. Bu holat Token buyruq satrida lab'ida bor.

Image tarixi. Build uchun kerak bo'lgan token ARG orqali berilsa, qiymat image metama'lumotiga yoziladi:

dockerfile
FROM alpine:latest
ARG NPM_TOKEN
RUN echo "token uzunligi: ${#NPM_TOKEN}"
RUN --mount=type=secret,id=npm_token wc -c /run/secrets/npm_token
console
$ docker history --no-trunc --format '{{.CreatedBy}}' blogdemo-build:tmp
RUN |1 NPM_TOKEN=tok-demo-123 /bin/sh -c wc -c /run/secrets/npm_token # buildkit
RUN |1 NPM_TOKEN=tok-demo-123 /bin/sh -c echo "token uzunligi: ${#NPM_TOKEN}" # buildkit
ARG NPM_TOKEN=tok-demo-123
CMD ["/bin/sh"]

ARG dan keyingi har bir RUN qiymatni o'zi bilan olib yuradi, hatto uni ishlatmasa ham. --mount=type=secret bilan berilgan faylning mazmuni esa tarixga yozilmaydi. Build buyrug'i: docker build --secret id=npm_token,src=npm_token.txt .. Compose'da buning uchun servisda build.secrets kaliti bor.

Sir fayl sifatida: secrets: va *_FILE#

yaml
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
secrets:
  db_password:
    file: ./secrets/db_password.txt

Compose ./secrets/db_password.txt ni konteynerga /run/secrets/db_password sifatida, faqat o'qish uchun ulaydi. postgres image'ining entrypoint'i POSTGRES_PASSWORD_FILE ni ko'rsa, parolni shu fayldan o'qiydi. docker inspect da endi parol emas, faqat yo'l turadi:

console
$ docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' blogdemosec-db-1 | grep POSTGRES
POSTGRES_USER=app
POSTGRES_PASSWORD_FILE=/run/secrets/db_password

*_FILE — Docker'ning funksiyasi emas, rasmiy image'lar (postgres, mysql, mariadb va boshqalar) entrypoint skriptlarining kelishuvi. O'z ilovangizda uni o'zingiz yozasiz:

python
import os
from pathlib import Path


def secret(name: str) -> str:
    """NAME_FILE berilgan bo'lsa — fayldan, aks holda NAME dan."""
    path = os.environ.get(f"{name}_FILE")
    if path:
        # echo yoki openssl yozgan fayl oxirida "\n" bor — u parolga qo'shilib ketmasin.
        # strip() emas: parol bo'shliq bilan tugashi mumkin.
        return Path(path).read_text().rstrip("\r\n")
    value = os.environ.get(name)
    if not value:
        raise RuntimeError(f"{name} ham, {name}_FILE ham berilmagan")
    return value

Bu usul nima beradi va nima bermaydi:

  • Qiymat compose faylida, docker compose config chiqishida va docker inspect dagi muhitda yo'q.
  • Swarm'siz oddiy Compose'da sir shifrlanmaydi: bu host'dagi faylning read-only bind mount'i, docker inspect dagi Mounts uning yo'lini ko'rsatadi. Faylni repozitoriydan tashqarida saqlang va chmod 600 bilan yoping.
  • Konteyner ichida sir baribir ochiq. postgres entrypoint'i faylni o'qib, qiymatni server jarayonining muhitiga eksport qiladi: sinovda postgres foydalanuvchisi /proc/1/environ dan POSTGRES_PASSWORD ni o'qidi. *_FILE ni o'zi o'qiydigan ilovada esa qiymat faqat xotirada turadi va muhitni to'liq chiqaradigan debug sahifasiga tushmaydi (DEBUG=1 ishlab chiqarishda).

CI sirni muhit o'zgaruvchisi sifatida bersa, uni faylga yozish shart emas — sir manbai environment: ham bo'la oladi:

yaml
secrets:
  api_token:
    environment: API_TOKEN

Compose API_TOKEN ni o'z muhitidan yoki .env dan olib, konteynerda /run/secrets/api_token faylini yaratadi. Konteyner muhitida esa API_TOKEN bo'lmaydi.

Sir sizib chiqdi: tartib bilan almashtirish#

Postgres misolida ko'ramiz, chunki unda tuzoq bor. POSTGRES_PASSWORD (va _FILE) faqat birinchi ishga tushishda, ma'lumotlar katalogi bo'sh bo'lganda ishlatiladi. Keyingi startlarda image unga qaramaydi:

text
db-1  | PostgreSQL Database directory appears to contain a database; Skipping initialization

Sinovda sir faylini yangilab, db ni --force-recreate bilan qayta yaratdik: eski parol ishlashda davom etdi, yangisi rad etildi. Parolni bazaning o'zida almashtirish kerak.

  1. Yangi qiymat yarating: openssl rand -hex 32.
  2. Bazada almashtiring. psql ning \password buyrug'i yangi parolni so'raydi va uni serverga xeshlangan holda yuboradi, shuning uchun parol na buyruqlar tarixida, na server logida ochiq qoladi. ALTER ROLE ... PASSWORD '...' da esa ochiq qolishi mumkin. Rasmiy image'da konteyner ichidagi socket orqali ulanish parol so'ramaydi.
  3. Yangi qiymatni sir fayliga yoki .env ga yozing va uni ishlatadigan servislarni qayta yarating: docker compose up -d --force-recreate api worker.
  4. Eski qiymat endi ishlamasligini tarmoq orqali, boshqa konteynerdan tekshiring.
console
$ docker compose exec db psql -U app -d app
app=# \password app
Enter new password for user "app":
Enter it again:
app=# \q
$ docker run --rm --network blogdemosec_default -e PGPASSWORD="$OLD" postgres:16-alpine psql -h db -U app -d app -c 'select 1'
psql: error: connection to server at "db" (172.30.0.3), port 5432 failed: FATAL:  password authentication failed for user "app"

3-qadamdagi --force-recreate bejiz emas. Sir fayli mazmuni o'zgarganini Compose sezmaydi: sinovda oddiy up -d konteynerlarni Running holida qoldirdi. docker compose restart ham yordam bermaydi: u o'sha konteynerni o'sha muhit bilan qayta ishga tushiradi. .env o'zgargandan keyin ham docker inspect eski qiymatni ko'rsatdi.

API tokenlar uchun tartib o'sha: provayderda yangisini chiqaring, iste'molchilarni yangilang, eskisini bekor qiling. Provayderda kirish jurnali bo'lsa, eski token bilan kim kirganini ko'rib chiqing.

Shundan keyingina tarix haqida o'ylang. Kerak bo'lsa git filter-repo (alohida o'rnatiladi) faylni yoki qiymatni tarixdan olib tashlaydi, lekin commit hash'lari o'zgaradi, force-push kerak bo'ladi, boshqalarning clone'lariga esa bu tegmaydi. Almashtirilgan sir tarixda qolsa ham endi hech narsani ochmaydi.

Keyingisini commitdan oldin ushlash#

.gitignore ga ikki qator:

text
.env
secrets/

Namuna uchun .env.example ni commit qiling — unda kalitlar bor, qiymatlar yo'q. Serverdagi .env ni esa faqat egasi o'qiy olsin: chmod 600 .env.

Bir tuzoq bor: .gitignore faqat kuzatilmayotgan fayllarga ta'sir qiladi. .env avval commit qilingan bo'lsa, git uni kuzatishda davom etadi:

console
$ git status --short
 M .env
?? .gitignore
$ git check-ignore -v .env
$ git rm --cached .env
rm '.env'
$ git check-ignore -v .env
.gitignore:1:.env	.env

git check-ignore avval hech narsa demadi, chunki kuzatilayotgan faylga ignore qoidasi ishlamaydi. git rm --cached faylni diskda qoldirib, indeksdan chiqaradi. Tarixdagi nusxalar esa joyida qoladi.

Qo'l bilan saqlanadigan tartib bir kun buziladi, shuning uchun commitdan oldin avtomatik tekshiruv qo'ying. gitleaks'ni pre-commit (yangi oynada) orqali ulash:

yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.30.1
    hooks:
      - id: gitleaks

pre-commit install dan keyin har commitda gitleaks git --pre-commit --redact --staged --verbose ishlaydi. U faqat stage qilingan o'zgarishlarni tekshiradi, topilgan sirni logda REDACTED qilib ko'rsatadi va commitni to'xtatadi. Hook'ni git commit --no-verify bilan chetlab o'tish mumkin, shuning uchun CI'da butun tarixni ham tekshiring:

console
$ gitleaks git --no-banner -v --redact
Finding:     SECRET_KEY=REDACTED
Secret:      REDACTED
RuleID:      generic-api-key
Entropy:     3.797127
File:        .env
Line:        1
Commit:      ff29f88884f52482ad694dcd9dfac7754c04410d
Author:      demo
Email:       demo@example.com
Date:        2026-09-21T08:53:22Z
Fingerprint: ff29f88884f52482ad694dcd9dfac7754c04410d:.env:generic-api-key:1

8:54AM INF 3 commits scanned.
8:54AM INF scanned ~271 bytes (271 bytes) in 1.13s
8:54AM WRN leaks found: 1

Endi nima topilmaganiga qarang. O'sha repozitoriyda admin123 ikki joyda bor edi — compose.yaml tarixida va .env ning ikkinchi qatorida — gitleaks ularning birortasini ko'rsatmadi. U ma'lum formatdagi tokenlarni va entropiyasi yuqori qatorlarni qidiradi, odam o'ylab topgan qisqa parol esa bu filtrdan o'tib ketadi. Skaner — himoya to'ri, qoidaning o'rnini bosmaydi. Qoida esa oddiy: sir faylga yozilmaydi.

Tekshiruv#

  1. git ls-files | grep -E '(^|/)\.env$|^secrets/' — hech narsa chiqmasligi kerak.
  2. gitleaks git -v butun tarix bo'yicha no leaks found deydi, lekin qisqa parollarni u tutmasligini unutmang.
  3. docker compose config --quiet 0 qaytaradi; majburiy o'zgaruvchi yetishmasa, qaysi biri ekanini aytadi.
  4. _FILE ishlatadigan servisning docker inspect muhitida parol emas, fayl yo'li turadi.
  5. Almashtirilgan eski sir bilan ulanish rad etiladi.

Amaliyot#

Quyidagi lab'larda sirlar faylda ochiq turibdi — ularni tashqariga chiqaring va server tekshiruvidan o'tkazing:

labosonParol compose faylida`POSTGRES_PASSWORD: admin123` — bu fayl repozitoriyada, demak parol ham.mashqni ochish labosonO'n ikki o'zgaruvchi — bitta fayl`environment:` ostida 12 qator. Ular `.env` da bo'lishi va `env_file` bilan kelishi kerak.mashqni ochish labqiyinSir fayl orqali: compose `secrets`Muhit o'zgaruvchisi `docker inspect` da ko'rinadi. `secrets` esa faylga o'rnatiladi.mashqni ochish labqiyinAudit: faylda birorta sir qolmasinUch servis, beshta sir, bitta DEBUG — hammasi ochiq. Tozalang.mashqni ochish

chmod 600 .env va openssl rand bilan sir yaratish qo'l xotirasiga o'tishi uchun:

buyruq mashqi8-bosqich · 16 vazifaTarmoq va xavfsizlikOchiq portlar, kalitlar, sertifikatlar va sirlar. Serverni himoyalashning kundalik ishi.bosqichni boshlash