IlmHamroh
JavaScript Full-stack/31-qism. Prodakshn backend va AI integratsiya7/34-dars23 daqiqa
Mundarija (29)

Ma'lumotlar bazasi xavfsizligi: TLS, eng kam huquq va ustun shifrlash

Qisqacha: Baza — ilovaning eng qimmat qismi, uni bir necha qatlam himoyalaydi. Port faqat ichki tarmoqda, ulanish TLS bilan va server sertifikati tekshiriladi (sslmode=verify-full), ilova superuser emas — eng kam huquqli rol bilan ishlaydi, eng nozik ustunlar shifrlanadi. Kanon tajribasida sslmode=require darajasidagi ulanish begona sertifikat bilan ham ulanib ketdi — shifrlangan, lekin kim bilan gaplashayotganini tekshirmagan. Ilova roli esa DROP TABLE ham, RLS'ni o'chirish ham qila olmadi.

Bu darsda

  • Bazani himoyalash qatlamlarini chizib tushuntirasiz.
  • sslmode darajalarini va require bilan verify-full farqini bilasiz.
  • Ilova rolining huquqlarini o'z bazangizda tekshirasiz.
  • pgcrypto bilan ustunni shifrlaysiz va shifrlangan ustun bo'yicha qidirish uchun blind index qurasiz.
  • Analitik rol uchun PII'ni maskalaydigan view yasaysiz.

Oldin bilishingiz kerak: Rollar va huquqlar, Row Level Security, HTTPS, TLS va HTTP/2, crypto 2: parolni hash'lash, shifrlash va imzo, Sirlarni boshqarish.

Oldingi ikki darsda bazani o'stirishni o'rgandik. Endi uni himoyalaymiz. 28-qismda ilovani himoyaladik: parollar, sessiyalar, injection. Bu dars — bir qavat pastda: bazaning o'zi.

1. Nega bu kerak?

Haqiqiy voqealarning ko'pi murakkab hujum emas, oddiy e'tiborsizlik:

  • Postgres porti (5432) internetga ochiq qolgan, postgres parolini esa taxmin qilish oson.
  • Ilova bazaga shifrsiz ulanadi — bir tarmoqdagi har kim so'rovlarni o'qiydi.
  • Ilova postgres superuser bilan ishlaydi — bitta SQL injection butun bazani o'chiradi.
  • Zaxira fayli ochiq S3 bucket'da yotibdi — undagi telefon raqamlari ochiq matnda.

Har bir punkt — alohida qatlam. Chuqur himoya (defense in depth) tamoyilini Xavfsizlik tamoyillari darsidan bilasiz: bitta qatlam yiqilsa, keyingisi ushlab qoladi.

O'xshatish: bank seyfi. Bino eshigi qo'riqlanadi (tarmoq), seyf xonasiga faqat ruxsatnoma bilan kiriladi (autentifikatsiya), kassir faqat o'z kassasini ochadi (huquq), eng qimmat narsalar alohida qutida, kaliti mijozda (shifrlash), har kirish jurnalga yoziladi (audit).

flowchart TB
  T["1. Tarmoq: port faqat ichkarida"] --> S["2. Transport: TLS, verify-full"]
  S --> A["3. Autentifikatsiya: scram-sha-256"]
  A --> H["4. Huquq: rol, RLS"]
  H --> M["5. Ma'lumot: shifrlash, maskalash"]
  M --> K["6. Kuzatuv: audit, log"]

Diagrammani yuqoridan o'qing: hujumchi har qatlamdan o'tishi kerak. Dars shu tartibda boradi.

2. Tarmoq: portni ochmaslik

Eng oddiy va eng samarali qadam — bazani internetdan ko'rinmaydigan qilish. Ikki sozlama:

  • listen_addresses — Postgres qaysi tarmoq manzillarida ulanish kutadi. localhost — faqat shu kompyuterdan. * — hamma manzildan.
  • pg_hba.conf — "kim, qaysi bazaga, qayerdan, qanday usul bilan" qoidalari. HBA — host-based authentication.

Bizning test klasterimizda (biz ishga tushirdik, PostgreSQL 17.6, initdb sukut sozlamalari):

sql
SHOW listen_addresses;
SELECT type, address, auth_method
  FROM pg_hba_file_rules WHERE database::text = '{all}';
text
 listen_addresses
------------------
 localhost
(1 row)

 type  |  address  | auth_method
-------+-----------+-------------
 local |           | trust
 host  | 127.0.0.1 | trust
 host  | ::1       | trust
(3 rows)

localhost — yaxshi. trust esa — parolsiz kirish: faqat lokal tajriba uchun. Bu blok nocheck: sizning o'rnatmangizda boshqacha bo'lishi mumkin. Masalan, Windows o'rnatuvchisi odatda listen_addresses = '*' va scram-sha-256 qo'yadi. O'zingiznikini tekshiring.

Prodakshnda qoidalar: baza ilova bilan bitta yopiq tarmoqda, port tashqariga ochilmaydi, pg_hba.conf faqat ilova serverlari manzillariga va faqat hostssl (TLS bilan) ruxsat beradi. Kanon ham shunday qildi: hamma konteyner portlari faqat 127.0.0.1 da (konteyner nomlari qisqartirilgan).

text
=== (4) tinglash manzillari
  mailpit 127.0.0.1:5682->1025/tcp, 127.0.0.1:5683->8025/tcp
  minio 127.0.0.1:5680->9000/tcp, 127.0.0.1:5681->9001/tcp
  redis 127.0.0.1:6384->6379/tcp
  pg 127.0.0.1:5496->5432/tcp

