Security & Crypto · امنیت و رمزنگاری سنیورSenior ~58 دقیقه مطالعه~50 min read
مدیریت کلید، رازها و حفاظت از دادهKey Management, Secrets & Data Protection
راهنمای عملی مدیریت کلید و راز برای backend: از AEAD و envelope encryption با مدل DEK/KEK و KMS/Vault و HSM تا Kubernetes Secrets و Spring Cloud Vault، رمزنگاری داده، tokenization و الزامات PCI-DSS، crypto-shredding و آمادگی post-quantum.A practical guide to keys and secrets in a backend: AEAD, envelope encryption with DEK/KEK, cloud KMS, Vault and HSMs, Kubernetes Secrets and Spring Cloud Vault, data encryption, tokenization and PCI-DSS, crypto-shredding and post-quantum readiness.
پیشنیاز:Prerequisites: رمزنگاری از پایه: AES، RSA، هش و امضای دیجیتالCryptography Foundations: AES, RSA, Hashing & Digital SignaturesPKI، TLS، گواهیها و GPGPKI, TLS, Certificates & GPG
تقریباً هر مهندس backend بلد است AES را صدا بزند. تقریباً هیچکس بلد نیست بگوید آن کلید از کجا آمد، چه کسی میتواند بخواندش، چطور عوضش میکنیم، و اگر فردا لو رفت چه میشود. این فاصله — بین «رمزنگاری» و «مدیریت کلید» — همان جایی است که شرکتها واقعاً هک میشوند.
رمزنگاری یک مسئلهٔ حلشده است: کسی امروز AES-256 را نمیشکند. چیزی که میشکند اینهاست: کلید در application.yml کامیت شده؛ سه سال است عوض نشده؛ همهٔ سرویسها یک کلید مشترک دارند؛ Secret در Kubernetes فقط base64 است؛ و وقتی یک نفر از تیم میرود هیچکس نمیداند به چه رازهایی دسترسی داشت.
احراز هویت و مجوزدهی (OAuth2/JWT/RBAC) در فصلهای spring-security و ms-security آمده و اینجا فقط ارجاعشان میدهیم؛ تمرکز ما روی کلید، راز و خودِ داده است.
۱. پایهها: entropy، symmetric/asymmetric، و چرا AEAD پیشفرض توست. ۲. چرخهٔ حیات کلید: تولید تا نابودی، و مفهوم cryptoperiod. ۳. Envelope Encryption: مدل DEK/KEK و چرا بدون آن چرخش کلید غیرممکن است. ۴. KMS: cloud KMS، encryption context، و Vault — transit، dynamic secrets، lease، seal. ۵. HSM: FIPS 140-2/3، PIN block، key ceremony، و چرا پرداخت بدون آن مجوز ندارد. ۶. رازها در عمل: git، Kubernetes Secrets، Sealed Secrets، ESO، secret scanning، و Spring Boot (Jasypt، Config Server، Vault). ۷. رمزنگاری داده: at rest در برابر in transit، و مسئلهٔ جستوجوپذیری. ۸. PII/PAN: tokenization در برابر encryption و masking؛ PCI-DSS و کاهش scope. ۹. بلوغ: کمینهسازی داده، crypto-shredding، ممیزی، crypto-agility و post-quantum.
۱. پایهها: کلید واقعاً چیست؟
یک قفل خوب را میشود خرید و نقشهاش را هم منتشر کرد؛ امنیتش به مخفی بودن مکانیزم نیست، به این است که فقط تو کلید داری (اصل Kerckhoffs: الگوریتم عمومی، کلید خصوصی). حالا تصور کن ساختمانی با ۵۰۰ در داری: مشکلت دیگر ساختن قفل نیست، بلکه این است که کلیدها را کجا نگه داری، به چه کسی بدهی، و اگر یکی گم شد چند در باید عوض شوند. این فصل دربارهٔ قفل نیست، دربارهٔ دفترچهٔ کلیدهاست.
کلید فقط یک رشتهٔ بیت است؛ چیزی که آن را کلید میکند غیرقابلحدس بودن است. معیارش entropy است: تعداد بیتهای واقعاً تصادفی درونش. کلیدی که از SHA-256("password123") ساخته شده ۲۵۶ بیت طول دارد ولی شاید ۳۰ بیت entropy — در چند ثانیه میشکند. پس قانون اول: کلید را از یک CSPRNG بگیر (مولدی که خروجیاش از روی خروجیهای قبلی قابل پیشبینی نیست)، نه از یک رشتهٔ انسانی.
KeyGenerator kg = KeyGenerator.getInstance("AES");
kg.init(256, SecureRandom.getInstanceStrong());
SecretKey dek = kg.generateKey(); // کلید ۲۵۶ بیتی AES
byte[] nonce = new byte[12];
new SecureRandom().nextBytes(nonce); // nonce ۹۶ بیتی برای GCM
java.util.Random یک LCG با seed ۴۸ بیتی است؛ با دو خروجی متوالی کل دنبالهٔ آینده بازسازی میشود. و یک نکتهٔ عملیاتی: SecureRandom.getInstanceStrong() روی لینوکس ممکن است در کانتینر تازهبالاآمده (entropy pool خالی) بلاک شود و startup را قفل کند. برای مصرف عادی new SecureRandom() کافی و امن است؛ اگر startup کند شد، -Djava.security.egd=file:/dev/./urandom رفعش میکند.
symmetric یک کلید برای رمز و بازکردن دارد و سریع است (AES، ChaCha20) ولی مسئلهٔ توزیع دارد. asymmetric جفتکلید عمومی/خصوصی دارد، توزیع را حل میکند ولی هزاران برابر کندتر است (RSA، Ed25519، X25519). قاعدهٔ جهان واقعی: asymmetric برای تبادل کلید و امضا، symmetric برای خود داده — دقیقاً کاری که TLS و envelope encryption میکنند.
AEAD: چرا فقط «رمز کردن» کافی نیست
اگر داده را با AES-CBC رمز کنی، مهاجم نمیتواند بخواندش ولی میتواند تغییرش دهد: تغییر یک بایت در ciphertext یک بایت مشخص از plaintext را تغییر میدهد. اگر آن بایت amount باشد، شب خوبی نداری.
AEAD (Authenticated Encryption with Associated Data) هم محرمانگی میدهد هم یکپارچگی: خروجی یک authentication tag دارد و دستکاری یک بیتی باعث استثنا هنگام decrypt میشود. بخش AAD دادهای است که رمز نمیشود ولی احراز میشود — مثلاً tenant_id — و جلوی «جابهجایی» ciphertext بین ردیفها را میگیرد.
public final class Aead {
private static final int IV_LEN = 12; // 96-bit، مقدار توصیهشدهٔ NIST برای GCM
private static final int TAG_BITS = 128;
public static byte[] encrypt(byte[] key, byte[] plaintext, byte[] aad) throws Exception {
byte[] iv = new byte[IV_LEN];
new SecureRandom().nextBytes(iv);
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"),
new GCMParameterSpec(TAG_BITS, iv));
if (aad != null) c.updateAAD(aad); // احراز میشود، رمز نمیشود
byte[] ct = c.doFinal(plaintext);
return ByteBuffer.allocate(IV_LEN + ct.length).put(iv).put(ct).array();
}
// decrypt قرینهٔ همین است با DECRYPT_MODE و همان IV/AAD؛
// اگر ciphertext یا AAD دستکاری شده باشد، doFinal یک AEADBadTagException میدهد.
}
اگر همان کلید با همان IV دو بار استفاده شود، مهاجم با XOR دو ciphertext رابطهٔ دو plaintext را درمیآورد و — بدتر — کلید احراز داخلی را بازسازی میکند و از آن به بعد میتواند پیام جعلی با تگ معتبر بسازد. NIST SP 800-38D میگوید با IV تصادفی ۹۶ بیتی حداکثر ۲³² پیام با یک کلید رمز کن. اگر «برای تکرارپذیری» IV را ثابت کردی یا از counter ای استفاده کردی که بعد از ریاستارت صفر میشود، روز اول شکستهای.
چون CBC فقط محرمانگی میدهد نه یکپارچگی. مهاجمی که به ciphertext دسترسی دارد (backup دیتابیس یا یک ستون قابل نوشتن) میتواند بیتها را طوری تغییر دهد که plaintext رمزگشاییشده بهشکل کنترلشده عوض شود و اپلیکیشن هیچ خطایی نبیند. ضمناً CBC با padding مستعد padding oracle است: اگر سیستم بین «padding خراب» و «داده خراب» تفاوت قابل مشاهدهای نشان دهد، مهاجم بایتبهبایت plaintext را بازیابی میکند.
پاسخ درست: AEAD — AES-GCM یا ChaCha20-Poly1305 — یا اگر مجبور به CBC هستی، Encrypt-then-MAC با HMAC روی (IV || ciphertext) و مقایسهٔ constant-time. در سطح سنیور اضافه میکنم که بهتر است این تصمیم اصلاً به دست توسعهدهنده نیفتد و از لایهای بالاتر مثل Tink یا AWS Encryption SDK استفاده شود.
۲. چرخهٔ حیات کلید — قلب موضوع
بیشتر تیمها فقط دو مرحله را پیاده میکنند: «کلید بساز» و «کلید را استفاده کن»؛ بقیهاش وقتی لازم میشود که دیر است.
چرخهٔ حیات یک کلید رمزنگاری، از تولید تا نابودی. — The lifecycle states a cryptographic key moves through, from generation to destruction.
stateDiagram-v2
[*] --> PreActivation: generate (CSPRNG / HSM)
PreActivation --> Active: activate
Active --> Active: use (encrypt + decrypt)
Active --> Deactivated: cryptoperiod ends / rotate
Deactivated --> Deactivated: decrypt-only (legacy data)
Active --> Compromised: incident detected
Deactivated --> Compromised: incident detected
Compromised --> Destroyed: emergency re-key
Deactivated --> Destroyed: all data re-wrapped
Destroyed --> [*]
۱. Generation. با CSPRNG یا داخل HSM. کلیدی که داخل HSM تولید شود و هرگز plaintext بیرون نیاید «non-exportable» است — بالاترین سطح تضمین.
۲. Storage. کلید هرگز کنار دادهٔ رمزشده نمینشیند. DEK رمزشده در همان ردیف اشکالی ندارد (چون با KEK محافظت شده)، ولی KEK باید در KMS/HSM باشد. کلید در همان جدولِ داده = رمزنگاری صفر.
۳. Distribution. بهترین حالت این است که کلید اصلاً به سرویس نرسد: سرویس فقط یک workload identity دارد و از KMS میخواهد برایش رمزگشایی کند.
۴. Rotation. ساختن نسخهٔ جدید و رمز کردن با آن، در حالی که نسخههای قدیمی هنوز برای رمزگشایی زندهاند. چرخش بدون versioning یعنی downtime. بازهٔ مجاز استفاده از یک کلید را cryptoperiod میگویند (NIST SP 800-57): معمولاً حداکثر یک تا دو سال برای کلید دادهٔ سکون، یا زودتر اگر حجم رمزشده زیاد باشد.
۵. Revocation. کلید دیگر برای رمز کردن استفاده نمیشود ولی برای رمزگشایی مجاز است — یا در حالت compromise هیچ استفادهای مجاز نیست و باید فوراً re-key شود.
۶. Destruction. حذف امن مادهٔ کلید؛ مرحلهای که تیمها هرگز انجام نمیدهند و ده سال بعد یک backup قدیمی با کلید فعال پیدا میشود. نابودی عامدانه یک ابزار قدرتمند هم هست: crypto-shredding (بخش ۹).
خیلیها فکر میکنند rotation یعنی همهٔ دادهٔ قدیمی را با کلید جدید دوباره رمز کن. در معماری درست، چرخش KEK چند ثانیه طول میکشد چون فقط DEKهای رمزشده rewrap میشوند، نه ترابایتها داده.
سؤال درستِ جلسهٔ طراحی: «اگر همین حالا بگویند این کلید لو رفته، چند دقیقه طول میکشد تا روی کلید جدید باشیم؟» و بدان که rotation واقعی سه بخش دارد: نسخهٔ جدید ساخته شد، همهٔ نوشتنها با آن انجام میشود، و یک job دادهٔ قدیمی را rewrap میکند تا min_decryption_version بالا برود. تا مرحلهٔ سوم تمام نشود، کلید قدیمی یک بدهی امنیتی است.
«هر ۹۰ روز» یک پاسخ حفظی است. پاسخ سنیور: بازهٔ چرخش تابع cryptoperiod است و cryptoperiod به سه چیز بستگی دارد — حجم دادهٔ رمزشده با آن کلید (محدودیتهای ریاضی مثل سقف ۲³² پیام برای GCM با IV تصادفی)، میزان در معرض بودن کلید (داخل HSM در برابر پخششده در حافظهٔ ۲۰۰ پاد)، و الزام قانونی یا قراردادی.
در عمل: KEK در KMS سالانه یا با بازهٔ سفارشی (AWS KMS از ۹۰ تا ۲۵۶۰ روز) بهعلاوهٔ چرخش on-demand هنگام حادثه؛ DEK عمر کوتاه دارد و عملاً چرخش نمیخواهد؛ اعتبارنامهٔ دیتابیس با dynamic secrets یکساعته؛ کلید امضای توکن روزانه تا هفتگی با overlap در JWKS. و مهمترین جمله: «توانایی چرخش را باید قبل از نیاز به آن ثابت کرده باشم — چرخش را دورهای تمرین میکنیم، نه فقط بعد از حادثه.»
۳. Envelope Encryption — مدل DEK/KEK
اگر فقط یک چیز از این فصل یاد بگیری، همین باشد.
نامهٔ محرمانه را در پاکتی میگذاری و قفلش میکنی. کلید آن پاکت را در پاکت کوچکتری میگذاری و آن را با کلید گاوصندوق بانک مهر میکنی، و هر دو را کنار هم میفرستی. برای باز کردن باید پاکت کوچک را به بانک بدهی و بگویی «بازش کن» — بانک کلید خودش را هرگز به کسی نمیدهد، فقط سرویس باز کردن میدهد.
نامه = دادهٔ تو · کلید پاکت = DEK · کلید گاوصندوق = KEK درون KMS/HSM · «بازش کن» = فراخوانی Decrypt.
سه دلیل عملی: (۱) اندازه و کارایی — KMSهای ابری فقط دادهٔ کوچک را مستقیم رمز میکنند (AWS KMS حداکثر ۴ کیلوبایت)؛ با envelope فقط ۳۲ بایت روی شبکه میرود. (۲) چرخش ارزان — برای چرخش KEK فقط DEKها rewrap میشوند. (۳) جداسازی اختیار — سرویس هرگز KEK را نمیبیند و هر فراخوانی در audit log مینشیند.
جریان کامل envelope encryption برای یک نوشتن و یک خواندن. — The full envelope-encryption flow for one write and one read.
sequenceDiagram
participant App as Application
participant KMS as KMS / Vault (KEK)
participant DB as Database
Note over App,DB: WRITE
App->>KMS: GenerateDataKey(keyId, AES_256, encryptionContext)
KMS-->>App: plaintextDEK + encryptedDEK
App->>App: AES-GCM encrypt(payload, plaintextDEK, aad)
App->>App: zeroise plaintextDEK in memory
App->>DB: store {ciphertext, iv, encryptedDEK, keyVersion}
Note over App,DB: READ
DB-->>App: {ciphertext, iv, encryptedDEK, keyVersion}
App->>KMS: Decrypt(encryptedDEK, encryptionContext)
KMS-->>App: plaintextDEK
App->>App: AES-GCM decrypt -> payload
# ۱) یک KEK بساز
aws kms create-key --description "payments-dek-wrapper" \
--key-usage ENCRYPT_DECRYPT --key-spec SYMMETRIC_DEFAULT
aws kms create-alias --alias-name alias/payments-kek --target-key-id <key-id>
# ۲) یک DEK بگیر: هم plaintext هم نسخهٔ رمزشده
aws kms generate-data-key --key-id alias/payments-kek --key-spec AES_256 \
--encryption-context tenant=acme,table=cards --output json
# { "CiphertextBlob": "<base64>", "Plaintext": "<base64>", "KeyId": "arn:..." }
# ۳) رمزگشایی — همان encryption context باید دقیقاً یکسان باشد
aws kms decrypt --ciphertext-blob fileb://dek.bin \
--encryption-context tenant=acme,table=cards \
--query Plaintext --output text | base64 --decode > dek.plain
# ۴) چرخش KEK: بازهٔ دلخواه ۹۰ تا ۲۵۶۰ روز، و چرخش فوری در حادثه
aws kms enable-key-rotation --key-id alias/payments-kek --rotation-period-in-days 180
aws kms rotate-key-on-demand --key-id alias/payments-kek
--encryption-context همان AAD است: نگاشت کلید-مقداری که رمز نمیشود ولی رمزنگاشتی به ciphertext گره میخورد. اگر با tenant=acme رمز کرده باشی، رمزگشایی با tenant=globex شکست میخورد. دو سود دیگر: context در audit log ثبت میشود، پس لاگ تو از «سرویس X یک decrypt کرد» به «سرویس X کارت tenant=acme رکورد ۹۹۲ را باز کرد» ارتقا مییابد؛ و میشود در key policy شرط گذاشت که یک نقش فقط با context مشخص اجازهٔ decrypt دارد. سه لایه کنترل با یک پارامتر.
public record Envelope(byte[] ciphertext, byte[] iv, byte[] wrappedDek, String kekVersion) {}
public Envelope protect(String pan, String tenantId) throws Exception {
var dk = kms.generateDataKey(Map.of("tenant", tenantId, "field", "pan"));
try {
byte[] b = Aead.encrypt(dk.plaintext(), pan.getBytes(UTF_8), tenantId.getBytes(UTF_8));
return new Envelope(Arrays.copyOfRange(b, 12, b.length),
Arrays.copyOfRange(b, 0, 12), dk.wrapped(), dk.kekVersion());
} finally {
Arrays.fill(dk.plaintext(), (byte) 0); // کلید را از حافظه پاک کن
}
}
دقت کن که برای مادهٔ کلید هرگز String استفاده نمیکنیم: String در جاوا immutable است و تا زمان GC در heap میماند — در heap dump، core dump و گاهی swap دیده میشود. از byte[]/char[] استفاده کن و بعد از مصرف صفرش کن (به همین دلیل KeyStore.PasswordProtection با char[] کار میکند). و صادق باش: با JIT و GC ممکن است کپیها باقی بمانند، پس در سیستمهای واقعاً حساس کلید اصلاً وارد JVM نمیشود و عملیات به HSM یا Vault transit سپرده میشود.
«داده را با یک کلید یکبارمصرف (DEK) بهصورت محلی رمز میکنیم؛ خود آن کلید را با یک کلید اصلی (KEK) که هرگز از KMS بیرون نمیآید رمز میکنیم؛ و DEK رمزشده را کنار داده ذخیره میکنیم.»
سه مشکل را حل میکند: محدودیت اندازه و تأخیر؛ چرخش ارزان (تغییر KEK فقط چند کیلوبایت DEK را درگیر میکند نه کل دیتابیس)؛ و مهمتر از همه کنترل دسترسی متمرکز و قابل ممیزی — اپلیکیشن هرگز مالک کلید اصلی نیست، فقط اجازهٔ فراخوانی دارد که در یک لحظه قابل لغو است. و اگر عمیقتر بروم: granularity ی DEK (بهازای رکورد، tenant یا partition) یک trade-off بین تعداد فراخوانی KMS و شعاع انفجار کلید لو رفته است — و DEK بهازای tenant امکان crypto-shredding میدهد.
۴. KMS: از مفهوم تا Vault
KMS سرویسی است که مادهٔ کلید را نگه میدارد و بهجای دادن کلید، عملیات رمزنگاری را برایت انجام میدهد — تغییر ذهنی از «کلید را بگیر» به «عملیات را درخواست کن». هر KMS جدی چهار چیز دارد: key policy، versioning، audit log، و backing store (نرمافزاری یا با پشتوانهٔ HSM دارای گواهی FIPS).
| گزینه | مدل اعتماد | چرخش | مناسبِ | نقطهضعف اصلی |
|---|---|---|---|---|
| متغیر محیطی / فایل کانفیگ | هر کسی که به هاست یا CI دسترسی دارد | دستی، عملاً هرگز | نمونهٔ اولیه، dev | راز روی دیسک و در لاگ crash؛ بدون ممیزی |
| Cloud KMS | ابر و IAM آن | خودکار + on-demand | اکثر بارهای کاری ابری | وابستگی به vendor |
| HashiCorp Vault | خوشهٔ Vault و مدل seal | نسخهبندی + rewrap | چندابری، on-prem، dynamic secrets | باید خودت HA و backup را بسازی |
| HSM / Cloud HSM | سختافزار با گواهی FIPS | ceremony یا API | پرداخت، PIN، امضای ریشه | گران، TPS محدود؛ اگر بیفتد داده در دسترس نیست |
| کلید در کد یا git | هیچ | — | هیچوقت | این باگ نیست، حادثه است |
env از فایل کانفیگ بهتر است ولی امن نیست: هر پروسهای با همان uid میتواند /proc/<pid>/environ را بخواند؛ بسیاری از فریمورکها هنگام crash کل environment را چاپ میکنند؛ در Kubernetes متغیرها در manifest پاد و kubectl describe pod دیده میشوند؛ و Docker آنها را در docker inspect نگه میدارد.
اگر مجبوری: راز را در زمان اجرا از Vault/KMS بگیر، و مطمئن شو ENTRYPOINT راز را بهعنوان آرگومان خط فرمان پاس نمیدهد (که در ps aux برای همه دیدنی است).
Vault: چهار مفهومی که باید بشناسی
۱. Seal / Unseal. Vault در حالت sealed بالا میآید: دادهاش رمز است و حتی خودش هم نمیتواند بخواندش. کلید اصلی با Shamir's Secret Sharing به چند سهم شکسته میشود و برای unseal باید حد نصاب سهمها ارائه شود — مثلاً ۳ از ۵. در production معمولاً auto-unseal با یک cloud KMS یا HSM استفاده میشود.
۲. Transit engine — رمزنگاری بهعنوان سرویس. کلید هرگز بیرون نمیآید؛ plaintext میفرستی و ciphertext میگیری.
vault secrets enable transit
vault write -f transit/keys/payments # نوع پیشفرض: aes256-gcm96
vault write transit/encrypt/payments plaintext=$(printf '4111111111111111' | base64)
# ciphertext = vault:v1:8SDd3WHDOjf7mq69CyCq...
vault write -field=plaintext transit/decrypt/payments \
ciphertext="vault:v1:8SDd3WHDOjf7mq69CyCq..." | base64 --decode
vault write -f transit/keys/payments/rotate # نسخهٔ v2؛ v1 هنوز رمزگشایی میکند
vault write transit/keys/payments/config auto_rotate_period=720h # حداقل یک ساعت
vault write transit/rewrap/payments ciphertext="vault:v1:8SDd3W..." # بدون دیدن plaintext
vault write transit/keys/payments/config min_decryption_version=2 # بازنشستگی v1
vault write -f transit/datakey/plaintext/payments # envelope: DEK + نسخهٔ wrapped
vault write -f transit/datakey/wrapped/payments # فقط wrapped
Vault شمارهٔ نسخهٔ کلید را داخل خود ciphertext مینویسد، پس هنگام رمزگشایی لازم نیست بدانی با کدام نسخه رمز شده — و یک job پسزمینه میتواند دقیقاً بشمارد چند رکورد هنوز با vault:v1: ماندهاند. اگر envelope دستی میسازی، حتماً شمارهٔ نسخهٔ کلید را کنار ciphertext ذخیره کن؛ نبود این فیلد شایعترین دلیلی است که تیمها نمیتوانند کلید بچرخانند.
۳. Dynamic secrets. بهجای یک کاربر دیتابیس ثابت با پسورد ثابت، Vault در لحظه کاربر میسازد و بعد از TTL خودش حذفش میکند.
vault secrets enable database
vault write database/config/payments-pg \
plugin_name="postgresql-database-plugin" \
connection_url="postgresql://{{username}}:{{password}}@pg.internal:5432/payments?sslmode=require" \
allowed_roles="payments-app" username="vault_root" password="<bootstrap>"
vault write -force database/rotate-root/payments-pg # حتی تو هم دیگر پسورد ریشه را نمیدانی
vault write database/roles/payments-app db_name=payments-pg \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';" \
default_ttl="1h" max_ttl="24h"
vault read database/creds/payments-app # username/password/lease_id با عمر یک ساعت
۴. Lease. هر راز پویا lease_id و lease_duration دارد؛ کلاینت باید تمدید کند وگرنه Vault کاربر را حذف میکند.
vault lease renew database/creds/payments-app/xxxx
vault lease revoke database/creds/payments-app/xxxx
vault lease revoke -prefix database/creds/payments-app # ابطال گروهی در حادثه
اگر HikariCP را با اعتبارنامهٔ ساعت اول پر کنی و lease منقضی شود، کانکشنهای باز کار میکنند (احراز فقط هنگام اتصال است) ولی هر کانکشن جدید با FATAL: password authentication failed میافتد — ساعتها بعد و در peak traffic، نه هنگام deploy.
راهحل: lifecycle management را فعال کن تا lease تمدید شود؛ maxLifetime هیکاری را کمتر از max_ttl بگذار؛ هنگام دریافت اعتبارنامهٔ جدید pool را با softEvictConnections() تخلیه کن؛ و در staging عمداً lease را revoke کن و رفتار سرویس را ببین.
static یک مقدار ثابت است که تا وقتی دستی عوضش نکنی معتبر است (مثل API key شریک تجاری). dynamic را سیستم راز در لحظهٔ درخواست میسازد، lease دارد و خودکار ابطال میشود.
dynamic وقتی ارزش دارد که backend مقصد بتواند هویت موقت بسازد (دیتابیس، cloud IAM، broker، PKI) و هزینهٔ لو رفتن اعتبارنامهٔ بلندعمر بالا باشد. سه سود: پنجرهٔ زمانی محدود، انتساب دقیق (هر مصرفکننده هویت جدا دارد پس در audit log تفکیکپذیر است)، و ابطال فوری و گروهی.
هزینهاش را هم صادقانه بگو: Vault به یک وابستگی حیاتی تبدیل میشود و باید HA باشد؛ اپلیکیشن باید منطق renew و reconnect داشته باشد؛ دیتابیس ممکن است با role های موقت پر شود اگر revocation درست کار نکند. و برای API keyهای شخص ثالث که اصلاً مفهوم کاربر موقت ندارند، dynamic ممکن نیست و باید سراغ چرخش خودکار static رفت.
۵. HSM — چرا بانکها به نرمافزار اکتفا نمیکنند
KMS نرمافزاری مثل یک کمد قفلدار خوب است: امن تا وقتی سیستمعامل و hypervisor سالم باشند. HSM مثل گاوصندوقی است که اگر کسی سوراخش کند یا دما و ولتاژش را دستکاری کند، بلافاصله محتویات را پاک میکند — و اصلاً دری برای بیرون آوردن کلید ندارد؛ فقط شکافی دارد که داده میدهی و امضا/رمز تحویل میگیری.
HSM (Hardware Security Module) دستگاهی سختافزاری برای تولید، نگهداری و استفاده از کلید است که کلید را هرگز plaintext بیرون نمیدهد؛ دسترسی معمولاً از طریق PKCS#11 (که جاوا با SunPKCS11 provider به آن وصل میشود) یا KMIP است. FIPS 140 هم استاندارد اعتبارسنجی ماژولهای رمزنگاری است؛ گواهی از طریق برنامهٔ CMVP (مشترک NIST و مرکز امنیت سایبری کانادا) صادر میشود.
| سطح | چه چیزی تضمین میشود |
|---|---|
| Level 1 | الگوریتمهای تأییدشده؛ بدون الزام فیزیکی (کتابخانهٔ نرمافزاری) |
| Level 2 | tamper-evidence (مهر و پوشش) + احراز مبتنی بر نقش |
| Level 3 | مقاومت در برابر دستکاری + پاکسازی خودکار کلید + جداسازی فیزیکی پورت ورود کلید — HSM بانکی معمول |
| Level 4 | پوشش کامل با تشخیص حملات محیطی (دما، ولتاژ) |
FIPS 140-3 از ۲۰۱۹ تصویب شد و در ۲۱ سپتامبر ۲۰۲۶ همهٔ گواهیهای FIPS 140-2 به وضعیت Historical منتقل میشوند — دستگاهها از کار نمیافتند، ولی نهادهای فدرال نباید ماژول Historical را در خریدهای جدید بیاورند. اگر روی قرارداد پرداخت یا دولتی کار میکنی بپرس «شمارهٔ certificate این HSM در FIPS 140-3 چیست و در چه سطحی؟» و در برابر «FIPS-compliant هستیم» مقاومت کن: compliant با validated فرق دارد؛ فقط شماره در فهرست CMVP معتبر است.
چرا صنعت پرداخت واقعاً HSM لازم دارد
۱. PIN هرگز نباید در حافظهٔ نرمافزار باشد. وقتی مشتری PIN را وارد میکند، دستگاه آن را در یک PIN block قالببندی میکند (ISO 9564؛ فرمتهای ۰، ۱، ۳ و امروز فرمت ۴ مبتنی بر AES) و بلافاصله رمز میکند. از آن لحظه تا HSM بانک صادرکننده، PIN هرگز plaintext وجود ندارد؛ عملیاتی مثل «ترجمهٔ PIN block از کلید acquirer به کلید issuer» فقط داخل HSM انجام میشود.
۲. تبادل کلید بین سازمانی. استاندارد ANSI X9.24 / TR-31 key block الزام میکند کلید همراه با صفاتش (برای چه کاری مجاز است) در یک بلوک احرازشده منتقل شود تا مهاجم نتواند کلید PIN را بهعنوان کلید داده استفاده کند؛ مهلت فاز ۳ این الزام در اکوسیستم PCI PIN ۱ ژانویهٔ ۲۰۲۵ بود. الگوی مرتبط DUKPT است: هر تراکنش کلیدی مشتقشده از یک کلید پایه دارد، پس لو رفتن یکی بقیه را لو نمیدهد.
۳. الزام ممیزی. ارزیاب شواهد میخواهد که کلیدها در «secure cryptographic device» زندگی میکنند و عملیات دستی روی کلید plaintext با split knowledge و dual control انجام میشود.
مراسم تولید کلید اصلی در محیط پرداخت. — The physical ceremony that generates a master key inside an HSM.
flowchart TD
A[Ceremony scheduled: date, room, witnesses] --> B[Room swept — phones removed]
B --> C[HSM set to secure state — tamper seals recorded]
C --> D[Custodian 1 enters component 1 via smartcard]
D --> E[Custodian 2 enters component 2 via smartcard]
E --> F[HSM combines components into the master key]
F --> G[HSM prints KCV: key check value]
G --> H[Components sealed in tamper-evident envelopes]
H --> I[Stored in separate safes, different custodians]
I --> J[Ceremony log signed by all parties and auditor]
split knowledge یعنی هیچ فردی کل کلید را نمیداند و هر متولی فقط جزء خودش را دارد؛ dual control یعنی هیچ عملیاتی با یک نفر انجام نمیشود. KCV هم یک اثر انگشت کوتاه از کلید است (چند بایت اول رمزگذاری یک بلوک صفر) که با آن دو طرف تأیید میکنند همان کلید را دارند، بدون فاش کردن کلید.
ظرفیت را دستکم نگیر: یک HSM شبکهای شاید چند هزار عملیات RSA در ثانیه بدهد؛ اگر برای هر درخواست HTTP یک امضا میخواهی، سقف TPS تو دیگر CPU نیست، HSM است — و افزودن HSM هفتهها و یک ceremony لازم دارد.
بازیابی را تمرین کن: HSM طوری ساخته شده که کلید بیرون نیاید؛ اگر دستگاه بمیرد و پشتیبان کلید نداشته باشی، دادهٔ رمزشدهات برای همیشه رفته است. و هرگز یک HSM تنها نگیر: حداقل جفت، در دو سایت، با کلید یکسانِ بارگذاریشده در یک ceremony.
از نظر قدرت الگوریتم فرقی نمیکند؛ تفاوت در مدل تهدید و قابلیت اثبات است.
اول، در نرمافزار کلید ناگزیر لحظاتی plaintext در RAM است؛ یک root محلی، memory dump یا swap شدن صفحه آن را افشا میکند. HSM سطح ۳ کلید را در مرزی نگه میدارد که خروجی plaintext ندارد و هنگام دستکاری فیزیکی پاکش میکند. دوم، جداسازی وظایف: در HSM میشود کلید را «فقط برای امضا، غیرقابل استخراج، با احراز دو نفره» تعریف کرد. سوم، الزام: برای PIN و کلیدهای پرداخت، PCI PIN و طرحهای کارتی صراحتاً secure cryptographic device با گواهی معتبر میخواهند — بدون آن مجوز پردازش نداری.
جمعبندی: HSM را جایی میگذاریم که ریشهٔ اعتماد است و برای حجم داده از envelope encryption استفاده میکنیم تا HSM گلوگاه نشود.
۶. رازها در عمل: از .env تا Kubernetes
Kubernetes Secret مقدارها را base64 نگه میدارد. base64 یک encoding است نه encryption: کلیدی ندارد و با یک دستور باز میشود.
kubectl get secret payments-db -o jsonpath='{.data.password}' | base64 --decode
پس Secret دقیقاً به اندازهٔ دو چیز امن است: RBAC روی منبع secrets، و رمزنگاری etcd. بدون این دو، Secret فقط یک ConfigMap با یک لایه آرایش است.
# روی control plane؛ با --encryption-provider-config=/etc/kubernetes/enc.yaml داده میشود
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- kms: # KMS v2 از Kubernetes 1.29 پایدار (GA) است
apiVersion: v2
name: cloud-kms
endpoint: unix:///var/run/kmsplugin/socket.sock
timeout: 3s
- identity: {} # فقط برای خواندن دادهٔ قدیمی؛ همیشه آخرین باشد
اولین provider برای نوشتن و همه برای خواندن (به ترتیب) استفاده میشوند. اگر identity را اول بگذاری، همهچیز plaintext نوشته میشود در حالی که فکر میکنی رمزنگاری روشن است.
مهمتر: روشن کردن رمزنگاری Secretهای موجود را رمز نمیکند، فقط نوشتنهای جدید را — و همین قاعده برای چرخش کلید هم صادق است:
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
--encryption-provider-config-automatic-reload=true اجازه میدهد فایل بدون ریاستارت apiserver بارگذاری شود، ولی rewrite را خودت باید بزنی.
GitOps و راز: دو الگوی بالغ
میخواهی همهچیز در git باشد ولی راز نباید در git باشد.
۱. Sealed Secrets — رمز کن، بعد کامیت کن. یک controller در خوشه جفتکلید دارد؛ تو با کلید عمومی رمز میکنی و فایل رمزشده را با خیال راحت کامیت میکنی.
kubectl create secret generic payments-db --from-literal=password='S3cr3t!' \
--dry-run=client -o yaml > secret.yaml
kubeseal --format yaml < secret.yaml > sealed-secret.yaml # این را کامیت کن
kubeseal --cert pub-cert.pem --format yaml < secret.yaml # بدون دسترسی به خوشه (CI)
kubeseal --re-encrypt < sealed-secret.yaml > tmp.yaml # مهر مجدد با کلید جدید
scope پیشفرض strict است: نام و namespace در رمزنگاری دخیلاند، پس همان فایل جای دیگر باز نمیشود. --scope namespace-wide و --scope cluster-wide شلترش میکنند — یعنی امنتر نه. کلیدهای مهر و موم خودکار هر ۳۰ روز تمدید میشوند.
۲. External Secrets Operator — راز در git نیست، فقط اشارهگر است.
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata: { name: vault-backend, namespace: payments }
spec:
provider:
vault:
server: "https://vault.internal:8200"
path: "kv"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "payments-app"
serviceAccountRef: { name: payments-app }
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: payments-db, namespace: payments }
spec:
refreshInterval: "1h"
secretStoreRef: { name: vault-backend, kind: SecretStore }
target: { name: payments-db-secret, creationPolicy: Owner }
data:
- secretKey: password
remoteRef: { key: payments/db, property: password }
مسیر رسیدن یک راز از منبع حقیقت تا پاد در یک استقرار GitOps. — How a secret travels from its source of truth to a running pod under GitOps.
flowchart LR
Dev[Engineer] -->|writes secret| Vault[(Vault / Cloud KMS)]
Dev -->|commits reference only| Git[(Git repo)]
Git --> ESO[External Secrets Operator]
ESO -->|k8s auth with ServiceAccount| Vault
Vault -->|secret value| ESO
ESO -->|creates and refreshes| Sec[K8s Secret]
Sec -->|mounted as file| Pod[Application Pod]
| روش | راز در git؟ | چرخش | ممیزی دسترسی | مناسبِ |
|---|---|---|---|---|
.env روی سرور |
نه، ولی روی دیسک | دستی | ندارد | dev محلی |
Spring Cloud Config + {cipher} |
ciphertext بله | دستی | تا حدی | تیم Spring بدون Vault |
| Kubernetes Secret خام | نه | دستی | RBAC + audit خوشه | بار کاری غیرحساس با etcd رمزشده |
| Sealed Secrets | ciphertext بله | مهر مجدد دستی | RBAC خوشه | GitOps بدون سرور راز |
| External Secrets Operator | فقط اشارهگر | خودکار با refresh | audit خود Vault/KMS | استاندارد امروزی GitOps |
| Dynamic secrets / Vault Agent | نه | خودکار، عمر ساعتی | کامل و قابل انتساب | دیتابیس، cloud IAM |
وقتی راز به git رفت
راز لو رفته را نمیشود «پاک» کرد، فقط میشود «باطل» کرد. اول بچرخان، بعد تاریخچه را تمیز کن — چون repo ممکن است fork یا clone شده باشد.
gitleaks git --redact -v --report-format sarif --report-path gitleaks.sarif .
gitleaks dir --redact -v . # اسکن فایلهای کاری بدون git
trufflehog git file://. --only-verified --fail # آیا این توکن هنوز زنده است؟
git filter-repo --invert-paths --path config/prod-secrets.yml # بعد از چرخش!
و برای جلوگیری قبل از وقوع، hook ای به نام gitleaks را در .pre-commit-config.yaml اضافه کن.
الگوی بالغ سه لایه دارد: hook روی لپتاپ، اسکن در CI با اعتبارسنجی زنده، و اسکن سمت پلتفرم برای وقتی که کسی --no-verify زده است.
ترتیب همان چیزی است که سنجیده میشود. ۱) دامنه: این راز چیست، به چه چیزی دسترسی میدهد، از چه زمانی در تاریخچه است، repo عمومی بوده یا خصوصی. ۲) بچرخان، نه پاک کن: راز جدید بساز، سرویسها را منتقل کن، راز قدیمی را باطل کن — تا این نشده هیچ کار دیگری اهمیت ندارد. ۳) لاگها را بررسی کن: آیا با آن اعتبارنامه دسترسی غیرمنتظرهای رخ داده؟ ۴) تاریخچه را پاک کن با git filter-repo و force-push هماهنگ — با این آگاهی که forkهای قبلی هنوز راز را دارند. ۵) ریشهیابی: چرا ممکن شد؟ ۶) کنترل بگذار و در صورت لزوم اطلاعرسانی قانونی انجام بده.
اشتباه رایج داوطلبها شروع کردن از مرحلهٔ ۴ است: پاک کردن تاریخچه بدون چرخش فقط شواهد را از بین میبرد و مهاجم همچنان کلید معتبر دارد.
۷. Spring Boot در عمل
Jasypt — رمزنگاری property در همان فایل
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>4.0.4</version> <!-- نیازمند Java 17+ و Spring Boot 3.5+ -->
</dependency>
mvn jasypt:encrypt-value -Djasypt.encryptor.password="$JASYPT_MASTER" \
-Djasypt.plugin.value="my-db-password"
export JASYPT_ENCRYPTOR_PASSWORD='...' # کلید اصلی هرگز داخل فایل نیست
spring:
datasource:
password: ENC(bkHFHmYvL2Nl9DkVc4Yy1V0BqPjF5W...)
jasypt:
encryptor:
algorithm: PBEWITHHMACSHA512ANDAES_256 # پیشفرض 3.x/4.x
key-obtention-iterations: 100000 # پیشفرض ۱۰۰۰ است — بالا ببر
حالا یک راز داری بهجای بیستتا — که پیشرفت است — ولی همان یک راز هنوز باید از جایی بیاید. مشکلات واقعی: چرخش یعنی رمزگذاری و کامیت مجدد همهٔ فایلها؛ ممیزی وجود ندارد؛ ابطال فوری ممکن نیست؛ و لو رفتن master password یعنی رفتن همهٔ رازها یکجا. key-obtention-iterations را حتماً از پیشفرض ۱۰۰۰ بالا ببر. Jasypt برای تیم کوچک یا on-prem بدون Vault قابل دفاع است؛ برای یک backend پرداخت نیست.
Spring Cloud Config Server
keytool -genkeypair -alias config-server-key -keyalg RSA -keysize 4096 \
-dname "CN=Config Server,OU=Platform,O=Example,L=City,S=State,C=US" \
-keystore server.jks -storetype PKCS12 -storepass changeit -keypass changeit
curl -s localhost:8888/encrypt -d 'my-db-password' # → a1b2c3...
encrypt: # روی خود config server
key-store:
location: file:/etc/config/server.jks
password: changeit
alias: config-server-key
secret: changeit
spring.datasource.password={cipher}a1b2c3d4e5f6... # در repo گیتِ کانفیگها
پیشفرض این است که سرور قبل از ارسال رمزگشایی میکند — یعنی مقدار plaintext روی شبکه میرود و TLS و احراز هویت بین سرویس و config server اجباری است. با spring.cloud.config.server.encrypt.enabled=false میشود ciphertext را تا کلاینت برد، ولی آنوقت باید کلید را به همهٔ کلاینتها بدهی.
Spring Cloud Vault — انتخاب پیشفرض برای مقیاس
از نسخهٔ ۳.۰ بارگذاری از طریق ConfigData API انجام میشود، یعنی spring.config.import نه bootstrap.yml.
spring:
config:
import: "vault://"
cloud:
vault:
uri: https://vault.internal:8200
authentication: KUBERNETES # هویت خود پاد، بدون هیچ راز اولیهای
kubernetes:
role: payments-app
kubernetes-path: kubernetes
service-account-token-file: /var/run/secrets/kubernetes.io/serviceaccount/token
kv:
enabled: true
backend: kv
application-name: payments
database:
enabled: true # اعتبارنامهٔ پویا
role: payments-app
backend: database
username-property: spring.datasource.username
password-property: spring.datasource.password
config.lifecycle:
enabled: true # تمدید خودکار lease
min-renewal: 10s
@Service
public class PanCipher { // transit: رمزنگاری بهعنوان سرویس
private final VaultTransitOperations transit;
public PanCipher(VaultOperations vault) { this.transit = vault.opsForTransit(); }
public String protect(String pan) { return transit.encrypt("payments", pan); }
public String reveal(String token) { return transit.decrypt("payments", token); }
}
پاسخ خوب با «کدام ابزار» شروع نمیشود، با «کدام معیار» شروع میشود: آیا راز در git مینشیند؟ چرخش خودکار است یا دستی؟ استفاده قابل ممیزی است؟ ابطال فوری ممکن است؟
با این معیارها: env و Jasypt در سه معیار آخر ضعیفاند و فقط برای dev یا on-prem کوچک قابل دفاعند؛ Spring Cloud Config با {cipher} مشکل «راز خام در git» را حل میکند ولی چرخش و ممیزی نمیدهد؛ Spring Cloud Vault با احراز هویت Kubernetes هر چهار را میدهد.
مسئلهٔ «راز صفرم» را هم باید نام ببری: برای گرفتن راز از Vault خودت به یک راز نیاز داری. راهحل درست حذف آن است — ServiceAccount token در Kubernetes، IAM role در ابر، یا AppRole با response-wrapping در VM. اگر یک VAULT_TOKEN ثابت در env بگذاری، به همان جایی برگشتهای که از آن فرار میکردی.
۸. رمزنگاری در حالت سکون و در حال انتقال
in transit یعنی روی سیم: TLS 1.3، mTLS بین سرویسها، و sslmode=verify-full برای دیتابیس. at rest یعنی روی دیسک — و سه لایهٔ کاملاً متفاوت دارد که اشتباه گرفتنشان شایعترین سوءتفاهم ممیزیهاست.
| لایه | در برابر چه محافظت میکند | در برابر چه نمیکند |
|---|---|---|
| رمزنگاری دیسک (LUKS، EBS) | سرقت فیزیکی دیسک | هر کسی که به سیستمعامل زنده دسترسی دارد |
| TDE | سرقت فایل داده، backup، دیسک | SQL injection، DBA، کاربر مجاز اپلیکیشن |
ستونی (pgcrypto، DBMS_CRYPTO) |
خواندن مستقیم جدول توسط نقش کمدسترسی | DBA ای که هم کلید هم داده را میبیند |
| اپلیکیشنی (envelope + KMS) | DBA، backup، injection، تفکیک tenant | آسیبپذیری در خود اپلیکیشن |
TDE داده را هنگام نوشتن روی دیسک رمز و هنگام خواندن رمزگشایی میکند — بهصورت شفاف برای هر کسی که با یک اتصال معتبر وصل شده. یعنی در برابر SQL injection صفر، در برابر اکانت اپلیکیشن لو رفته صفر، و در برابر DBA کنجکاو صفر. TDE دقیقاً یک سناریو را پوشش میدهد: کسی فایل داده یا نوار پشتیبان را برمیدارد.
PCI DSS این را رسمی کرده: از نسخهٔ ۴.۰، رمزنگاری در سطح دیسک برای رسانهٔ غیرقابلجداشدن بهتنهایی برای ناخوانا کردن PAN کافی نیست و باید سازوکار دیگری اضافه شود. اگر در جلسهٔ معماری کسی گفت «TDE داریم پس PAN امن است»، همان لحظه باید مداخله کنی.
-- PostgreSQL هنوز TDE بومی ندارد؛ معادلها: رمزنگاری دیسک/فایلسیستم، یا سطح ستون:
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE card (
id bigserial PRIMARY KEY,
pan_enc bytea NOT NULL, -- ciphertext تولیدشده در اپلیکیشن
pan_last4 char(4) NOT NULL, -- برای نمایش
pan_bidx bytea NOT NULL -- blind index برای جستوجو
);
CREATE INDEX ON card (pan_bidx);-- Oracle: TDE بومی دارد. باز کردن keystore و رمز کردن آنلاین tablespace:
ADMINISTER KEY MANAGEMENT SET KEYSTORE OPEN
IDENTIFIED BY "wallet_pwd" CONTAINER = CURRENT;
ALTER TABLESPACE users ENCRYPTION ONLINE USING 'AES256' ENCRYPT;
CREATE TABLE card (
id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
pan VARCHAR2(19) ENCRYPT USING 'AES256' NO SALT, -- ستونی، بومی
pan_last4 CHAR(4) NOT NULL,
pan_bidx RAW(32) NOT NULL
);
CREATE INDEX card_bidx_ix ON card (pan_bidx);TDE ستونی در Oracle پیشفرض salt میزند تا مقدارهای یکسان ciphertext یکسان ندهند — ولی ستون salt-دار قابل ایندکسگذاری نیست. وقتی NO SALT مینویسی تا index بسازی، رمزنگاری را قطعی (deterministic) کردهای و تحلیل فراوانی ممکن میشود؛ برای دامنههای کوچک (شمارهٔ ملی، کد وضعیت) این تقریباً معادل عدم رمزنگاری است. راه درست: ciphertext تصادفی نگه دار و جداگانه یک blind index بساز — HMAC از مقدار نرمالشده با کلیدی جدا از کلید رمزنگاری.
-- HMAC را در اپلیکیشن حساب کن و فقط بایت آماده بفرست؛ این فقط برای نمایش مفهوم است
SELECT id, pan_last4 FROM card
WHERE pan_bidx = hmac('4111111111111111', :bidx_key, 'sha256');SELECT id, pan_last4 FROM card
WHERE pan_bidx = DBMS_CRYPTO.MAC(
UTL_I18N.STRING_TO_RAW('4111111111111111', 'AL32UTF8'),
DBMS_CRYPTO.HMAC_SH256, UTL_RAW.CAST_TO_RAW(:bidx_key));هر دو نمونهٔ بالا یک ضعف عملیاتی جدی دارند: کلید وارد لایهٔ دیتابیس میشود. اگر log_statement = 'all' روشن باشد، یا کوئری در pg_stat_activity / V$SQL دیده شود، یا در یک خطای timeout چاپ شود، کلید در متن ساده جایی مینشیند که هیچکس انتظارش را ندارد. بدتر: اگر کلید را در جدولی از همان دیتابیس نگه داری، مهاجمی که به دیتابیس رسیده هم داده هم کلید را دارد و رمزنگاری صفر ارزش دارد.
قاعدهٔ سنیور: HMAC و رمزنگاری را در اپلیکیشن انجام بده و فقط بایت آماده به SQL بفرست. دیتابیس فقط bytea/RAW مقایسه میکند و هرگز کلیدی نمیبیند.
سه گزینه دارم. رمزنگاری قطعی: مقدار یکسان ciphertext یکسان میدهد پس مستقیم قابل index و join است؛ قیمتش نشت الگوست — در دامنههای کمتنوع مهاجم با تحلیل فراوانی مقدارها را حدس میزند.
blind index با HMAC کلیددار: ciphertext تصادفی میماند و یک ستون HMAC جدا برای جستوجوی برابری اضافه میشود. گزینهٔ پیشفرض من است چون فقط برابری را افشا میکند نه ترتیب یا محتوا؛ با کوتاه کردن HMAC میشود عمداً برخورد ایجاد کرد تا نشت کمتر شود و اپلیکیشن فیلتر نهایی را بزند.
جستوجوی محدوده یا متنی: اینجا باید صادق بود — order-preserving encryption نشت زیادی دارد؛ راهحل عملی بازطراحی است: روی فیلدهای مشتق و غیرحساس (BIN، چهار رقم آخر، بازهٔ مبلغ) جستوجو کن.
و نکتهای که امتیاز میگیرد: کلید blind index باید از کلید رمزنگاری جدا باشد، وگرنه چرخش یکی دیگری را هم مجبور به بازسازی میکند — و بازسازی blind index یعنی خواندن و رمزگشایی کل جدول.
۹. Tokenization، Encryption و Masking — و PCI-DSS
| Encryption | Tokenization | Masking | |
|---|---|---|---|
| برگشتپذیر؟ | بله، با کلید | بله، با جستوجو در token vault | نه |
| سازوکار | تبدیل رمزنگاشتی | نگاشت تصادفی در یک انبار | حذف/جایگزینی کاراکتر |
| رابطهٔ خروجی با ورودی | وابسته به کلید | هیچ | بخشی از ورودی باقی میماند |
| مناسبِ | ذخیرهسازی و انتقال | کاهش scope، سیستمهای پاییندستی | نمایش در UI، لاگ، محیط تست |
| امنیت وابسته به | امنیت کلید | امنیت token vault | یکطرفه بودن |
جریان tokenization و اینکه چطور scope را کوچک نگه میدارد. — How tokenization keeps real card data inside one small, audited service.
flowchart LR
UI[Checkout page] -->|PAN over TLS| TS[Tokenization service<br/>in PCI scope]
TS -->|encrypted PAN via KMS| TV[(Token vault)]
TS -->|token: tok_9f3a| ORD[Order service]
ORD --> BILL[Billing service]
ORD --> ANA[Analytics]
BILL -->|token| TS
TS -->|detokenised PAN| PSP[Acquirer / PSP]
دادهٔ دارندهٔ کارت شامل PAN، نام، تاریخ انقضا و کد خدمات است؛ دادهٔ حساس احراز (SAD) شامل کل محتوای track، CVV/CVC/CID، و PIN یا PIN block.
قاعدهٔ آهنین: SAD پس از authorization هرگز ذخیره نمیشود — حتی رمزشده. این الزام ۳.۳ در PCI DSS v4.0.1 است و استثنایش فقط برای صادرکنندگان کارت است. یعنی هیچجا نباید ستونی به نام cvv وجود داشته باشد؛ نه موقت، نه در لاگ، نه در جدول staging.
PAN را میشود ذخیره کرد ولی باید ناخوانا باشد (الزام ۳.۵.۱): رمزنگاری قوی، truncation، tokenization یا hash. تغییر مهم نسخهٔ ۴: hash باید کلیددار باشد (الزام ۳.۵.۱.۱) — SHA-256(PAN) کافی نیست چون فضای PAN قابل شمارش است؛ باید HMAC باشد. برای نمایش (الزام ۳.۴.۱) حداکثر شش رقم اول و چهار رقم آخر، فقط برای کسانی که نیاز کاری اثباتشده دارند.
public static String maskPan(String pan) { // نمایش امن، نه ذخیرهسازی امن
return pan.substring(0, 6) + "*".repeat(pan.length() - 10) + pan.substring(pan.length() - 4);
}
تیمها UI را درست ماسک میکنند و بعد PAN کامل از سه مسیر دیگر بیرون میرود: toString() خودکار روی entity یا record که در یک لاگ خطا چاپ میشود؛ سریالسازی Jackson در request/response logging یا در پیام Kafka؛ و stack trace یک ConstraintViolationException که مقدار نامعتبر را داخل پیام میگذارد.
دفاع: نوع اختصاصی بساز (record Pan(String value)) با toString() بازنویسیشده، @JsonIgnore یا serializer ماسککننده بگذار، و در CI تستی بگذار که یک PAN نمونه را از مسیر لاگ عبور دهد. لاگها سالها میمانند و کنترل دسترسی ضعیفتری از دیتابیس دارند.
«scope» یعنی هر سیستمی که دادهٔ کارت را ذخیره، پردازش یا منتقل میکند — یا میتواند بر امنیت آن اثر بگذارد. هر سرویسِ در scope باید ممیزی، سختسازی، پایش و مستندسازی شود و هزینه خطی با تعداد سرویسها بالا میرود.
پس بهترین بهینهسازی این است که PAN اصلاً وارد سیستم تو نشود: فرم پرداخت میزبانیشده یا iframe درگاه، یا tokenization در لبه. اگر وارد شد، در کوچکترین سرویس ممکن حبسش کن و به بقیه فقط توکن بده. تفاوت بین «۴۰ میکروسرویس در scope» و «۱ سرویس در scope» یک تصمیم معماری روز اول است.
اول چیزی که نگه نمیدارم: CVV هرگز — نه رمزشده، نه هششده، نه در Redis برای پنج دقیقه؛ همچنین track data و PIN block. بعد ترجیح میدهم PAN را هم نگه ندارم: برای پرداخت تکراری اکثر PSPها یک network token میدهند که فقط برای همان merchant معتبر است، پس PAN اصلاً وارد سیستم من نمیشود و scope تقریباً صفر میماند.
اگر الزام کسبوکار واقعاً token vault داخلی میخواهد: یک سرویس کوچک و جدا؛ PAN با envelope encryption رمز میشود (DEK بهازای هر رکورد، KEK در HSM)؛ توکن یک مقدار تصادفی بیمعناست نه تابعی از PAN؛ چهار رقم آخر و BIN ستونهای جدا و غیرحساساند تا UI و تحلیل هرگز detokenise نخواهند؛ detokenise فقط برای سرویس تسویه، با mTLS و مجوز اختصاصی، و هر فراخوانی لاگ میشود. و سیاست نگهداری: کارتی که مدت مشخصی استفاده نشده حذف میشود — و حذف یعنی نابودی DEK آن رکورد.
۱۰. کمینهسازی داده، crypto-shredding و ممیزی
data minimisation سادهترین و قویترین کنترل امنیتی است: دادهٔ ذخیرهنشده لو نمیرود. سه سؤال قبل از هر بحث رمزنگاری: واقعاً به این فیلد نیاز داریم؟ نسخهٔ کامل لازم است یا مشتق کافی است (سال تولد بهجای تاریخ کامل، BIN بهجای PAN)؟ چه مدت نگهداری لازم است؟
crypto-shredding پاسخ مهندسی به «حق فراموش شدن» است: بهازای هر کاربر یا tenant یک DEK جدا بده و برای حذف فقط کلیدش را نابود کن. دادهٔ رمزشده در همهٔ backupها، replicaها و پارتیشنهای Kafka در یک لحظه بایت بیمعنا میشود.
public void forget(String userId) { // حذف بدون لمس یک ردیف داده
kms.scheduleKeyDeletion(dekKeyIdFor(userId)); // در AWS حداقل ۷ روز انتظار
}
سه شکست رایج: همهٔ کاربران یک DEK مشترک دارند (نابودی کلید یعنی نابودی همه)؛ کلید در چند جا کپی شده و «نابودی» فقط یکی را میگیرد؛ و دادهٔ مشتقشده در انبار تحلیلی اصلاً با آن کلید رمز نشده بود. نکتهٔ حقوقی: برخی ناظران crypto-shredding را «حذف» میپذیرند و برخی نه — این را قبل از تکیه بر آن روشن کن.
ممیزی استفاده از کلید
سه چیز را ثبت کن: چه کسی (هویت workload نه فقط IP)، روی چه چیزی (encryption context)، و نتیجه. سیگنالهایی که باید هشدار بدهند: جهش ناگهانی نرخ Decrypt از یک سرویس (نشانهٔ استخراج انبوه)، decrypt از سرویسی که تاکنون نداشته، و رد شدنهای AccessDenied روی یک کلید (نشانهٔ کاوش).
و توجه کن که در معماری envelope نرخ فراخوانی KMS دیگر معادل حجم داده نیست: یک Decrypt میتواند DEK ای را باز کند که میلیونها رکورد را باز میکند. پس در سطح اپلیکیشن هم باید شمرد «چند رکورد با این DEK باز شد».
صادقانه: معمولاً نمیشود لو رفتن خودِ کلید را مستقیم تشخیص داد، چون کپی کردن یک کلید هیچ ردی نمیگذارد. پس استراتژی، تشخیص استفادهٔ غیرعادی است.
سه لایه: تلهگذاری با یک کلید یا راز عمداً بیاستفاده (honeytoken) که هیچ سرویس مشروعی نباید صدایش بزند — هر فراخوانی روی آن یعنی نفوذ قطعی با نرخ خطای مثبت تقریباً صفر؛ تحلیل رفتار با خط پایه از نرخ و الگوی هر workload و هشدار روی انحراف؛ و کنترلهای پیشگیرانه که تشخیص را ساده میکنند — کلید غیرقابل استخراج در HSM (تنها راه سوءاستفاده فراخوانی است که لاگ میشود) و محدود کردن decrypt به encryption context مشخص.
و چون تشخیص قطعی ممکن نیست، چرخش دورهای را بهعنوان کنترل جبرانی اجرا میکنم: اگر کلید هر ۹۰ روز عوض شود، پنجرهٔ ارزشمندی یک کلید دزدیدهشده محدود است حتی اگر هرگز متوجه دزدی نشوم.
۱۱. Crypto-agility و آمادگی post-quantum
crypto-agility یعنی توانایی تعویض الگوریتم، اندازهٔ کلید یا کتابخانه بدون بازنویسی اپلیکیشن. سه پیشنیاز دارد: (۱) شناسه کنار داده — هر ciphertext باید بگوید با چه الگوریتم و چه نسخهٔ کلیدی ساخته شده (کاری که vault:v1: میکند)؛ (۲) انتزاع در کد — اپلیکیشن با یک CryptoService حرف بزند نه با Cipher.getInstance(...) پخششده در ۴۰ کلاس؛ (۳) فهرست موجودی رمزنگاری — که PCI DSS v4 در الزام ۱۲.۳.۳ رسمیاش کرده، با بازبینی حداقل سالانه.
یک کامپیوتر کوانتومیِ بهاندازهٔ کافی بزرگ با الگوریتم Shor، RSA و رمزنگاری خم بیضوی را میشکند. چنین ماشینی هنوز وجود ندارد، ولی تهدید harvest now, decrypt later امروز واقعی است: مهاجم ترافیک رمزشده را ذخیره میکند و ده سال بعد بازش میکند. خبر خوب: رمزنگاری متقارن نسبتاً امن است — Grover جستوجو را از ۲ⁿ به ۲^(n/2) کاهش میدهد، پس AES-256 حاشیهٔ قابل قبولی دارد. مشکل در تبادل کلید و امضاست.
| استاندارد (اوت ۲۰۲۴) | الگوریتم | کاربرد | جایگزین چه |
|---|---|---|---|
| FIPS 203 | ML-KEM (از CRYSTALS-Kyber) | کپسولهسازی کلید | RSA-KEM، ECDH |
| FIPS 204 | ML-DSA (از CRYSTALS-Dilithium) | امضای دیجیتال | RSA، ECDSA |
| FIPS 205 | SLH-DSA (از SPHINCS+) | امضای مبتنی بر hash | امضای پشتیبان با فرض امنیتی متفاوت |
در ۲۰۲۵ الگوریتم HQC هم بهعنوان KEM پشتیبان با مبنای ریاضی متفاوت انتخاب شد تا اگر روزی حملهای به شبکههای لتیس پیدا شد همهچیز یکجا نیفتد. از منظر جاوا، JDK 24 با JEP 496 پیادهسازی ML-KEM و با JEP 497 پیادهسازی ML-DSA را اضافه کرد؛ یکپارچگی کامل با TLS در JSSE هنوز در راه است، در حالی که کتابخانهها و proxyهای TLS (مانند OpenSSL 3.5 به بعد) از گروههای ترکیبی مثل X25519MLKEM768 پشتیبانی میکنند.
نکن: الگوریتم post-quantum را دستی در پروتکل اختصاصی خودت پیاده نکن، و ECDSA را ناگهان با ML-DSA جایگزین نکن.
بکن: (۱) موجودی بگیر — کجا RSA/ECC داری و عمر مفید دادهٔ محافظتشده چقدر است؟ (۲) TLS را در لبه (CDN، ingress، load balancer) به حالت ترکیبی PQC ببر — کمریسکترین و پربازدهترین قدم. (۳) crypto-agility بساز: شناسهٔ الگوریتم را کنار داده ذخیره کن. (۴) عمر گواهیها را کوتاه و صدورشان را خودکار کن، چون مهاجرت واقعی از مسیر تعویض سریع گواهی میگذرد.
جملهٔ برندهٔ مصاحبه: «آمادگی post-quantum در عمل یعنی crypto-agility؛ اگر بتوانم الگوریتم را در یک ربع تعویض کنم، انتخاب دقیق الگوریتم امروز بحرانی نیست.»
۱۲. برگهٔ تقلب دستورها
| هدف | دستور |
|---|---|
| کلید تصادفی ۲۵۶ بیتی | openssl rand -base64 32 |
| فعال کردن transit | vault secrets enable transit |
| ساخت کلید در Vault | vault write -f transit/keys/<name> |
| رمز کردن با Vault | vault write transit/encrypt/<name> plaintext=$(printf '%s' "$V" | base64) |
| چرخش کلید Vault | vault write -f transit/keys/<name>/rotate |
| بازنشستگی نسخهٔ قدیمی | vault write transit/keys/<name>/config min_decryption_version=2 |
| گرفتن DEK از Vault | vault write -f transit/datakey/plaintext/<name> |
| اعتبارنامهٔ پویا دیتابیس | vault read database/creds/<role> |
| گرفتن DEK از AWS KMS | aws kms generate-data-key --key-id alias/k --key-spec AES_256 |
| چرخش KMS با بازهٔ دلخواه | aws kms enable-key-rotation --key-id alias/k --rotation-period-in-days 180 |
| چرخش فوری KMS | aws kms rotate-key-on-demand --key-id alias/k |
| رمزگذاری مجدد Secretها | kubectl get secrets -A -o json | kubectl replace -f - |
| مهر و موم کردن Secret | kubeseal --format yaml < secret.yaml > sealed.yaml |
| اسکن تاریخچهٔ git | gitleaks git --redact -v . |
| اسکن با اعتبارسنجی زنده | trufflehog git file://. --only-verified --fail |
| رمز کردن با Config Server | curl -s localhost:8888/encrypt -d 'value' |
از بالا: یک سلسلهمراتب کلید. در ریشه master key در HSM با گواهی FIPS 140-3 که در یک key ceremony با split knowledge و dual control تولید شده و هرگز plaintext وجود نداشته؛ زیر آن KEKها بهازای هر دامنه و هر محیط (production هرگز با staging مشترک نیست)؛ زیر آن DEKها بهازای هر رکورد یا tenant که در حافظه استفاده و بلافاصله پاک میشوند.
چرخش: DEK عمر کوتاه دارد؛ KEK با بازهٔ تعریفشده بهعلاوهٔ on-demand در حادثه؛ اعتبارنامهٔ دیتابیس پویا و ساعتی — و rewrap در هر سطح باید یک job قابل مشاهده باشد نه اسکریپت دستی. دسترسی: هر workload هویت خودش را دارد، سیاست کلید حداقلدسترسی و مقید به encryption context است، و هیچ انسانی دسترسی مستقیم به کلیدهای production ندارد — فقط break-glass با تأیید دو نفره و لاگ کامل.
داده: SAD اصلاً ذخیره نمیشود؛ PAN فقط در token vault و رمزشده؛ بقیهٔ سیستم فقط توکن و چهار رقم آخر میبیند؛ TDE لایهٔ دفاعی backup است نه کنترل اصلی. و در پایان آمادگی: audit log با هشدار روی رفتار غیرعادی، تمرین دورهای چرخش و بازیابی HSM، و runbook «کلید لو رفت» که تیم یک بار واقعاً اجرایش کرده باشد.
رمزنگاری مسئلهٔ سختی نیست؛ مدیریت کلید هست. کلید را از CSPRNG بگیر، با AEAD کار کن (AES-GCM با nonce یکتا و AAD معنادار)، و همیشه شناسهٔ نسخهٔ کلید را کنار ciphertext ذخیره کن تا چرخش ممکن بماند. برای هر حجم دادهٔ واقعی، envelope encryption الگوی پیشفرض توست: DEK محلی و یکبارمصرف، KEK درون KMS یا HSM که هرگز بیرون نمیآید، و encryption context بهعنوان یک کنترل دسترسی و ممیزی رایگان.
رازها هرگز در git نمیروند؛ base64 رمزنگاری نیست و Secret در Kubernetes دقیقاً به اندازهٔ RBAC و رمزنگاری etcd امن است. در GitOps یا Sealed Secrets استفاده کن یا — بهتر — External Secrets Operator؛ و یک قدم بالاتر، dynamic secrets که پنجرهٔ در معرض بودن را از «نامحدود» به «یک ساعت» میآورد. در Spring Boot، Spring Cloud Vault با احراز هویت Kubernetes مسئلهٔ «راز صفرم» را واقعاً حل میکند.
برای داده: TDE فقط از سرقت فایل و backup محافظت میکند، نه از injection و نه از DBA؛ جستوجوپذیری را با blind index کلیددار حل کن نه با رمزنگاری قطعی؛ SAD (CVV، track، PIN) هرگز پس از authorization ذخیره نمیشود؛ و بزرگترین برد معماری، بیرون نگه داشتن PAN از سیستم و کوچک کردن scope است. بلوغ هم در سه چیز دیده میشود: کمینهسازی داده، ممیزی معنادار استفاده از کلید، و crypto-agility — چون سؤال روز حادثه همیشه یکی است: «چقدر طول میکشد تا این کلید عوض شود؟»
Almost every backend engineer can call AES. Almost nobody can say where that key came from, who can read it, how it gets replaced, or what happens if it leaks tomorrow. That gap — between cryptography and key management — is where companies actually get breached.
Cryptography is a solved problem: nobody is breaking AES-256 today. What breaks is this: a key committed in application.yml; a key unchanged for three years; every service sharing one key; a Kubernetes Secret that is only base64; nobody knowing which secrets a departing teammate had.
Authentication and authorization (OAuth2/JWT/RBAC) live in the spring-security and ms-security chapters and are only referenced here. This chapter is about keys, secrets and the data itself.
- Fundamentals: entropy, symmetric vs asymmetric, why AEAD is your default.
- Key lifecycle: generation to destruction, and the cryptoperiod.
- Envelope encryption: the DEK/KEK model, and why rotation is impossible without it.
- KMS: cloud KMS, encryption context, and Vault — transit, dynamic secrets, leases, seal.
- HSM: FIPS 140-2/3, PIN blocks, key ceremonies, and why payments cannot ship without one.
- Secrets in practice: git, Kubernetes Secrets, Sealed Secrets, ESO, scanning, and Spring Boot (Jasypt, Config Server, Vault).
- Encrypting data: at rest vs in transit, and the searchability problem.
- PII/PAN: tokenization vs encryption vs masking; PCI-DSS and scope reduction.
- Maturity: data minimisation, crypto-shredding, auditing, crypto-agility, post-quantum.
1. Fundamentals: what a key really is
You can buy a good lock and publish its blueprints; its security is not the secrecy of the mechanism but the fact that only you hold the key (Kerckhoffs's principle: public algorithm, private key). Now picture a building with 500 doors. Your problem is no longer making locks — it is where to store keys, who to hand them to, and how many doors you must re-key when one goes missing. This chapter is not about the lock. It is about the key register.
A key is just a string of bits; what makes it a key is being unguessable. The measure is entropy: how many of those bits are truly random. A key derived from SHA-256("password123") is 256 bits long but maybe 30 bits of entropy — it falls in seconds. Rule one: take keys from a CSPRNG (a generator whose output cannot be predicted from prior output), never from a human-typed string.
KeyGenerator kg = KeyGenerator.getInstance("AES");
kg.init(256, SecureRandom.getInstanceStrong());
SecretKey dek = kg.generateKey(); // 256-bit AES key
byte[] nonce = new byte[12];
new SecureRandom().nextBytes(nonce); // 96-bit nonce for GCM
java.util.Random is an LCG with a 48-bit seed; two consecutive outputs let an attacker reconstruct the whole future stream. One operational note: SecureRandom.getInstanceStrong() can block on Linux in a freshly started container with an empty entropy pool and stall startup. For normal use new SecureRandom() is sufficient and safe; if startup drags, -Djava.security.egd=file:/dev/./urandom fixes it.
Symmetric crypto uses one key to encrypt and decrypt and is fast (AES, ChaCha20) but has a distribution problem. Asymmetric crypto uses a public/private pair, solves distribution, and is thousands of times slower (RSA, Ed25519, X25519). The real-world rule: asymmetric for key exchange and signatures, symmetric for the data itself — exactly what TLS and envelope encryption do.
AEAD: why "encrypting" is not enough
Encrypt with AES-CBC and an attacker cannot read your data — but they can change it: flipping a byte of ciphertext flips a known byte of plaintext. If that byte is amount, you are having a bad night.
AEAD (Authenticated Encryption with Associated Data) gives confidentiality and integrity: the output carries an authentication tag, and one tampered bit makes decryption throw. The AAD part is authenticated but not encrypted — say tenant_id — which stops an attacker moving ciphertext between rows.
public final class Aead {
private static final int IV_LEN = 12; // 96-bit, the NIST-recommended GCM IV size
private static final int TAG_BITS = 128;
public static byte[] encrypt(byte[] key, byte[] plaintext, byte[] aad) throws Exception {
byte[] iv = new byte[IV_LEN];
new SecureRandom().nextBytes(iv);
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"),
new GCMParameterSpec(TAG_BITS, iv));
if (aad != null) c.updateAAD(aad); // authenticated, not encrypted
byte[] ct = c.doFinal(plaintext);
return ByteBuffer.allocate(IV_LEN + ct.length).put(iv).put(ct).array();
}
// decrypt mirrors this with DECRYPT_MODE and the same IV/AAD;
// if ciphertext or AAD was tampered with, doFinal throws AEADBadTagException.
}
Use the same key with the same IV twice and an attacker XORs the two ciphertexts to relate the plaintexts and — far worse — recovers the internal authentication key, after which they can forge messages with valid tags. NIST SP 800-38D caps random 96-bit IVs at 2³² messages per key. If you pinned the IV "for reproducibility", or used a counter that resets on restart, you were broken on day one.
Because CBC gives confidentiality but not integrity. An attacker who can reach the ciphertext — a database backup, a writable column — can flip bits so the decrypted plaintext changes in a controlled way, and the application sees no error. CBC with padding is also vulnerable to padding oracles: if the system distinguishes "bad padding" from "bad data" by error or timing, an attacker recovers plaintext byte by byte.
The right answer is AEAD — AES-GCM or ChaCha20-Poly1305 — or, if CBC is mandatory, Encrypt-then-MAC with an HMAC over (IV || ciphertext) and a constant-time comparison. At senior level I add that this decision should not reach the developer at all: use a higher-level layer such as Tink or the AWS Encryption SDK.
2. The key lifecycle — the heart of the topic
Most teams implement exactly two stages: "make a key" and "use the key". The rest becomes urgent only when it is already too late.
The lifecycle states a cryptographic key moves through, from generation to destruction. — چرخهٔ حیات یک کلید رمزنگاری، از تولید تا نابودی.
stateDiagram-v2
[*] --> PreActivation: generate (CSPRNG / HSM)
PreActivation --> Active: activate
Active --> Active: use (encrypt + decrypt)
Active --> Deactivated: cryptoperiod ends / rotate
Deactivated --> Deactivated: decrypt-only (legacy data)
Active --> Compromised: incident detected
Deactivated --> Compromised: incident detected
Compromised --> Destroyed: emergency re-key
Deactivated --> Destroyed: all data re-wrapped
Destroyed --> [*]
1. Generation. From a CSPRNG or inside an HSM. A key generated in an HSM that never leaves in plaintext is "non-exportable" — the strongest assurance level.
2. Storage. A key never sits next to the data it protects. A wrapped DEK in the same row is fine (the KEK protects it), but the KEK belongs in a KMS/HSM. Key in the same table as the data = zero encryption.
3. Distribution. Best case, the key never reaches the service: the service has a workload identity and asks the KMS to decrypt on its behalf.
4. Rotation. Creating a new version and encrypting with it while old versions stay valid for decryption. Rotation without versioning means downtime. The window in which one key may be used is its cryptoperiod (NIST SP 800-57): typically at most one to two years for data-at-rest keys, sooner if the volume encrypted under it is large.
5. Revocation. The key stops being used for encryption but may still decrypt — or, on compromise, no use is permitted and you must re-key immediately.
6. Destruction. Securely erasing key material; the stage teams never perform, which is why a ten-year-old backup surfaces with a still-live key. Deliberate destruction is also a powerful tool: crypto-shredding (section 9).
Many people think rotation means re-encrypting all old data. In a sound architecture, rotating a KEK takes seconds because only wrapped DEKs are re-wrapped, not terabytes of data.
The right design-review question: "If I were told right now that this key had leaked, how many minutes until we are on a new one?" And real rotation has three parts: the new version exists, all writes use it, and a background job re-wraps old data so min_decryption_version can be raised. Until part three finishes, the old key is still a liability.
"Every 90 days" is a memorised answer. The senior answer: the interval follows the cryptoperiod, which depends on the volume encrypted under that key (limits such as the 2³² message cap for GCM with random IVs), the key's exposure (inside an HSM versus spread across 200 pods' memory), and legal or contractual obligation.
In practice: KEKs annually or on a custom schedule (AWS KMS allows 90 to 2560 days) plus on-demand rotation during an incident; DEKs are short-lived and effectively never rotate; dynamic database credentials last an hour; token-signing keys rotate daily to weekly with JWKS overlap. And the sentence that matters most: "I must prove I can rotate before I need to — we rehearse rotation, we don't discover it during an incident."
3. Envelope encryption — the DEK/KEK model
If you take one thing from this chapter, take this.
You seal a confidential letter in a locked envelope. You put that envelope's key into a smaller envelope and seal it with the bank vault's key, then send both together. To open the letter you must hand the small envelope to the bank and say "open this" — the bank never gives out its own key, it only offers an opening service.
Letter = your data · envelope key = the DEK · vault key = the KEK inside a KMS/HSM · "open this" = a Decrypt call.
Three practical reasons: (1) size and performance — cloud KMSes encrypt only small payloads directly (AWS KMS caps at 4 KB), so with envelopes just 32 bytes cross the network; (2) cheap rotation — rotating a KEK re-wraps DEKs only; (3) separation of authority — the service never sees the KEK, and every call is audited.
The full envelope-encryption flow for one write and one read. — جریان کامل envelope encryption برای یک نوشتن و یک خواندن.
sequenceDiagram
participant App as Application
participant KMS as KMS / Vault (KEK)
participant DB as Database
Note over App,DB: WRITE
App->>KMS: GenerateDataKey(keyId, AES_256, encryptionContext)
KMS-->>App: plaintextDEK + encryptedDEK
App->>App: AES-GCM encrypt(payload, plaintextDEK, aad)
App->>App: zeroise plaintextDEK in memory
App->>DB: store {ciphertext, iv, encryptedDEK, keyVersion}
Note over App,DB: READ
DB-->>App: {ciphertext, iv, encryptedDEK, keyVersion}
App->>KMS: Decrypt(encryptedDEK, encryptionContext)
KMS-->>App: plaintextDEK
App->>App: AES-GCM decrypt -> payload
# 1) create a KEK
aws kms create-key --description "payments-dek-wrapper" \
--key-usage ENCRYPT_DECRYPT --key-spec SYMMETRIC_DEFAULT
aws kms create-alias --alias-name alias/payments-kek --target-key-id <key-id>
# 2) get a DEK: plaintext copy plus wrapped copy
aws kms generate-data-key --key-id alias/payments-kek --key-spec AES_256 \
--encryption-context tenant=acme,table=cards --output json
# { "CiphertextBlob": "<base64>", "Plaintext": "<base64>", "KeyId": "arn:..." }
# 3) decrypt - the encryption context must match exactly
aws kms decrypt --ciphertext-blob fileb://dek.bin \
--encryption-context tenant=acme,table=cards \
--query Plaintext --output text | base64 --decode > dek.plain
# 4) rotate the KEK: any period from 90 to 2560 days, plus on-demand
aws kms enable-key-rotation --key-id alias/payments-kek --rotation-period-in-days 180
aws kms rotate-key-on-demand --key-id alias/payments-kek
--encryption-context is AAD: a key/value map that is not encrypted but is cryptographically bound to the ciphertext. Encrypt with tenant=acme and decryption with tenant=globex fails. Two more benefits: the context lands in the audit log, upgrading your trail from "service X called decrypt" to "service X opened the card for tenant=acme, record 992"; and a key policy can require a specific context before allowing decrypt. Three layers of control from one parameter.
public record Envelope(byte[] ciphertext, byte[] iv, byte[] wrappedDek, String kekVersion) {}
public Envelope protect(String pan, String tenantId) throws Exception {
var dk = kms.generateDataKey(Map.of("tenant", tenantId, "field", "pan"));
try {
byte[] b = Aead.encrypt(dk.plaintext(), pan.getBytes(UTF_8), tenantId.getBytes(UTF_8));
return new Envelope(Arrays.copyOfRange(b, 12, b.length),
Arrays.copyOfRange(b, 0, 12), dk.wrapped(), dk.kekVersion());
} finally {
Arrays.fill(dk.plaintext(), (byte) 0); // wipe the key from memory
}
}
Key material never belongs in a String: Java strings are immutable and linger on the heap until GC, surfacing in heap dumps, core dumps and sometimes swap. Use byte[]/char[] and zero them after use — this is why KeyStore.PasswordProtection takes char[]. JIT and GC may still leave copies, which is why in truly sensitive systems the key never enters the JVM at all.
"We encrypt the data locally with a single-use key (the DEK); we encrypt that key with a master key (the KEK) that never leaves the KMS; we store the wrapped DEK next to the data."
It solves three things: the size and latency limit (a KMS is not built for gigabytes); cheap rotation (changing the KEK touches a few kilobytes of DEKs, not the database); and above all centralised, auditable access control — the application never owns the master key, only a revocable permission to call. Going deeper: DEK granularity (per record, per tenant, per partition) trades KMS call volume against the blast radius of a leaked key — and per-tenant DEKs enable crypto-shredding.
4. KMS: from concept to Vault
A KMS holds key material and, instead of handing you the key, performs the operation for you — a shift from "give me the key" to "do this operation". Every serious KMS offers four things: a key policy, versioning, an audit log, and a backing store (software, or HSM-backed with a FIPS certificate).
| Option | Trust model | Rotation | Good for | Main weakness |
|---|---|---|---|---|
| Env var / config file | Anyone with host or CI access | Manual, effectively never | Prototypes, dev | Secret on disk and in crash logs; no audit |
| Cloud KMS | The cloud and its IAM | Automatic + on-demand | Most cloud workloads | Vendor lock-in |
| HashiCorp Vault | The Vault cluster and its seal model | Versioning + rewrap | Multi-cloud, on-prem, dynamic secrets | You must run it HA and back it up |
| HSM / Cloud HSM | Hardware with a FIPS certificate | Ceremony or API | Payments, PIN, root signing | Costly, limited TPS; if it dies, data is unavailable |
| Key in code or git | None | — | Never | Not a bug, an incident |
Env beats a config file but is not safe: any process with the same uid can read /proc/<pid>/environ; many frameworks print the whole environment on crash; in Kubernetes the variables appear in the pod manifest and kubectl describe pod; and Docker keeps them in docker inspect.
If you must: fetch the secret from Vault/KMS at runtime, and make sure ENTRYPOINT does not pass secrets as command-line arguments (visible to everyone in ps aux).
Vault: four concepts you must know
1. Seal / unseal. Vault starts sealed: its storage is encrypted and even Vault cannot read it. The master key is split with Shamir's Secret Sharing and unsealing needs a quorum of shares — say 3 of 5. In production you use auto-unseal backed by a cloud KMS or HSM.
2. Transit engine — encryption as a service. The key never leaves Vault; you send plaintext and get ciphertext.
vault secrets enable transit
vault write -f transit/keys/payments # default type: aes256-gcm96
vault write transit/encrypt/payments plaintext=$(printf '4111111111111111' | base64)
# ciphertext = vault:v1:8SDd3WHDOjf7mq69CyCq...
vault write -field=plaintext transit/decrypt/payments \
ciphertext="vault:v1:8SDd3WHDOjf7mq69CyCq..." | base64 --decode
vault write -f transit/keys/payments/rotate # v2 created; v1 still decrypts
vault write transit/keys/payments/config auto_rotate_period=720h # minimum one hour
vault write transit/rewrap/payments ciphertext="vault:v1:8SDd3W..." # never sees plaintext
vault write transit/keys/payments/config min_decryption_version=2 # retire v1
vault write -f transit/datakey/plaintext/payments # envelope: DEK + wrapped copy
vault write -f transit/datakey/wrapped/payments # wrapped only
Vault writes the key version into the ciphertext itself, so decryption never needs to know which version was used — and a background job can count exactly how many records still start with vault:v1:. If you build envelopes by hand, always store the key version alongside the ciphertext; the absence of that field is the single most common reason teams cannot rotate.
3. Dynamic secrets. Instead of one static database user with a static password, Vault creates a user on demand and deletes it when the TTL expires.
vault secrets enable database
vault write database/config/payments-pg \
plugin_name="postgresql-database-plugin" \
connection_url="postgresql://{{username}}:{{password}}@pg.internal:5432/payments?sslmode=require" \
allowed_roles="payments-app" username="vault_root" password="<bootstrap>"
vault write -force database/rotate-root/payments-pg # now not even you know the root password
vault write database/roles/payments-app db_name=payments-pg \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';" \
default_ttl="1h" max_ttl="24h"
vault read database/creds/payments-app # username/password/lease_id, one-hour lifetime
4. Leases. Every dynamic secret has a lease_id and lease_duration; the client must renew or Vault deletes the user.
vault lease renew database/creds/payments-app/xxxx
vault lease revoke database/creds/payments-app/xxxx
vault lease revoke -prefix database/creds/payments-app # bulk revocation during an incident
Fill HikariCP with hour-one credentials and let the lease expire: open connections keep working (authentication happens only at connect time) but every new connection fails with FATAL: password authentication failed — hours later, at peak traffic, not at deploy time.
The fix: enable lifecycle management so the lease is renewed; set Hikari's maxLifetime below max_ttl; call softEvictConnections() when new credentials arrive; and deliberately revoke a lease in staging to watch what the service does.
A static secret is a fixed value that stays valid until you rotate it by hand — a partner API key, say. A dynamic secret is minted on request, carries a lease, and is revoked automatically.
Dynamic is worth it when the target backend can create ephemeral identities (databases, cloud IAM, brokers, PKI) and a long-lived credential leaking would be expensive. You gain a bounded exposure window, precise attribution (each consumer has its own identity, so the audit log separates them), and instant bulk revocation.
Be honest about the cost: Vault becomes a critical dependency and must be HA; the application needs renew and reconnect logic; the database can fill with orphaned temporary roles if revocation misbehaves. And for third-party API keys with no concept of ephemeral users, dynamic is impossible — there you automate rotation of the static secret instead.
5. HSM — why banks do not settle for software
A software KMS is a good locked cabinet: safe as long as the OS and hypervisor are healthy. An HSM is a safe that erases its contents the moment someone drills it or tampers with its temperature or voltage — and it has no door for taking keys out at all, only a slot where you feed data in and get a signature or ciphertext back.
An HSM (Hardware Security Module) is dedicated hardware for generating, storing and using keys that never releases them in plaintext; access is usually via PKCS#11 (which Java reaches through the SunPKCS11 provider) or KMIP. FIPS 140 is the validation standard for cryptographic modules; certificates are issued through the CMVP, run jointly by NIST and the Canadian Centre for Cyber Security.
| Level | What it guarantees |
|---|---|
| Level 1 | Approved algorithms; no physical requirements (a validated software library) |
| Level 2 | Tamper-evidence (seals, coatings) plus role-based authentication |
| Level 3 | Tamper resistance + automatic key zeroisation + physically separated key-entry ports — the typical banking HSM |
| Level 4 | Full envelope protection with environmental attack detection (temperature, voltage) |
FIPS 140-3 was approved in 2019, and on 21 September 2026 every FIPS 140-2 certificate moves to Historical status — devices keep working, but federal buyers should not cite Historical modules in new procurement. If you work on a payments or government contract, ask "what is this HSM's FIPS 140-3 certificate number, and at which level?" and push back on "we are FIPS-compliant": compliant is not validated; only a number on the CMVP list counts.
Why payments genuinely needs an HSM
1. A PIN must never sit in software memory. When a customer enters a PIN, the device formats it into a PIN block (ISO 9564; formats 0, 1, 3 and today AES-based format 4) and encrypts it immediately. From that instant until the issuer's HSM the PIN never exists in plaintext; operations such as "translate the PIN block from the acquirer key to the issuer key" happen only inside an HSM.
2. Inter-organisation key exchange. The ANSI X9.24 / TR-31 key block standard requires a key to travel with its attributes (what it may be used for) inside an authenticated block, so an attacker cannot repurpose a PIN key as a data key; the phase-3 deadline across the PCI PIN ecosystem was 1 January 2025. A related pattern is DUKPT: every transaction uses a key derived from a base key, so leaking one does not leak the others.
3. Audit obligation. An assessor wants evidence that keys live in a "secure cryptographic device" and that manual operations on plaintext keys use split knowledge and dual control.
The physical ceremony that generates a master key inside an HSM. — مراسم تولید کلید اصلی در محیط پرداخت.
flowchart TD
A[Ceremony scheduled: date, room, witnesses] --> B[Room swept — phones removed]
B --> C[HSM set to secure state — tamper seals recorded]
C --> D[Custodian 1 enters component 1 via smartcard]
D --> E[Custodian 2 enters component 2 via smartcard]
E --> F[HSM combines components into the master key]
F --> G[HSM prints KCV: key check value]
G --> H[Components sealed in tamper-evident envelopes]
H --> I[Stored in separate safes, different custodians]
I --> J[Ceremony log signed by all parties and auditor]
Split knowledge means no single person knows the whole key; dual control means no operation happens with one person present. The KCV is a short fingerprint of the key (the first bytes of encrypting a zero block) letting two parties confirm they hold the same key without revealing it.
Do not underestimate capacity: a network HSM may do a few thousand RSA operations per second. If every HTTP request needs a signature, your TPS ceiling is the HSM rather than CPU — and adding one takes weeks plus a ceremony.
Rehearse recovery: an HSM is built so keys cannot leave; if the device dies with no key backup, your encrypted data is gone permanently. And never buy a single HSM: at least a pair, in two sites, loaded with the same key in one ceremony.
Algorithmically there is no difference; the difference is the threat model and provability.
First, in software the key is inevitably plaintext in RAM for moments; a local root, a memory dump or a swapped page exposes it. A Level 3 HSM keeps it inside a boundary with no plaintext output and zeroises it on tampering. Second, separation of duties: an HSM key can be defined as "signing only, non-extractable, two-person authentication". Third, obligation: for PIN and payment keys, PCI PIN and the card schemes require a certified secure cryptographic device — without it you are not licensed to process. So put the HSM where the root of trust lives, and use envelope encryption for data volume so it never becomes the bottleneck.
6. Secrets in practice: from .env to Kubernetes
A Kubernetes Secret stores values in base64. base64 is an encoding, not encryption: no key, one command to reverse.
kubectl get secret payments-db -o jsonpath='{.data.password}' | base64 --decode
So a Secret is exactly as safe as two things: RBAC on the secrets resource, and etcd encryption at rest. Without both it is a ConfigMap wearing makeup.
# on the control plane; passed via --encryption-provider-config=/etc/kubernetes/enc.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- kms: # KMS v2 is stable (GA) since Kubernetes 1.29
apiVersion: v2
name: cloud-kms
endpoint: unix:///var/run/kmsplugin/socket.sock
timeout: 3s
- identity: {} # decrypt-only fallback; must always be last
The first provider is used for writes, all of them for reads, in order. Put identity first and everything is written in plaintext while you believe encryption is on.
More importantly, turning encryption on only affects new writes — the same applies to key rotation:
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
--encryption-provider-config-automatic-reload=true reloads the file without restarting the apiserver, but the rewrite is still yours to trigger.
GitOps and secrets: two mature patterns
You want everything in git, but secrets must not be in git.
1. Sealed Secrets — encrypt, then commit. A controller in the cluster holds a keypair; you encrypt with the public key and commit the result safely.
kubectl create secret generic payments-db --from-literal=password='S3cr3t!' \
--dry-run=client -o yaml > secret.yaml
kubeseal --format yaml < secret.yaml > sealed-secret.yaml # commit this
kubeseal --cert pub-cert.pem --format yaml < secret.yaml # offline, e.g. in CI
kubeseal --re-encrypt < sealed-secret.yaml > tmp.yaml # re-seal with the newest key
The default scope is strict: name and namespace are bound into the encryption, so the file will not unseal elsewhere. --scope namespace-wide and --scope cluster-wide loosen that — less safe, not more. Sealing keys renew automatically every 30 days.
2. External Secrets Operator — the secret is never in git, only a pointer.
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata: { name: vault-backend, namespace: payments }
spec:
provider:
vault:
server: "https://vault.internal:8200"
path: "kv"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "payments-app"
serviceAccountRef: { name: payments-app }
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: payments-db, namespace: payments }
spec:
refreshInterval: "1h"
secretStoreRef: { name: vault-backend, kind: SecretStore }
target: { name: payments-db-secret, creationPolicy: Owner }
data:
- secretKey: password
remoteRef: { key: payments/db, property: password }
How a secret travels from its source of truth to a running pod under GitOps. — مسیر رسیدن یک راز از منبع حقیقت تا پاد در یک استقرار GitOps.
flowchart LR
Dev[Engineer] -->|writes secret| Vault[(Vault / Cloud KMS)]
Dev -->|commits reference only| Git[(Git repo)]
Git --> ESO[External Secrets Operator]
ESO -->|k8s auth with ServiceAccount| Vault
Vault -->|secret value| ESO
ESO -->|creates and refreshes| Sec[K8s Secret]
Sec -->|mounted as file| Pod[Application Pod]
| Approach | Secret in git? | Rotation | Access audit | Good for |
|---|---|---|---|---|
.env on the server |
No, but on disk | Manual | None | Local dev |
Spring Cloud Config + {cipher} |
Ciphertext yes | Manual | Partial | Spring teams without Vault |
| Raw Kubernetes Secret | No | Manual | Cluster RBAC + audit | Low-sensitivity workloads |
| Sealed Secrets | Ciphertext yes | Manual re-seal | Cluster RBAC | GitOps without a secrets server |
| External Secrets Operator | Pointer only | Automatic refresh | Vault/KMS audit | Today's GitOps default |
| Dynamic secrets / Vault Agent | No | Automatic, hourly | Full, attributable | Databases, cloud IAM |
When a secret reaches git
A leaked secret cannot be "deleted", only invalidated. Rotate first, clean history second — the repo may already be forked or cloned.
gitleaks git --redact -v --report-format sarif --report-path gitleaks.sarif .
gitleaks dir --redact -v . # scan the working tree, no git
trufflehog git file://. --only-verified --fail # is this token still live?
git filter-repo --invert-paths --path config/prod-secrets.yml # after rotating!
Add the gitleaks hook to .pre-commit-config.yaml for prevention. The mature pattern has three layers: a hook on the laptop, a CI scan with live verification, and a platform-side scan for when someone used --no-verify.
The order is what is being tested. 1) Scope it: what the secret unlocks, how long it has been in history, whether the repo was public. 2) Rotate, do not delete: mint a new secret, migrate services, invalidate the old one — until that is done nothing else matters. 3) Review logs: was there unexpected access with that credential? 4) Purge history with git filter-repo and a coordinated force-push, knowing existing forks still hold it. 5) Root cause, then 6) add controls and notify legally if required.
The common candidate mistake is starting at step 4: purging history without rotating destroys evidence while the attacker keeps a valid key.
7. Spring Boot in practice
Jasypt — encrypted properties in the same file
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>4.0.4</version> <!-- requires Java 17+ and Spring Boot 3.5+ -->
</dependency>
mvn jasypt:encrypt-value -Djasypt.encryptor.password="$JASYPT_MASTER" \
-Djasypt.plugin.value="my-db-password"
export JASYPT_ENCRYPTOR_PASSWORD='...' # the master key never lives in a file
spring:
datasource:
password: ENC(bkHFHmYvL2Nl9DkVc4Yy1V0BqPjF5W...)
jasypt:
encryptor:
algorithm: PBEWITHHMACSHA512ANDAES_256 # default in 3.x/4.x
key-obtention-iterations: 100000 # the default is 1000 - raise it
You now have one secret instead of twenty — progress — but it still has to come from somewhere. The real limitations: rotation means re-encrypting and re-committing every file; there is no audit trail; instant revocation is impossible; and losing the master password loses everything at once. Always raise key-obtention-iterations above the default of 1000. Jasypt is defensible for a small team or an on-prem stack without Vault; not for a payments backend.
Spring Cloud Config Server
keytool -genkeypair -alias config-server-key -keyalg RSA -keysize 4096 \
-dname "CN=Config Server,OU=Platform,O=Example,L=City,S=State,C=US" \
-keystore server.jks -storetype PKCS12 -storepass changeit -keypass changeit
curl -s localhost:8888/encrypt -d 'my-db-password' # -> a1b2c3...
encrypt: # on the config server itself
key-store:
location: file:/etc/config/server.jks
password: changeit
alias: config-server-key
secret: changeit
spring.datasource.password={cipher}a1b2c3d4e5f6... # in the config git repo
By default the server decrypts before responding — the plaintext crosses the network, so TLS and authentication between service and config server are mandatory. Setting spring.cloud.config.server.encrypt.enabled=false sends ciphertext all the way to the client, but then every client needs the key.
Spring Cloud Vault — the default choice at scale
Since 3.0, configuration is loaded through the ConfigData API: spring.config.import, not bootstrap.yml.
spring:
config:
import: "vault://"
cloud:
vault:
uri: https://vault.internal:8200
authentication: KUBERNETES # the pod's own identity, no bootstrap secret
kubernetes:
role: payments-app
kubernetes-path: kubernetes
service-account-token-file: /var/run/secrets/kubernetes.io/serviceaccount/token
kv:
enabled: true
backend: kv
application-name: payments
database:
enabled: true # dynamic credentials
role: payments-app
backend: database
username-property: spring.datasource.username
password-property: spring.datasource.password
config.lifecycle:
enabled: true # automatic lease renewal
min-renewal: 10s
@Service
public class PanCipher { // transit: encryption as a service
private final VaultTransitOperations transit;
public PanCipher(VaultOperations vault) { this.transit = vault.opsForTransit(); }
public String protect(String pan) { return transit.encrypt("payments", pan); }
public String reveal(String token) { return transit.decrypt("payments", token); }
}
A good answer starts with criteria, not tools: does the secret land in git? Is rotation automatic? Is usage auditable? Is instant revocation possible?
By those criteria: env vars and Jasypt are weak on the last three and defensible only for dev; Spring Cloud Config with {cipher} fixes "raw secret in git" but gives neither rotation nor audit; Spring Cloud Vault with Kubernetes auth gives all four.
Also name the secret zero problem: to fetch a secret from Vault you need a secret. The right fix is to remove it — a ServiceAccount token in Kubernetes, an IAM role in the cloud, or AppRole with response wrapping on VMs. Put a fixed VAULT_TOKEN in an env var and you are back where you started.
8. Encryption at rest and in transit
In transit means on the wire: TLS 1.3, mTLS between services, sslmode=verify-full for the database. At rest means on disk — with several very different layers whose confusion is the most common audit misunderstanding.
| Layer | Protects against | Does not protect against |
|---|---|---|
| Disk encryption (LUKS, EBS) | Physical theft of the disk | Anyone with access to the running OS |
| TDE | Stolen data files, backups, disks | SQL injection, the DBA, a valid app account |
Column-level (pgcrypto, DBMS_CRYPTO) |
A low-privilege role reading the table | A DBA who sees both key and data |
| Application-level (envelope + KMS) | DBA, backups, injection, tenant isolation | A vulnerability in the application itself |
TDE encrypts on write and decrypts on read — transparently for anyone connected with valid credentials. That means zero protection against SQL injection, a compromised application account, or a curious DBA. TDE covers exactly one scenario: someone walks off with the data file or the backup tape.
PCI DSS made this official: since v4.0, disk-level encryption alone is not sufficient to render PAN unreadable on non-removable media. When someone says "we have TDE, so PAN is safe", that is your moment to intervene.
-- PostgreSQL still has no native TDE; the equivalents are disk/filesystem
-- encryption, or column-level encryption:
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE card (
id bigserial PRIMARY KEY,
pan_enc bytea NOT NULL, -- ciphertext produced in the application
pan_last4 char(4) NOT NULL, -- for display
pan_bidx bytea NOT NULL -- blind index for lookups
);
CREATE INDEX ON card (pan_bidx);-- Oracle has native TDE. Open the keystore and encrypt a tablespace online:
ADMINISTER KEY MANAGEMENT SET KEYSTORE OPEN
IDENTIFIED BY "wallet_pwd" CONTAINER = CURRENT;
ALTER TABLESPACE users ENCRYPTION ONLINE USING 'AES256' ENCRYPT;
CREATE TABLE card (
id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
pan VARCHAR2(19) ENCRYPT USING 'AES256' NO SALT, -- native column TDE
pan_last4 CHAR(4) NOT NULL,
pan_bidx RAW(32) NOT NULL
);
CREATE INDEX card_bidx_ix ON card (pan_bidx);Oracle column TDE salts by default so identical values do not produce identical ciphertext — but a salted column cannot be indexed. Writing NO SALT to get an index makes the encryption deterministic and enables frequency analysis; for small domains (national ID, status code) that is nearly equivalent to no encryption. The right approach: keep ciphertext randomised and build a separate blind index — an HMAC of the normalised value, under a key distinct from the encryption key.
-- Compute the HMAC in the application and send ready bytes; this shows the concept only
SELECT id, pan_last4 FROM card
WHERE pan_bidx = hmac('4111111111111111', :bidx_key, 'sha256');SELECT id, pan_last4 FROM card
WHERE pan_bidx = DBMS_CRYPTO.MAC(
UTL_I18N.STRING_TO_RAW('4111111111111111', 'AL32UTF8'),
DBMS_CRYPTO.HMAC_SH256, UTL_RAW.CAST_TO_RAW(:bidx_key));Both snippets share a serious operational flaw: the key enters the database layer. With log_statement = 'all', or the query visible in pg_stat_activity / V$SQL, or printed in a timeout error, the key lands in cleartext somewhere nobody expects. Worse, store the key in another table of the same database and an attacker who reached the database has both, making the encryption worthless.
The senior rule: do HMAC and encryption in the application and send only finished bytes to SQL. The database compares bytea/RAW and never sees a key.
Three options. Deterministic encryption: equal values give equal ciphertext, so it indexes and joins directly; the price is pattern leakage — in low-cardinality domains frequency analysis effectively reveals the values.
Blind index with a keyed HMAC: ciphertext stays randomised and a separate HMAC column supports equality lookups. My default, because it leaks only equality, not order or content; truncating the HMAC deliberately creates collisions so the database returns a candidate set and the application filters.
Range or text search: be honest — order-preserving encryption leaks a lot. The practical answer is redesign: search on derived, non-sensitive fields (BIN, last four digits, amount buckets).
The point that earns credit: the blind-index key must be separate from the encryption key, otherwise rotating one forces rebuilding the other — and rebuilding a blind index means decrypting the whole table.
9. Tokenization, encryption and masking — and PCI-DSS
| Encryption | Tokenization | Masking | |
|---|---|---|---|
| Reversible? | Yes, with the key | Yes, via a vault lookup | No |
| Mechanism | Cryptographic transform | Random mapping in a vault | Removing/replacing characters |
| Output vs input | Key-dependent | No relationship | Part of the input survives |
| Good for | Storage and transport | Scope reduction, downstream systems | UI display, logs, test data |
| Security rests on | Key security | Token-vault security | Being one-way |
How tokenization keeps real card data inside one small, audited service. — جریان tokenization و اینکه چطور scope را کوچک نگه میدارد.
flowchart LR
UI[Checkout page] -->|PAN over TLS| TS[Tokenization service<br/>in PCI scope]
TS -->|encrypted PAN via KMS| TV[(Token vault)]
TS -->|token: tok_9f3a| ORD[Order service]
ORD --> BILL[Billing service]
ORD --> ANA[Analytics]
BILL -->|token| TS
TS -->|detokenised PAN| PSP[Acquirer / PSP]
Cardholder data is the PAN, name, expiry and service code; sensitive authentication data (SAD) is full track content, CVV/CVC/CID, and the PIN or PIN block.
The iron rule: SAD is never stored after authorization — not even encrypted. That is requirement 3.3 in PCI DSS v4.0.1, with an exception only for issuers. There must be no column named cvv anywhere: not temporarily, not in logs, not in a staging table.
PAN may be stored but must be rendered unreadable (3.5.1): strong encryption, truncation, tokenization or hashing. A key change in v4: the hash must be keyed (3.5.1.1) — SHA-256(PAN) no longer suffices because the PAN space is enumerable; it must be an HMAC. For display (3.4.1) at most the first six and last four digits, and only for a proven business need.
public static String maskPan(String pan) { // safe display, not safe storage
return pan.substring(0, 6) + "*".repeat(pan.length() - 10) + pan.substring(pan.length() - 4);
}
Teams mask the UI correctly and then the full PAN escapes three other ways: a generated toString() printed in an error log; Jackson serialisation in request/response logging or a Kafka message; and the stack trace of a ConstraintViolationException that embeds the offending value.
The defence: a dedicated type (record Pan(String value)) with an overridden toString(), @JsonIgnore or a masking serializer, and a CI test that pushes a sample PAN through the logging path. Logs live for years with weaker access control than the database.
"Scope" is every system that stores, processes or transmits card data — or can affect its security. Every in-scope service must be audited, hardened, monitored and documented, and cost grows linearly with service count.
So the best optimisation is to keep the PAN out of your system entirely: a hosted payment form or gateway iframe, or tokenization at the edge. If it does enter, confine it to one small service and give everyone else a token — "40 services in scope" versus "1" is a day-one decision.
First, what I will not keep: never the CVV — not encrypted, not hashed, not in Redis for five minutes; likewise never track data or PIN blocks. Second, I would rather not keep the PAN either: most PSPs issue a network token valid only for that merchant, so the PAN never enters my system and scope stays near zero.
If the business genuinely needs an in-house token vault: a small isolated service; the PAN under envelope encryption (per-record DEK, KEK in an HSM); the token a random value unrelated to the PAN; last four digits and BIN as separate non-sensitive columns so UI and analytics never detokenise; detokenisation restricted to the settlement service over mTLS with a dedicated permission and every call logged. Plus retention: cards unused for a defined period are deleted — and deletion means destroying that record's DEK.
10. Data minimisation, crypto-shredding and auditing
Data minimisation is the simplest and strongest control: data you never stored cannot leak. Three questions before any encryption discussion: do we truly need this field? Full value or a derivative (birth year instead of full date, BIN instead of PAN)? How long must we retain it?
Crypto-shredding is the engineering answer to the right to erasure: give every user or tenant a separate DEK and, to delete, destroy only that key. Encrypted data in every backup, replica and Kafka partition becomes meaningless bytes instantly.
public void forget(String userId) { // deletion without touching a row
kms.scheduleKeyDeletion(dekKeyIdFor(userId)); // AWS enforces a 7-day minimum wait
}
Three common failures: all users share one DEK (destroying it destroys everyone); the key was copied elsewhere and "destruction" catches one copy; and derived data in the analytics warehouse was never encrypted under that key. A legal note: some regulators accept crypto-shredding as deletion and some do not — settle that before relying on it.
Auditing key usage
Record three things: who (the workload identity, not just an IP), on what (the encryption context), and the outcome. Alert on a sudden jump in Decrypt rate from one service (bulk exfiltration), decrypt calls from a service that never made them, and AccessDenied failures on a key (probing).
And note that under envelope encryption KMS call rate no longer tracks data volume: one Decrypt can unwrap a DEK that opens millions of records. So count "how many records were opened with this DEK" at the application level too.
Honestly: you usually cannot detect compromise of the key itself, because copying a key leaves no trace. So the strategy is detecting abnormal use.
Three layers: a honeytoken — a key or secret deliberately left unused that no legitimate service should ever call, so any call is a near-zero-false-positive intrusion signal; behavioural analysis with a baseline of each workload's call pattern, alerting on deviation; and preventive controls that make detection easy — a non-extractable key in an HSM (the only abuse path is a logged call) and restricting decrypt to a specific encryption context.
And because certain detection is impossible, I run periodic rotation as a compensating control: a stolen key has a bounded useful window even if I never notice the theft.
11. Crypto-agility and post-quantum readiness
Crypto-agility is the ability to swap algorithm, key size or library without rewriting the application. Three prerequisites: (1) an identifier next to the data — every ciphertext must declare which algorithm and key version produced it (what vault:v1: does); (2) an abstraction in code — the application talks to a CryptoService, not to Cipher.getInstance(...) scattered across 40 classes; (3) a cryptographic inventory, which PCI DSS v4 formalises in requirement 12.3.3, reviewed at least annually.
A sufficiently large quantum computer running Shor's algorithm breaks RSA and elliptic-curve cryptography. No such machine exists yet, but harvest now, decrypt later is real today: an attacker stores encrypted traffic and opens it ten years later. The good news is that symmetric crypto holds up — Grover reduces search from 2ⁿ to 2^(n/2), so AES-256 keeps an acceptable margin. The problem is key exchange and signatures.
| Standard (August 2024) | Algorithm | Purpose | Replaces |
|---|---|---|---|
| FIPS 203 | ML-KEM (from CRYSTALS-Kyber) | Key encapsulation | RSA-KEM, ECDH |
| FIPS 204 | ML-DSA (from CRYSTALS-Dilithium) | Digital signatures | RSA, ECDSA |
| FIPS 205 | SLH-DSA (from SPHINCS+) | Hash-based signatures | A backup with different security assumptions |
In 2025 HQC was also selected as a backup KEM with different mathematical foundations, so a future attack on lattices would not take everything down at once. On the Java side, JDK 24 added ML-KEM via JEP 496 and ML-DSA via JEP 497; full TLS integration in JSSE is still on the way, while TLS libraries and proxies (OpenSSL 3.5 and later) already support hybrid groups such as X25519MLKEM768.
Do not: hand-implement a post-quantum algorithm in your own protocol, and do not swap ECDSA for ML-DSA overnight.
Do: (1) take inventory — where is RSA/ECC used, and how long must the data stay confidential? (2) move edge TLS (CDN, ingress, load balancer) to hybrid PQC — the lowest-risk, highest-return step; (3) build crypto-agility by storing the algorithm identifier next to the data; (4) shorten certificate lifetimes and automate issuance, because real migration runs through fast certificate replacement.
The interview-winning line: "Post-quantum readiness in practice means crypto-agility; if I can swap an algorithm in fifteen minutes, picking the exact algorithm today is not the critical decision."
12. Command cheat sheet
| Goal | Command |
|---|---|
| Random 256-bit key | openssl rand -base64 32 |
| Enable transit | vault secrets enable transit |
| Create a Vault key | vault write -f transit/keys/<name> |
| Encrypt with Vault | vault write transit/encrypt/<name> plaintext=$(printf '%s' "$V" | base64) |
| Rotate a Vault key | vault write -f transit/keys/<name>/rotate |
| Retire an old version | vault write transit/keys/<name>/config min_decryption_version=2 |
| Get a DEK from Vault | vault write -f transit/datakey/plaintext/<name> |
| Dynamic DB credentials | vault read database/creds/<role> |
| Get a DEK from AWS KMS | aws kms generate-data-key --key-id alias/k --key-spec AES_256 |
| Custom KMS rotation period | aws kms enable-key-rotation --key-id alias/k --rotation-period-in-days 180 |
| Immediate KMS rotation | aws kms rotate-key-on-demand --key-id alias/k |
| Re-encrypt all Secrets | kubectl get secrets -A -o json | kubectl replace -f - |
| Seal a Secret | kubeseal --format yaml < secret.yaml > sealed.yaml |
| Scan git history | gitleaks git --redact -v . |
| Scan with live verification | trufflehog git file://. --only-verified --fail |
| Encrypt via Config Server | curl -s localhost:8888/encrypt -d 'value' |
Top down: a key hierarchy. At the root, a master key in a FIPS 140-3 certified HSM, generated in a ceremony with split knowledge and dual control, never existing in plaintext; below it KEKs per domain and per environment (production never shares with staging); below those DEKs per record or tenant, used in memory and wiped immediately.
Rotation: DEKs are short-lived; KEKs rotate on a schedule plus on demand during an incident; database credentials are dynamic and hourly — and re-wrapping must be an observable job, not a manual script. Access: every workload has its own identity, key policies are least-privilege and bound to an encryption context, and no human touches production keys directly.
Data: SAD is never stored; the PAN lives encrypted in the token vault only; everything else sees a token and the last four digits; TDE is defence in depth, not the primary control. Finally readiness: alerts on abnormal key usage, rehearsed rotation and HSM recovery, and a "key leaked" runbook the team has actually executed once.
Cryptography is not the hard part; key management is. Take keys from a CSPRNG, use AEAD (AES-GCM with a unique nonce and a meaningful AAD), and always store the key version next to the ciphertext so rotation stays possible. For any real data volume, envelope encryption is your default: a local single-use DEK, a KEK inside a KMS or HSM that never leaves, and encryption context as a free access-control and audit mechanism.
Secrets never go into git; base64 is not encryption, and a Kubernetes Secret is exactly as safe as its RBAC and etcd encryption. Under GitOps use Sealed Secrets or — better — the External Secrets Operator; one step further, dynamic secrets shrink the exposure window from "unbounded" to "one hour". In Spring Boot, Spring Cloud Vault with Kubernetes authentication actually solves the secret-zero problem.
For data: TDE protects only against stolen files and backups, not injection and not the DBA; solve searchability with a keyed blind index; SAD (CVV, track, PIN) is never stored after authorization; and the biggest architectural win is keeping the PAN out of your system. Maturity shows in three things: data minimisation, meaningful auditing of key usage, and crypto-agility — because the question on incident day is always the same: "how long does it take to change this key?"