Mundarija (29)
- Bu darsda
- 1. Nega bu kerak?
- 2. Tarmoq: portni ochmaslik
- 3. Transport: TLS va sslmode
- 3.1 sslmode darajalari
- 3.2 Kanon tajribasi
- 3.3 Natijani halol o'qish
- 4. Autentifikatsiya: parol qanday tekshiriladi
- 5. Huquq: eng kam huquqli rol
- 5.1 Ilova roli nima qila oladi
- 5.2 Bizda ham shunday chiqdi
- 6. Ma'lumot: ustunni shifrlash
- 6.1 Diskdagi shifrlash va ustun shifrlash
- 6.2 pgcrypto
- 6.3 Shifrlangan ustun bo'yicha qidirish
- 6.4 Blind index
- 6.5 Kalit qayerda turadi
- 7. PII maskalash
- 8. Kuzatuv va audit
- 9. Hujumchi nigohi
- 10. Ko'p uchraydigan xatolar
- 11. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): ulanish satrini tekshirish
- 3-mashq (qiyin): blind index uchun telefonni shaklga keltirish
- 12. Loyiha qadami: kanon tajribasi
- 13. Real ishda
- Xulosa
- Manbalar
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 tajribasidasslmode=requiredarajasidagi ulanish begona sertifikat bilan ham ulanib ketdi — shifrlangan, lekin kim bilan gaplashayotganini tekshirmagan. Ilova roli esaDROP TABLEham, RLS'ni o'chirish ham qila olmadi.
Bu darsda
- Bazani himoyalash qatlamlarini chizib tushuntirasiz.
sslmodedarajalarini varequirebilanverify-fullfarqini 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,
postgresparolini esa taxmin qilish oson. - Ilova bazaga shifrsiz ulanadi — bir tarmoqdagi har kim so'rovlarni o'qiydi.
- Ilova
postgressuperuser 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):
SHOW listen_addresses;
SELECT type, address, auth_method
FROM pg_hba_file_rules WHERE database::text = '{all}'; 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).
=== (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/tcpHar 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):
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:
=== (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-syQatorma-qator:
- 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.confda shifrsiz ulanish uchun qoida yo'q (no encryption). Bu — server tomondagi himoya. require— ulandi, TLSv1.3. Lekin shu ulanish ham hech qanday sertifikatni tekshirmadi.verify-full, to'g'ri CA — ulandi. Shifr ham, ishonch ham bor.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:requireaynan shu vaziyatda ham ulanib ketardi.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: falseqo'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):
SHOW password_encryption; 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:
=== (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 publicBuni 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):
SELECT rolsuper, rolcreatedb, rolcreaterole, rolbypassrls,
rolconnlimit
FROM pg_roles WHERE rolname = 'vazifalar_api'; rolsuper | rolcreatedb | rolcreaterole | rolbypassrls | rolconnlimit
----------+-------------+---------------+--------------+--------------
f | f | f | f | 40
(1 row)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);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 = 0chiqdi. 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:
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;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:
SELECT pgp_sym_decrypt(telefon_shifr, 'notogri') FROM mijoz;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:
SELECT pgp_sym_encrypt('+998900000000', 'k')
= pgp_sym_encrypt('+998900000000', 'k') AS teng; 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:
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.
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');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:
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:
69a445bdfbcb759fae30bbd6d607b4fab54909f17695f43d955df3df415d3f9aPostgres'da tekshirib ko'ramiz:
SELECT encode(hmac('+998900000000', 'indeks-kaliti', 'sha256'),
'hex') AS hex; 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:
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; 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:
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;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 foydalanuvchilarAnalitik 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.comdan 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'— harCREATE,ALTER,DROPlogga.- 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.
- Birinchi urinish:
DROP TABLE vazifalar. Ilova roli jadval egasi emas —must be owner. Qatlam ushlab qoldi. - Ikkinchi urinish: boshqa foydalanuvchilarning vazifalarini o'qish. RLS faqat joriy foydalanuvchi qatorlarini ko'rsatadi — begona vazifa ko'rinmaydi.
- Uchinchi urinish:
sessiyalarjadvalidan tokenlarni o'g'irlash. Bu jadval auth rolining ishi —permission denied. - 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 TABLEqilganda 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
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:
[
"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
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:
+998900000000 69a445bdfbcb759f
+998900000000 69a445bdfbcb759f
+998900000000 69a445bdfbcb759f
boshqa raqam: 9cdd19af93b251fbUch 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-fullva rollar — sizning ishingiz. - Qonunchilik: shaxsiy ma'lumotlarni himoyalash talablari keyingi darslarda — Shaxsiy ma'lumotlar qonunchiligi: O'zbekiston va GDPR.
- Intervyu savollari: "
sslmode=requirevaverify-fullfarqi?", "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 faqatverify-fullda rad etildi.- Ilova — superuser emas: kanonda ham, bizda ham ilova roli
DROP TABLEva 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
- PostgreSQL 18 hujjati — SSL Support (libpq, sslmode jadvali): https://www.postgresql.org/docs/18/libpq-ssl.html
- PostgreSQL 18 hujjati — Secure TCP/IP Connections with SSL: https://www.postgresql.org/docs/18/ssl-tcp.html
- PostgreSQL 18 hujjati — Password Authentication (md5 eskirgani): https://www.postgresql.org/docs/18/auth-password.html
- PostgreSQL 18 hujjati — The pg_hba.conf File: https://www.postgresql.org/docs/18/auth-pg-hba-conf.html
- PostgreSQL 18 hujjati — pgcrypto (Security Limitations bo'limi): https://www.postgresql.org/docs/18/pgcrypto.html
- node-postgres hujjati — SSL: https://node-postgres.com/features/ssl
- pgAudit (PostgreSQL 14+ uchun audit kengaytmasi): https://github.com/pgaudit/pgaudit
- Kanon o'lchovi (IlmHamroh, 2026-11-03): PostgreSQL 18.6 (
pgvector/pgvector:pg18), TLSv1.3, pg 8.23.1, Node 24.21.0. SQL bloklari, HMAC modeli va mashq yechimlari — biz ishga tushirdik (2026-11-03, PostgreSQL 17.6, Node 24.21.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!