Har qatordagi 127.0.0.1: — konteyner porti faqat shu kompyuterda ochiq. Docker'da -p 5432:5432 deb yozilsa, port hamma tarmoq interfeyslarida ochiladi — ko'pincha xato aynan shu yerda. Docker'ni 33-qismda o'rganamiz; hozircha bu qoidani eslab qoling.

Diqqat: Redis va boshqa xizmatlarga ham xuddi shu qoida. Parolsiz, internetga ochiq Redis — eng tez buziladigan xizmatlardan biri.

3. Transport: TLS va sslmode

3.1 sslmode darajalari

Ilova va baza orasidagi trafik ham himoyalanishi kerak. Postgres TLS'ni qo'llaydi (HTTPS dagi o'sha protokol — HTTPS, TLS va HTTP/2).

Jadvaldan oldin bitta so'z. Sertifikat markazi (CA, certificate authority) — sertifikatlarni imzolaydigan tashkilot yoki fayl. Xuddi pasport stoli kabi: pasportga ishonasiz, chunki uni ishonchli idora bergan. Mijoz serverning sertifikatini "qaysi CA imzolagan?" deb tekshiradi.

Mijoz tomonda asosiy sozlama — sslmode:

sslmode Shifrlaydimi Serverni tekshiradimi
disable yo'q yo'q
prefer (libpq sukuti) iloji bo'lsa yo'q
require ha yo'q
verify-ca ha sertifikat ishonchli CA'dan
verify-full ha CA + host nomi

Muhim farq — require va verify-full orasida. require trafikni shifrlaydi, lekin sertifikat kimniki ekanini tekshirmaydi. Tarmoqdagi hujumchi o'zini baza qilib ko'rsatsa (MITM — man-in-the-middle, "o'rtadagi odam"), require u bilan ham bajonidil shifrlangan ulanish quradi va parolni unga beradi.

3.2 Kanon tajribasi

Kanon alohida Postgres konteynerini TLS bilan ko'tardi: o'z sertifikat markazi (CA), 127.0.0.1 uchun server sertifikati, faqat TLSv1.3, pg_hba.conf da faqat hostssl. Keyin node-postgres bilan besh xil ulanishni sinadi. Skript yadrosi (qatorlar bo'lindi, parol — faqat shu vaqtinchalik konteyner uchun lokal qiymat, qisqartirilgan):

js
import pg from "pg";
import { readFileSync } from "node:fs";

const D = process.argv[2]; // sertifikatlar papkasi
const asos = {
  host: "127.0.0.1", port: 5601, user: "postgres",
  password: "ssl-lokal", database: "postgres",
  connectionTimeoutMillis: 5000,
};
async function sina(nom, ssl) {
  const c = new pg.Client({ ...asos, ssl });
  try {
    await c.connect();
    const { rows: [r] } = await c.query(
      "SELECT ssl, version, cipher FROM pg_stat_ssl " +
        "WHERE pid = pg_backend_pid()");
    console.log(`  ${nom.padEnd(52)} → ulandi: ` +
      `ssl=${r.ssl} ${r.version ?? ""} ${r.cipher ?? ""}`);
  } catch (e) {
    console.log(`  ${nom.padEnd(52)} → XATO: ` +
      `${e.code ?? ""} ${e.message.slice(0, 110)}`);
  } finally {
    await c.end().catch(() => {});
  }
}
await sina("ssl: false (sslmode=disable)", false);
await sina("ssl: { rejectUnauthorized: false } (sslmode=require)",
  { rejectUnauthorized: false });
await sina("ssl: { ca: o'z CA } (sslmode=verify-full)",
  { ca: readFileSync(`${D}/ca.crt`, "utf8") });
await sina("ssl: { ca: BEGONA CA } (verify-full, MITM)",
  { ca: readFileSync(`${D}/begona-ca.crt`, "utf8") });
await sina("ssl: true (tizim CA'lari — o'z-o'zini imzolagan rad)",
  true);

node-postgres'da sslmode so'zi yo'q — o'rniga ssl obyekti. rejectUnauthorized: false — sertifikatni tekshirmaslik, ya'ni require ning o'zi. ca berilsa — sertifikat shu CA bilan va host nomi bilan tekshiriladi, bu verify-full.

Natijada har qatorning oxiriga qarang: ulandi yoki XATO. Bizga kerakli manzara — faqat to'g'ri CA bilan ulanish:

text
=== (1) TLS rejimlari (server: hostssl, TLSv1.3)
  ssl: false (sslmode=disable)                         → XATO: 28000 no pg_hba.conf entry for host "172.17.0.1", user "postgres", database "postgres", no encryption
  ssl: { rejectUnauthorized: false } (sslmode=require) → ulandi: ssl=true TLSv1.3 TLS_AES_256_GCM_SHA384
  ssl: { ca: o'z CA } (sslmode=verify-full)            → ulandi: ssl=true TLSv1.3 TLS_AES_256_GCM_SHA384
  ssl: { ca: BEGONA CA } (verify-full, MITM)           → XATO: UNABLE_TO_VERIFY_LEAF_SIGNATURE unable to verify the first certificate; if the root CA is installed locally, try running Node.js with --use-sy
  ssl: true (tizim CA'lari — o'z-o'zini imzolagan rad) → XATO: UNABLE_TO_VERIFY_LEAF_SIGNATURE unable to verify the first certificate; if the root CA is installed locally, try running Node.js with --use-sy

Qatorma-qator:

  1. Shifrsiz — server o'zi rad etdi. 28000 — xato kodi (SQLSTATE — Postgres har xato turiga beradigan 5 belgili kod), ma'nosi "invalid authorization", ya'ni "ruxsat yo'q": pg_hba.conf da shifrsiz ulanish uchun qoida yo'q (no encryption). Bu — server tomondagi himoya.
  2. require — ulandi, TLSv1.3. Lekin shu ulanish ham hech qanday sertifikatni tekshirmadi.
  3. verify-full, to'g'ri CA — ulandi. Shifr ham, ishonch ham bor.
  4. verify-full, begona CA — bu MITM modeli: mijoz boshqa CA'ga ishonadi, server esa uning sertifikati bilan imzolanmagan. Rad etildi: "birinchi sertifikatni tekshirib bo'lmadi". Endi 2-qatorga qayting: require aynan shu vaziyatda ham ulanib ketardi.
  5. ssl: true — Node'ning tizim CA ro'yxati bilan tekshiradi. Bizning o'z-o'zimiz yasagan CA unda yo'q — rad.

3.3 Natijani halol o'qish

Ikki kichik narsa. Birinchi qatordagi 172.17.0.1 — Docker tarmog'ining ichki manzili: ulanish konteynerga Docker shlyuzi orqali kelgan. Oxirgi ikki qatordagi xabar --use-sy da kesilgan — bu Node xatosi emas, skript e.message.slice(0, 110) bilan 110 belgida qirqqan. To'liq xabar Node'ni tizim sertifikatlari bilan ishga tushirishni maslahat beradi.

Kanon TLS'ni faqat lokal sertifikat bilan sinadi. Managed xizmatlarda (Neon, Supabase, AWS RDS) provayder o'z CA faylini beradi — ca ga aynan uni qo'yasiz. Bu sinalmagan, lekin mexanizm o'sha.

Tekshirib ko'ring: Jamoadoshingiz "sertifikat xatosi chiqyapti" deb rejectUnauthorized: false qo'ydi. Bu nimani buzadi?

Javob

Shifrlash qoladi, lekin server tekshiruvi o'chadi — ulanish require darajasiga tushadi. Kanon tajribasidagi 4-holat (begona CA) endi xatosiz ulanadi: tarmoqdagi hujumchi o'zini baza qilib ko'rsatsa, ilova unga parol va hamma so'rovlarni beradi. To'g'ri yechim — provayderning CA faylini ca ga berish. Sertifikat xatosini o'chirish — xatoni yashirish, tuzatish emas.

vazifalar-api ning .env.example faylidagi izohda server uchun namuna shunday: postgres://user:<parol>@host:5432/baza?sslmode=verify-full.

4. Autentifikatsiya: parol qanday tekshiriladi

TLS kanalni himoyaladi. Endi server mijozga "sen kimsan?" deb so'raydi. pg_hba.conf dagi oxirgi ustun — usul: trust (parolsiz), md5, scram-sha-256, sertifikat va boshqalar.

SCRAM-SHA-256 — parol tarmoqda ham, serverda ham ochiq ko'rinishda bo'lmaydigan usul. Server parolning o'zini emas, undan hosil qilingan tuzli xeshni saqlaydi. Kirishda mijoz parolni bilishini isbotlaydi, lekin parolni yubormaydi. Parolni xavfsiz saqlash darsidagi g'oya — tuz va sekin xesh — bu yerda ham bor.

Yangi parol qaysi usulda saqlanishini password_encryption sozlamasi belgilaydi (biz ishga tushirdik, PostgreSQL 17.6):

sql
SHOW password_encryption;
text
 password_encryption
---------------------
 scram-sha-256
(1 row)

PostgreSQL 14 dan beri sukut — scram-sha-256. Eski md5 usuli zaif. PostgreSQL 18 hujjatida ogohlantirish bor: MD5 bilan saqlangan parollar eskirgan (deprecated) va kelajakdagi versiyada olib tashlanadi (hujjat, 2026-11 holatiga). Eski serverdan ko'chirilgan rollarda parol hali md5 ko'rinishida bo'lishi mumkin — ularning parolini qayta o'rnatish kerak.

Parolning o'zi kodda emas, sozlamada turadi: DATABASE_URL muhit o'zgaruvchisida yoki sir menejerida (Sirlarni boshqarish). Loyihaning sql/ fayllarida rollar parolsiz yaratilgan — lokal konteyner trust bilan ishlaydi, serverda esa parol .env dan beriladi. Har muhitning (lokal, test, prodakshn) o'z paroli bo'lsin: bitta parol hamma joyda — bitta oqish hammasini ochadi.

5. Huquq: eng kam huquqli rol

5.1 Ilova roli nima qila oladi

Eng kam huquq (least privilege) — har rol faqat o'z ishi uchun zarur huquqqa ega. Rollar va huquqlar darsida loyiha uchun rollar yaratdik, 28-qismda ularga RLS qo'shdik. Kanon vazifalar_api rolini haqiqiy bazada tekshirdi:

text
=== (3) least privilege: vazifalar_api (kanon bazasi)
superuser=false createdb=false createrole=false bypassrls=false conn_limit=40
  vazifalar_api> DROP TABLE vazifalar → ERROR:  must be owner of table vazifalar
  vazifalar_api> ALTER TABLE vazifalar DISABLE ROW LEVEL SECURITY → ERROR:  must be owner of table vazifalar
  vazifalar_api> SELECT count(*) FROM sessiyalar → ERROR:  permission denied for table sessiyalar
  vazifalar_api> SELECT count(*) FROM vazifalar → 0
  vazifalar_api> CREATE TABLE x (i int) → ERROR:  permission denied for schema public

Buni o'z bazangizda takrorlash mumkin — loyihaning sql/ fayllari bilan qurilgan bazada. SET ROLE — superuser sessiyasini vaqtincha boshqa rolga o'tkazadi (biz ishga tushirdik, PostgreSQL 17.6):

sql
SELECT rolsuper, rolcreatedb, rolcreaterole, rolbypassrls,
       rolconnlimit
  FROM pg_roles WHERE rolname = 'vazifalar_api';
text
 rolsuper | rolcreatedb | rolcreaterole | rolbypassrls | rolconnlimit
----------+-------------+---------------+--------------+--------------
 f        | f           | f             | f            |           40
(1 row)
sql
SET ROLE vazifalar_api;
DROP TABLE vazifalar;
ALTER TABLE vazifalar DISABLE ROW LEVEL SECURITY;
SELECT count(*) FROM sessiyalar;
SELECT count(*) FROM vazifalar;
CREATE TABLE x (i int);
text
SET
ERROR:  must be owner of table vazifalar
ERROR:  must be owner of table vazifalar
ERROR:  permission denied for table sessiyalar
 count
-------
     0
(1 row)

ERROR:  permission denied for schema public
LINE 1: CREATE TABLE x (i int);
                     ^

Har qator — alohida himoya:

  • must be owner — jadvalni o'chirish va RLS'ni o'chirish faqat egasiga ruxsat. Egasi — migratsiya roli. Ilova jarayoni buzilsa ham (SQL injection, kutubxonadagi zaiflik), hujumchi jadvalni o'chira olmaydi.
  • permission denied for table sessiyalar — sessiyalar auth roli (vazifalar_auth) ning ishi. Vazifalar API'si ularni umuman ko'rmaydi.
  • count = 0 — RLS: so'rovda foydalanuvchi konteksti yo'q, shuning uchun hech qator ko'rinmaydi. Bu yopiq holatda xavfsiz (fail closed): dasturchi kontekstni o'rnatishni unutsa, ilova bo'sh ro'yxat qaytaradi, begona ma'lumot emas.
  • conn_limit = 40 — bitta rol butun ulanishlar limitini egallab ololmaydi.

Maslahat: Ilova uchun ikkita ulanish satri bo'lsin: migratsiya uchun (vazifalar_migratsiya — jadval yaratadi) va ish uchun (vazifalar_api — faqat DML). Prodakshn serveridagi ilova jarayoni birinchisini umuman bilmasin.

Tekshirib ko'ring: RLS bilan count = 0 chiqdi. Nega bu "xato" emas, balki yaxshi belgi?

Javob

Chunki xato tomonga emas, xavfsiz tomonga yiqildi. Foydalanuvchi konteksti o'rnatilmagan — RLS "bu kimligini bilmayman" deb hech narsa ko'rsatmadi. Aksincha bo'lganda (kontekst yo'q — hammasi ko'rinadi) bitta unutilgan qator butun jadvalni ochib qo'yardi. Bo'sh ro'yxatni dasturchi testda darhol sezadi, begona ma'lumot oqishini esa sezmasligi mumkin.

5.2 Bizda ham shunday chiqdi

Kanon xatolari va bizning chiqishimiz bir xil: must be owner of table vazifalar va permission denied. Faqat CREATE TABLE xatosini psql to'liq ko'rsatadi — xato joyini belgilovchi LINE 1: qatori bilan. Kanon skripti har javobning faqat birinchi qatorini olgan.

6. Ma'lumot: ustunni shifrlash

6.1 Diskdagi shifrlash va ustun shifrlash

Ikki xil shifrlashni ajrating:

  • Diskdagi shifrlash (encryption at rest) — butun disk yoki fayl tizimi shifrlangan. Disk o'g'irlansa, ma'lumot o'qilmaydi. Lekin bazaga ulangan har kim ma'lumotni ochiq ko'radi. Bu odatda OS yoki managed xizmat darajasida yoqiladi (sinalmagan).
  • Ustun shifrlash — faqat eng nozik ustunlar (telefon, pasport raqami) shifrlanadi, kalit esa bazada emas, ilovada. Baza zaxirasi oqib ketsa ham, shu ustunlar o'qilmaydi.

6.2 pgcrypto

Postgres'ning pgcrypto kengaytmasi bazaning ichida shifrlash funksiyalarini beradi. pgp_sym_encrypt — simmetrik shifrlash (bitta kalit bilan shifrlanadi va ochiladi). Biz ishga tushirdik, PostgreSQL 17.6:

sql
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE mijoz (id int, telefon_shifr bytea);
INSERT INTO mijoz VALUES (1, pgp_sym_encrypt(
  '+998900000000', 'sinov-kaliti', 'cipher-algo=aes256'));

SELECT octet_length(telefon_shifr) AS bayt,
       pgp_sym_decrypt(telefon_shifr, 'sinov-kaliti') AS ochiq
  FROM mijoz;
text
CREATE EXTENSION
CREATE TABLE
INSERT 0 1
 bayt |     ochiq
------+---------------
   79 | +998900000000
(1 row)

13 belgilik raqam shifrlanib 79 baytga aylandi: shifrmatn ichida tuz, tekshiruv yig'indisi va sarlavha bor. Kanon tajribasida ham aynan 79 bayt chiqdi. Noto'g'ri kalit bilan:

sql
SELECT pgp_sym_decrypt(telefon_shifr, 'notogri') FROM mijoz;
text
ERROR:  Wrong key or corrupt data

"Kalit noto'g'ri yoki ma'lumot buzilgan" — Postgres qaysi biri ekanini aytmaydi. Bu ham himoya: hujumchiga ishora berilmaydi.

6.3 Shifrlangan ustun bo'yicha qidirish

Endi "shu telefon raqamli mijozni top" kerak. Muammo shundaki, bir xil matn ikki marta shifrlansa — ikki xil natija:

sql
SELECT pgp_sym_encrypt('+998900000000', 'k')
     = pgp_sym_encrypt('+998900000000', 'k') AS teng;
text
 teng
------
 f
(1 row)

Har shifrlashda tasodifiy tuz qo'shiladi. Bu yaxshi: bir xil telefonli ikki mijozni shifrmatnidan bilib bo'lmaydi. Lekin WHERE telefon_shifr = ... endi ishlamaydi, indeks ham foydasiz. Qolgan yo'l — har qatorni ochib solishtirish, ya'ni butun jadvalni o'qish.

Kanon chiqishida shunday qator bor:

text
  shifrlangan ustun bo'yicha qidiruv: Seq Scan on mijoz  (cost=0.00..29.05 rows=6 width=36) (indeks ishlatib bo'lmaydi — blind index/HMAC kerak)

Halol o'qish: bu EXPLAIN o'zi hech narsani isbotlamaydi — kanondagi vaqtinchalik jadvalda umuman indeks yo'q edi, Seq Scan baribir chiqardi. Haqiqiy dalil — yuqoridagi teng = f: shifrmatn tasodifiy, shuning uchun unga indeks qurish ma'nosiz.

6.4 Blind index

Blind index ("ko'r indeks") — ochiq qiymatning kalitli xeshi (HMAC) alohida ustunda saqlanadi. HMAC deterministik: bir xil telefon va bir xil kalit — doim bir xil natija. Kalitsiz esa xeshdan telefonni tiklab bo'lmaydi va hatto "+998..." ning hamma variantini sanab chiqib ham topib bo'lmaydi — kalit kerak. HMAC'ni crypto 1: tasodif, hash va HMAC darsidan bilasiz.

sql
ALTER TABLE mijoz ADD COLUMN telefon_hmac bytea;
UPDATE mijoz SET telefon_hmac =
  hmac('+998900000000', 'indeks-kaliti', 'sha256');
CREATE INDEX ON mijoz (telefon_hmac);

SELECT id FROM mijoz WHERE telefon_hmac =
  hmac('+998900000000', 'indeks-kaliti', 'sha256');
text
ALTER TABLE
UPDATE 1
CREATE INDEX
 id
----
  1
(1 row)

Topildi — shifrmatnni ochmasdan. Ikki qoida: indeks kaliti shifrlash kalitidan boshqa bo'lsin va xeshdan oldin qiymat bir xil shaklga keltirilsin (+998 90 000-00-00 va 998900000000 bir xil xesh bersin). Shaklga keltirishni mashqda qilasiz.

Xuddi shu HMAC'ni ilova ham hisoblay oladi. Brauzerda Web Crypto bilan — natija Postgres'nikiga teng bo'lishi kerak:

js
const kodla = (s) => new TextEncoder().encode(s);
const kalit = await crypto.subtle.importKey(
  "raw", kodla("indeks-kaliti"),
  { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
);
const imzo = await crypto.subtle.sign(
  "HMAC", kalit, kodla("+998900000000"));
const hex = [...new Uint8Array(imzo)]
  .map((b) => b.toString(16).padStart(2, "0")).join("");
console.log(hex);

Konsolda:

text
69a445bdfbcb759fae30bbd6d607b4fab54909f17695f43d955df3df415d3f9a

Postgres'da tekshirib ko'ramiz:

sql
SELECT encode(hmac('+998900000000', 'indeks-kaliti', 'sha256'),
              'hex') AS hex;
text
                               hex
------------------------------------------------------------------
 69a445bdfbcb759fae30bbd6d607b4fab54909f17695f43d955df3df415d3f9a
(1 row)

Bir xil. Demak indeks qiymatini ilova ham, baza ham hisoblay oladi.

Tekshirib ko'ring: Nega blind index uchun oddiy SHA-256 (kalitsiz) yetmaydi?

Javob

O'zbek mobil raqamlari soni cheklangan: +998 va 9 ta raqam — ko'pi bilan bir milliard variant. Kompyuter ularning hammasini kalitsiz xeshlab, tayyor "xesh → telefon" jadvalini tuzib oladi. Zaxira oqib ketsa, har xesh bir zumda telefonga aylanadi. HMAC'da esa kalit kerak — kalitsiz bunday jadvalni tuzib bo'lmaydi.

6.5 Kalit qayerda turadi

pgcrypto'ning zaif joyi — kalit SQL so'rov bilan birga serverga boradi. PostgreSQL hujjati ("Security Limitations" bo'limi) buni ochiq yozadi: pgcrypto funksiyalari server ichida ishlaydi, shuning uchun ma'lumot va kalit mijoz bilan server orasida ochiq ko'rinishda yuradi. Hujjat ikki shart qo'yadi: lokal yoki TLS bilan ulaning va server hamda baza administratoriga ishoning. Bo'lmasa — shifrlashni ilovada qiling.

Kalit so'rov matnining bir qismi ekanini o'zimiz ko'ramiz. pg_stat_activity — hozir bajarilayotgan so'rovlar ro'yxati, administrator uni har doim ko'radi. Biz ishga tushirdik, PostgreSQL 17.6:

sql
SELECT length(pgp_sym_encrypt('+998900000000', 'sinov-kaliti')) > 0
         AS shifrlandi,
       (SELECT query FROM pg_stat_activity
         WHERE pid = pg_backend_pid()) LIKE '%sinov-kaliti%'
         AS kalit_korinadi;
text
 shifrlandi | kalit_korinadi
------------+----------------
 t          | t
(1 row)

Nima bo'ldi? Shifrlash ishladi, lekin shu paytning o'zida so'rov matni — kaliti bilan birga — pg_stat_activity da turibdi. log_statement = 'all' yoqilgan serverda u log fayliga ham yoziladi. Shuning uchun ko'p loyihalarda shifrlash ilovaning o'zida qilinadi, bazaga faqat shifrmatn boradi. Loyihamiz ham shunday: 28-qismdagi TOTP siri ilovada AES-256-GCM bilan shifrlanadi, kalit — ilova sozlamasida (ILOVA_SIRI), bazada emas. pgcrypto kanonga kirmadi.

pgcrypto (bazada) Ilovada (AES-GCM)
Kalit qayerda so'rov bilan bazaga boradi faqat ilova xotirasida
SQL'dan qidirish blind index bilan blind index bilan
Qachon tez yechim, ichki tizim foydalanuvchi ma'lumoti, prodakshn

7. PII maskalash

PII (shaxsga doir ma'lumot) — inglizcha personally identifiable information, ya'ni odamni aniqlab beradigan ma'lumot: ism, email, telefon, manzil. Loyihada vazifalar_hisobot roli bor — analitika uchun, faqat o'qish. U hamma jadvalni o'qiy oladi, emaillarni ham. Analitikka emaillarning ochiq ko'rinishi kerakmi? Ko'pincha yo'q — unga "nechta foydalanuvchi" kerak, "kim" emas.

Yechim — maskalovchi ko'rinish (view) va asl jadvalga huquqni olib qo'yish. View'ni View va materialized view darsida ko'rgansiz: saqlangan SELECT, jadval kabi o'qiladi. Quyidagi blokda e'tibor bering: bitta rol view'dan o'qiydi, asl jadvaldan esa rad oladi:

sql
CREATE VIEW foydalanuvchilar_mask AS
SELECT id,
       regexp_replace(email, '^(.).*(@.*)$', '\1***\2') AS email,
       yaratilgan_vaqt
  FROM foydalanuvchilar;
REVOKE SELECT ON foydalanuvchilar FROM vazifalar_hisobot;
GRANT SELECT ON foydalanuvchilar_mask TO vazifalar_hisobot;

SET ROLE vazifalar_hisobot;
SELECT id, email FROM foydalanuvchilar_mask ORDER BY id LIMIT 3;
SELECT email FROM foydalanuvchilar LIMIT 1;
text
CREATE VIEW
REVOKE
GRANT
SET
 id |      email
----+------------------
  1 | a***@example.com
  2 | d***@example.com
  3 | s***@example.com
(3 rows)

ERROR:  permission denied for table foydalanuvchilar

Analitik endi a***@example.com ni ko'radi — foydalanuvchilarni sanash uchun yetarli. Asl jadval yopiq. View o'z egasining huquqi bilan ishlaydi: vazifalar_hisobot jadvalni bevosita o'qiy olmaydi, lekin view orqali — faqat maskalangan ustunlarni ko'radi. Bu namuna kanonda yo'q — biz uni darsda qo'shdik. Hisobot roli BYPASSRLS bilan yaratilgan (27-qism qarori), shuning uchun unga aynan shunday cheklov kerak.

Tekshirib ko'ring: Analitik a***@example.com dan asl emailni tiklay oladimi? Nima uchun?

Javob

Yo'q. View faqat birinchi harf va domenni qoldiradi, qolgani *** bilan almashadi — bu ma'lumot view ichida yo'qoladi. Asl jadvalga esa huquq olib qo'yilgan. Lekin ehtiyot bo'ling: kichik jamoada "a" bilan boshlanadigan bitta odam bo'lsa, uni taxmin qilish mumkin. Maskalash anonimlashtirish emas — buni Ma'lumot saqlash muddati, arxivlash va anonimlashtirish darsida batafsil ko'ramiz.

8. Kuzatuv va audit

Oxirgi qatlam — kim nima qilganini bilish. Postgres'da:

  • log_connections, log_disconnections — har ulanish logga.
  • log_statement = 'ddl' — har CREATE, ALTER, DROP logga.
  • pgaudit — kengaytma: kim qaysi jadvalni o'qidi yoki o'zgartirdi, batafsil audit logi. Kanon image'ida yo'q — sinalmagan.

Ilova darajasidagi audit jurnali — "admin foydalanuvchini blokladi", "API kalit yaratildi" — alohida mavzu: Audit log darsida loyihaga hash zanjirli jurnal qo'shamiz.

9. Hujumchi nigohi

Keling, hujumchi o'rnida turib bitta voqeani boshidan oxirigacha kuzatamiz. U vazifalar-api dagi bitta endpoint'da SQL injection topdi, deylik.

  1. Birinchi urinish: DROP TABLE vazifalar. Ilova roli jadval egasi emas — must be owner. Qatlam ushlab qoldi.
  2. Ikkinchi urinish: boshqa foydalanuvchilarning vazifalarini o'qish. RLS faqat joriy foydalanuvchi qatorlarini ko'rsatadi — begona vazifa ko'rinmaydi.
  3. Uchinchi urinish: sessiyalar jadvalidan tokenlarni o'g'irlash. Bu jadval auth rolining ishi — permission denied.
  4. To'rtinchi urinish: zaxira faylini topish. Topdi ham, deylik. Telefon ustuni shifrlangan, kalit esa zaxirada yo'q — u ilova sozlamasida.

Har urinishda hujumchi boshqa devorga uriladi. Bitta qatlam (injection'dan himoya) yiqildi, lekin zarar kichik qoldi — chuqur himoya aynan shu. Endi teskarisini tasavvur qiling: ilova postgres superuser bilan ulangan. Birinchi urinishning o'zi oxirgi bo'lardi.

Yana bir yo'l — tarmoq. Hujumchi ilova bilan baza o'rtasiga tushib oldi (masalan, bir xil bulut tarmog'ida). sslmode=require bo'lsa, ilova unga parolni o'zi beradi. verify-full bo'lsa — ulanish rad etiladi va log'da xato paydo bo'ladi.

10. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Bazaga internetdan ulanish mumkin listen_addresses = '*' + ochiq port yopiq tarmoq, firewall, 127.0.0.1:
rejectUnauthorized: false kodda sertifikat xatosi "tuzatilgan" provayder CA faylini ca ga berish
Ilova postgres bilan ulangan qulaylik alohida DML roli, migratsiya roli alohida
permission denied dan keyin GRANT ALL tez yechim aniq bitta huquq berish
Shifrlangan ustun bo'yicha qidiruv sekin shifrmatn tasodifiy blind index (HMAC)
Shifrlash kaliti bazadagi jadvalda "qulay" kalit — ilova sozlamasida yoki sir menejerida

11. Mashqlar

Mashqlar kurs/mashqlar/31/07-xavfsizlik/ da. 2- va 3-mashq yechimlari Node 24.21.0 da tekshirilgan (biz ishga tushirdik).

1-mashq (oson): bo'shliqlarni to'ldiring

  • Shifrlaydi, lekin server sertifikatini tekshirmaydigan daraja — sslmode=
  • Shifrlaydi va CA + host nomini tekshiradi — sslmode=
  • Ilova roli DROP TABLE qilganda olgan xato — must be of table
  • Shifrlangan ustun bo'yicha qidirish uchun kalitli xesh — index
  • Kanonda telefon raqamining pgcrypto shifrmatni uzunligi — bayt
Yechim
  • require — MITM'dan himoya yo'q.
  • verify-full — prodakshn uchun to'g'ri tanlov.
  • owner — jadval egasi migratsiya roli.
  • blind — HMAC, deterministik.
  • 79 — biz ham 79 olgan edik.

2-mashq (o'rta): ulanish satrini tekshirish

ulanishniTekshir(satr) funksiyasini yozing. U postgres:// ulanish satrini oladi va ogohlantirishlar massivini qaytaradi: sslmode yo'q yoki verify-full emas; foydalanuvchi postgres (superuser); host localhost/127.0.0.1 emas, lekin port 5432 (ochiq standart port — eslatma sifatida). Ishora: new URL(satr), url.searchParams.get(...), url.username (URL va URLSearchParams).

Yechim
js
const LOKAL = ["localhost", "127.0.0.1"];

function ulanishniTekshir(satr) {
  const url = new URL(satr);
  const ogoh = [];
  const ssl = url.searchParams.get("sslmode");
  if (ssl !== "verify-full") {
    ogoh.push(`sslmode=${ssl ?? "yo'q"} — verify-full kerak`);
  }
  if (url.username === "postgres") {
    ogoh.push("superuser bilan ulanish");
  }
  const lokal = LOKAL.includes(url.hostname);
  if (!lokal && (url.port === "" || url.port === "5432")) {
    ogoh.push("standart port 5432 — tarmoqdan yopiqmi?");
  }
  return ogoh;
}

console.log(ulanishniTekshir(
  "postgres://postgres:x@db.example.com/vazifalar"));
console.log(ulanishniTekshir(
  "postgres://vazifalar_api:x@db.example.com:6432/vazifalar" +
    "?sslmode=require"));
console.log(ulanishniTekshir(
  "postgres://vazifalar_api:x@db.example.com:6432/vazifalar" +
    "?sslmode=verify-full"));

Konsolda:

text
[
  "sslmode=yo'q — verify-full kerak",
  'superuser bilan ulanish',
  'standart port 5432 — tarmoqdan yopiqmi?'
]
[ 'sslmode=require — verify-full kerak' ]
[]

Birinchi satrda uchta xato birdan: TLS yo'q, superuser, standart port. URL'da port yozilmasa url.port bo'sh satr qaytaradi — shuning uchun "" ni ham tekshirdik. Uchinchi satr toza.

3-mashq (qiyin): blind index uchun telefonni shaklga keltirish

telefonIndeksi(telefon, kalit) — telefon raqamini bir xil shaklga keltiradi (faqat raqamlar; boshida 998 yo'q bo'lsa — qo'shadi; natija +998XXXXXXXXX), keyin HMAC-SHA256 ni hex ko'rinishida qaytaradi. Uch xil yozilgan bitta raqam bir xil indeks bersin, boshqa raqam — boshqa. Ishora: replace(/\D/g, "") — raqam bo'lmagan hamma belgini olib tashlaydi (RegExp); crypto.subtle.sign("HMAC", ...) — darsdagi blok.

Yechim
js
const kodla = (s) => new TextEncoder().encode(s);

function telefonShakli(telefon) {
  const raqam = telefon.replace(/\D/g, "");
  const toliq = raqam.startsWith("998") ? raqam : "998" + raqam;
  return "+" + toliq;
}

async function telefonIndeksi(telefon, kalit) {
  const k = await crypto.subtle.importKey(
    "raw", kodla(kalit),
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
  );
  const imzo = await crypto.subtle.sign(
    "HMAC", k, kodla(telefonShakli(telefon)));
  return [...new Uint8Array(imzo)]
    .map((b) => b.toString(16).padStart(2, "0")).join("");
}

const variantlar = [
  "+998 90 000-00-00", "998900000000", "(90) 000 00 00",
];
for (const t of variantlar) {
  const h = await telefonIndeksi(t, "indeks-kaliti");
  console.log(telefonShakli(t), h.slice(0, 16));
}
const boshqa = await telefonIndeksi(
  "+998 91 000 00 00", "indeks-kaliti");
console.log("boshqa raqam:", boshqa.slice(0, 16));

Konsolda:

text
+998900000000 69a445bdfbcb759f
+998900000000 69a445bdfbcb759f
+998900000000 69a445bdfbcb759f
boshqa raqam: 9cdd19af93b251fb

Uch xil yozuv — bitta shakl va bitta indeks. U darsdagi Postgres hmac() natijasi bilan ham bir xil boshlanadi. Chiqishda xeshning faqat dastlabki 16 belgisini ko'rsatdik — o'qish oson bo'lishi uchun. Bitta raqam farq qilsa, xesh butunlay boshqacha.

12. Loyiha qadami: kanon tajribasi

Bu darsda vazifalar-api o'zgarmaydi va commit yo'q (Partitioning darsidagi o'zgarishlar hali commit qilinmagan — ular Time-series va analitik DB'lar darsida commit qilinadi). Loyihada himoya qatlamlarining ko'pi allaqachon bor: rollar va RLS (27–28-qismlar), ilova tomonida shifrlangan TOTP siri (28-qism), .env.example dagi sslmode=verify-full namunasi, faqat 127.0.0.1 dagi portlar. Yetishmaydigan qismlar — TLS sertifikatlari va tarmoq qoidalari — infratuzilma ishi, ular 33-qismda (deploy) serverda sozlanadi. pgcrypto va pgaudit kanonga kirmadi: birinchisi kalitni bazaga yuboradi, ikkinchisi image'da yo'q.

Kanon tajribasi: o'z CA va server sertifikati bilan alohida TLS konteyner, besh xil ulanish, pgcrypto bilan ustun shifrlash, kanon bazasida vazifalar_api huquqlari, portlar ro'yxati. Oxirida TLS konteyneri va sertifikatlar o'chirildi.

O'zingiz takrorlaydigan qism Docker'siz: loyihaning sql/ fayllari bilan qurilgan lokal bazangizda «Ilova roli nima qila oladi» bo'limidagi SET ROLE vazifalar_api blokini bajaring. Keyin SELECT set_config('ilova.foydalanuvchi_id', '1', false); ni qo'shib, SELECT count(*) FROM vazifalar qayta bajaring va nima o'zgarganini tushuntiring. Maskalash view'ini ham shu bazada sinab ko'ring.

13. Real ishda

  • Qayerda uchraydi: har prodakshn bazasida. Managed xizmatlar (AWS RDS, Google Cloud SQL, Neon) TLS va diskdagi shifrlashni sukut bo'yicha yoqadi, lekin sslmode=verify-full va rollar — sizning ishingiz.
  • Qonunchilik: shaxsiy ma'lumotlarni himoyalash talablari keyingi darslarda — Shaxsiy ma'lumotlar qonunchiligi: O'zbekiston va GDPR.
  • Intervyu savollari: "sslmode=require va verify-full farqi?", "Ilova bazaga qaysi rol bilan ulanishi kerak?", "Shifrlangan ustun bo'yicha qanday qidirasiz?", "Encryption at rest nimadan himoyalaydi, nimadan yo'q?"
  • Kod review'da: "rejectUnauthorized: false — nega?", "bu yangi jadvalga hisobot roli kerakmi, PII bormi?", "migratsiya va ilova bitta rol bilan ishlayapti".

Xulosa

  • Bazani qatlamlar himoyalaydi: tarmoq, TLS, autentifikatsiya, huquq, shifrlash, audit.
  • sslmode=require — shifr bor, ishonch yo'q; verify-full — ikkalasi ham. Kanonda begona CA faqat verify-full da rad etildi.
  • Ilova — superuser emas: kanonda ham, bizda ham ilova roli DROP TABLE va RLS'ni o'chirishni qila olmadi.
  • pgcrypto shifrmatni tasodifiy — qidirish uchun blind index (HMAC) kerak; kalit bazada saqlanmaydi.
  • Analitik rolga PII o'rniga maskalangan view beriladi.

Keyingi dars: Vektor bazalar va pgvector — matnning ma'nosini sonlar massivi sifatida saqlaymiz va "ma'nosi o'xshash" yozuvlarni qidiramiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Ma'lumotlar bazasi xavfsizligi: TLS, eng kam huquq va ustun shifrlash — IlmHamroh