Security & Crypto · امنیت و رمزنگاری سنیورSenior ~68 دقیقه مطالعه~61 min read
PKI، TLS، گواهیها و GPGPKI, TLS, Certificates & GPG
از صفر تا سطح senior در زنجیرهی اعتماد: ساختار واقعی گواهی X.509، CA و CSR و ابطال، دستدادن TLS 1.2 در برابر 1.3، mTLS، دنیای keystore/truststore و خطاهای کلاسیک PKIX در جاوا، آشپزخانهی openssl، چرخش گواهی با ACME و Kubernetes، و در پایان GPG برای امضای فایل و artifact و commit.A zero-to-senior walk through the chain of trust: what an X.509 certificate really contains, CAs, CSRs and revocation, the TLS 1.2 vs 1.3 handshake, mTLS, the Java keystore/truststore world and its classic PKIX failures, an openssl cookbook, certificate rotation with ACME and Kubernetes, and finally GPG for signing files, artifacts, commits and tags.
پیشنیاز:Prerequisites: رمزنگاری از پایه: AES، RSA، هش و امضای دیجیتالCryptography Foundations: AES, RSA, Hashing & Digital Signatures
پشت آن قفل کوچک کنار آدرس سایت، یک ماشین اعتماد چندلایه کار میکند که اکثر مهندسها فقط لایهی اولش را میشناسند. تا وقتی همهچیز کار میکند کسی سؤال نمیپرسد؛ اما لحظهای که در لاگ production این خط ظاهر شود —
PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException:
unable to find valid certification path to requested target
— فرق مهندس mid و senior معلوم میشود: یکی یک TrustManager که «همهچیز را قبول میکند» کپی میکند، دیگری در سی ثانیه میفهمد کدام حلقهی زنجیره غایب است.
این فصل همان ماشین را از پیچ اول باز میکند: از «گواهی اصلاً چیست» تا handshake، ابزارهای روزمره، چرخش خودکار در Kubernetes، و در پایان دنیای موازی PGP/GPG که همان مسئله را با فلسفهای متفاوت حل کرده است.
۱. پایه: کلید عمومی/خصوصی، hash، امضا. ۲. گواهی: فیلدهای X.509، SAN، EKU. ۳. زنجیرهی اعتماد: root، intermediate، leaf، trust store. ۴. چرخهی صدور: CSR، انقضا، کوتاهشدن جهانی عمر گواهیها. ۵. ابطال: CRL، OCSP، stapling. ۶. TLS: 1.2 در برابر 1.3، cipher suite، forward secrecy، SNI، ALPN، resumption، 0-RTT. ۷. mTLS. ۸. جاوا: keystore/truststore، keytool، cacerts، SSLContext، خطاهای کلاسیک. ۹. openssl cookbook. ۱۰. عملیات: pinning، CT، ACME، Kubernetes. ۱۱. GPG: web of trust، subkey، امضای artifact و commit، keyserver، ابطال.
بخش ۱ — سه مفهوم پایه
صندوق پستی با یک شکاف روی در: هرکسی میتواند نامه بیندازد (کلید عمومی)، ولی فقط دارندهی کلید فلزی میتواند بازش کند (کلید خصوصی). حالا برعکسش کن: اگر صاحب صندوق یک مهر شخصی داشته باشد که فقط او دارد، هرکسی مهر را میبیند میتواند تأیید کند نامه از اوست — بدون اینکه بتواند مهر را بسازد. اولی رمزنگاری است، دومی امضا.
رمزنگاری نامتقارن یعنی جفتی از کلیدها که از روی عمومی نمیشود خصوصی را ساخت. با کلید عمومی رمز کن تا فقط خصوصی باز کند؛ با کلید خصوصی امضا کن تا همه با عمومی تأیید کنند. الگوریتمهای امروز: RSA (کند و بزرگ، ولی همهجا هست)، ECDSA روی P-256/secp256r1 (سریع و کوچک)، و Ed25519 (مدرنترین؛ پیشفرض جدید OpenPGP).
hash یک تابع یکطرفه است که هر ورودی را به اثر انگشتی با طول ثابت تبدیل میکند (SHA-256 = ۲۵۶ بیت). تغییر یک بیت ورودی کل خروجی را عوض میکند و پیدا کردن دو ورودی با hash یکسان (collision) باید ناممکن باشد. MD5 و SHA-1 هر دو شکستهاند؛ به همین دلیل JDK از نسخهی ۲۵ امضای handshake با SHA-1 را پیشفرض غیرفعال کرده است.
امضای دیجیتال = از سند hash بگیر، hash را با کلید خصوصی رمز کن، کنار سند بگذار. تأیید = خودت hash بگیر، امضا را با کلید عمومی باز کن، مقایسه کن. اگر برابر بودند هم integrity داری هم authenticity.
تمام PKI و تمام GPG فقط یک جمله را حل میکنند: «این کلید عمومی مال چه کسی است؟» PKI جواب میدهد «چون یک مرجع مورد اعتماد تأییدش کرده»؛ PGP جواب میدهد «چون کسانی که من به آنها اعتماد دارم تأییدش کردهاند».
بخش ۲ — گواهی واقعاً چیست؟
پاسپورت یک کاغذ است که میگوید «این عکس و این نام مال یک شخص واقعی است»، با یک مهر دولتی پایش. مأمور مرزی به تو اعتماد نمیکند؛ به مهر اعتماد میکند چون نمونهاش را دارد. گواهی TLS همین است: سندی که میگوید «این کلید عمومی متعلق به api.example.com است»، با امضای یک CA پایش.
گواهی یک ساختار استاندارد X.509 v3 است که با ASN.1/DER کدگذاری میشود. همان DER را با Base64 بپیچ و بین -----BEGIN CERTIFICATE----- و -----END CERTIFICATE----- بگذار، میشود PEM — همان چیزی که در فایلهای .crt و .pem میبینی.
| فیلد | معنی | نکتهی عملی |
|---|---|---|
Serial Number |
شناسهی یکتا نزد صادرکننده | برای ابطال و پیگیری با CA لازم است |
Signature Algorithm |
مثلاً sha256WithRSAEncryption |
اگر SHA-1 دیدی، گواهی مرده است |
Issuer |
DN صادرکننده | باید دقیقاً با Subject گواهی بالادست برابر باشد |
Validity |
notBefore / notAfter |
منشأ اکثر حوادث production |
Subject |
DN دارنده (CN=...) |
برای تطبیق hostname دیگر استفاده نمیشود |
SubjectPublicKeyInfo |
کلید عمومی + الگوریتمش | چیزی که واقعاً امضا میشود |
Subject Alternative Name |
فهرست دامنهها/IPها | تنها منبع حقیقت برای hostname |
Basic Constraints |
CA:TRUE/FALSE و pathlen |
تفاوت یک CA با یک برگ |
Key Usage |
digitalSignature, keyCertSign … |
مصرف مجاز کلید |
Extended Key Usage |
serverAuth, clientAuth, codeSigning |
mTLS بدون clientAuth کار نمیکند |
AKI / SKI |
اثر انگشت کلید صادرکننده / خودش | با اینها زنجیره ساخته میشود |
CRL Distribution Points |
آدرس فهرست ابطال | ممکن است اصلاً نباشد |
Authority Information Access |
آدرس OCSP و گواهی صادرکننده | caIssuers نجاتدهندهی زنجیرهی ناقص |
SCT List |
رسیدهای Certificate Transparency | مرورگرها بدون آن گواهی را رد میکنند |
# گواهی یک سرور را بگیر و کامل بخوان
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -text
# فقط چیزهایی که ۹۰٪ مواقع لازم داری
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
سالها دامنه در Subject CN نوشته میشد. از حدود ۲۰۱۷ مرورگرهای اصلی CN را برای تطبیق hostname کاملاً نادیده میگیرند و فقط subjectAltName را میخوانند. اگر گواهی داخلی میسازی و فقط -subj "/CN=api.internal" میدهی، در مرورگر و در curl و در کلاینتهای جدید جاوا با hostname mismatch رد میشود. همیشه -addext "subjectAltName=DNS:api.internal" را هم بده.
گواهی self-signed یعنی Issuer == Subject. از نظر رمزنگاری معتبر است و TLS با آن کار میکند؛ فقط هیچکس دلیلی برای اعتماد ندارد مگر دستی به trust store اضافه شود. تیمها برای سرویسهای داخلی self-signed میسازند و در هر کلاینت استثنا میگذارند؛ سه ماه بعد ۴۰ گواهی دارند بدون هیچ راهی برای ابطال یا چرخش. راه درست: یک CA داخلی بساز، root آن را یکبار در همهی trust storeها پخش کن، بعد برای هر سرویس برگ صادر کن — آنوقت چرخش برگها هیچ تغییری در کلاینتها لازم ندارد.
بخش ۳ — زنجیرهی اعتماد
مدیرعامل پای هر قرارداد کوچکی امضا نمیگذارد؛ به معاونها وکالت میدهد و آنها امضا میکنند. طرف مقابل امضای معاون را میبیند، وکالتنامهاش را چک میکند و به امضای مدیرعامل میرسد که نمونهاش را دارد. root CA مدیرعامل است، intermediate معاون، و گواهی سرور قرارداد.
- Root CA: self-signed، کلیدش در HSM آفلاین، عمر ۱۵–۲۵ سال، و در trust store سیستمعامل/مرورگر/JDK حاضر است.
- Intermediate CA: با root امضا شده، آنلاین، روزانه میلیونها گواهی صادر میکند. لو رفتنش فقط خودش را میسوزاند نه root را.
- Leaf: گواهی سرور تو، با
CA:FALSE؛ نمیتواند چیزی امضا کند.
زنجیره از root تا برگ — Chain of trust from root to leaf:
flowchart TD
R["Root CA<br/>self-signed, offline HSM<br/>in OS/JDK trust store"]
I["Intermediate CA<br/>online, signs millions<br/>sent by the server"]
L["Leaf certificate<br/>SAN = api.example.com<br/>CA:FALSE"]
K["Server private key<br/>never leaves the host"]
R -->|signs| I
I -->|signs| L
L -.-> K
قانون طلایی: سرور باید برگ + تمام intermediateها را بفرستد ولی نباید root را بفرستد. کلاینت با AKI/Issuer زنجیره را میسازد تا به یک root در trust store خودش برسد.
سرور فقط برگ را میفرستد. در مرورگر دسکتاپ کار میکند (چون intermediate را cache کرده یا با AIA میآورد) ولی در curl، جاوا، اپ موبایل و سرویسهای backend میشکند. علامتش unable to get local issuer certificate در openssl و PKIX path building failed در جاواست. تست قطعی:
openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts </dev/null
# باید دو (یا بیشتر) بلوک CERTIFICATE ببینی و در انتها: Verify return code: 0 (ok)
راهحل: در nginx مقدار ssl_certificate باید fullchain باشد نه فقط cert.pem.
| محیط | trust store پیشفرض |
|---|---|
| Debian/Ubuntu | /etc/ssl/certs/ca-certificates.crt (update-ca-certificates) |
| RHEL/Fedora | /etc/pki/tls/certs/ca-bundle.crt (update-ca-trust) |
| JDK | $JAVA_HOME/lib/security/cacerts (رمز پیشفرض changeit) |
| Python/requests | بستهی certifi |
| Node.js | لیست داخلی + NODE_EXTRA_CA_CERTS |
این نکته ساعتها debug تلف میکند: CA شرکت را با update-ca-certificates اضافه میکنی، curl کار میکند، ولی اپلیکیشن جاوا همچنان PKIX path building failed میدهد چون JVM cacerts خودش را دارد. بعضی توزیعها با بستهی ca-certificates-java این دو را همگام میکنند، ولی روی image رسمی JDK داخل Docker معمولاً چنین چیزی نیست.
بخش ۴ — CSR، صدور و انقضا
CSR یک درخواست امضاشده است: «این کلید عمومی من، این هم نامهایی که میخواهم، لطفاً امضا کن». CSR با کلید خصوصی خودت امضا میشود تا CA مطمئن شود واقعاً آن کلید را در اختیار داری.
فرایند صدور گواهی — Certificate issuance flow:
sequenceDiagram
participant S as Server / Ops
participant CA as Certificate Authority
participant DNS as DNS / HTTP endpoint
S->>S: generate private key (never leaves)
S->>S: build CSR (public key + SANs), self-sign it
S->>CA: submit CSR
CA->>DNS: domain control validation (HTTP-01 / DNS-01)
DNS-->>CA: challenge satisfied
CA->>CA: apply policy, add SCT, sign with intermediate key
CA-->>S: leaf certificate + chain
S->>S: install cert + chain, reload server
سه حقیقت دربارهی CSR که تازهکارها اشتباه میکنند: (۱) کلید خصوصی هرگز در CSR نیست و نباید برای CA فرستاده شود — اگر ارائهدهندهای خودش کلید را تولید میکند، آن کلید دیگر خصوصی نیست. (۲) CA مجبور نیست هرچه در CSR هست را بپذیرد؛ عملاً همهچیز بهجز کلید عمومی و SANها را خودش تعیین میکند. (۳) با هر چرخش کلید، CSR جدید لازم است.
# ۱) کلید خصوصی مدرن (ECDSA P-256)
openssl ecparam -name prime256v1 -genkey -noout -out api.key
chmod 600 api.key
# ۲) CSR با SAN — نکته: -noenc جایگزین -nodes قدیمی در OpenSSL 3.x است
openssl req -new -key api.key -out api.csr \
-subj "/CN=api.example.com/O=Example Inc" \
-addext "subjectAltName=DNS:api.example.com,DNS:www.api.example.com"
# ۳) قبل از ارسال، همیشه CSR را بخوان
openssl req -in api.csr -noout -text -verify
SAN، wildcard و انتخاب استراتژی
DNS:api.example.com دقیقاً همان نام است. DNS:*.example.com فقط یک برچسب را پوشش میدهد: با a.example.com میخورد، با a.b.example.com نه، و با خود example.com هم نه. برای IP هم باید IP:10.0.0.5 بنویسی؛ گذاشتن IP در فیلد DNS بیفایده است.
| گزینه | مزیت | ریسکی که senior باید بداند |
|---|---|---|
| تکدامنه | کوچکترین شعاع انفجار، چرخش مستقل | تعداد زیاد گواهی → اتوماسیون اجباری |
| SAN چنددامنهای | یک گواهی برای چند سرویس مرتبط | افشای فهرست دامنههای داخلی؛ چرخش همه با هم |
| Wildcard | بدون صدور جدید برای زیردامنهی جدید | یک کلید خصوصی روی همهی ماشینها؛ فقط با DNS-01 |
| کوتاهعمر (۶ روزه) | ابطال عملاً بیمعنی میشود | اتوماسیون باید بینقص باشد |
| mTLS از CA خصوصی | کنترل کامل، بدون هزینهی CA عمومی | خودت باید چرخش و HSM را اداره کنی |
wildcard وسوسهانگیز است ولی یعنی همان یک کلید خصوصی روی هر ماشینی که یک زیردامنه سرو میکند کپی میشود — از load balancer تا آن سرویس آزمایشی که کسی patch نمیکند. با ACME، صدور جداگانه برای هر نام تقریباً رایگان است. wildcard را برای جایی نگه دار که زیردامنهها پویا هستند (multi-tenant) و صدور on-demand عملی نیست.
انقضا: بزرگترین حادثهی سری در مهندسی
عمر گواهیهای عمومی بهسرعت کوتاه میشود. طبق مصوبهی SC-081v3 در CA/Browser Forum (آوریل ۲۰۲۵):
| از تاریخ | حداکثر عمر گواهی TLS عمومی |
|---|---|
| اکنون | ۳۹۸ روز |
| ۱۵ مارس ۲۰۲۶ | ۲۰۰ روز |
| ۱۵ مارس ۲۰۲۷ | ۱۰۰ روز |
| ۱۵ مارس ۲۰۲۹ | ۴۷ روز |
Let's Encrypt جلوتر رفته: پروفایل shortlived با اعتبار ۱۶۰ ساعت (کمی بیش از ۶ روز) از ژانویه ۲۰۲۶ عمومی شده، و اعلام کرده پروفایل پیشفرض classic را از فوریه ۲۰۲۷ به ۶۴ روز و از فوریه ۲۰۲۸ به ۴۵ روز کاهش میدهد.
با عمر ۴۷ روزه، «یادآوری تقویم» جواب نمیدهد. سه چیز لازم است: inventory از همهی گواهیها با تاریخ انقضا، تجدید خودکار (ACME/cert-manager/Vault)، و alert مستقل از ابزار صدور که مستقیماً به پورت وصل میشود — چون خرابی رایج این نیست که گواهی صادر نشود، بلکه این است که صادر شود ولی سرویس reload نشود و همچنان گواهی قدیمی را سرو کند.
اگر inventory را در پایگاهداده نگه میداری، پرسوجوی هشدار چنین است:
SELECT common_name, environment, not_after,
(not_after::date - CURRENT_DATE) AS days_left
FROM cert_inventory
WHERE not_after < CURRENT_DATE + INTERVAL '21 days'
ORDER BY not_after;SELECT common_name, environment, not_after,
TRUNC(not_after) - TRUNC(SYSDATE) AS days_left
FROM cert_inventory
WHERE not_after < SYSDATE + 21
ORDER BY not_after;در PostgreSQL تفریق دو date عدد صحیح روز میدهد و INTERVAL صریح است؛ در Oracle تفریق دو DATE عدد اعشاری روز میدهد، پس TRUNC لازم است و جمع عدد ساده با SYSDATE یعنی روز. اگر ستون در Oracle از نوع TIMESTAMP باشد، نتیجه یک INTERVAL DAY TO SECOND میشود نه عدد — یک تلهی کلاسیک.
بخش ۵ — ابطال: CRL، OCSP و stapling
فرض کن کلید خصوصی سرور لو رفته و گواهی هنوز ۴۰ روز اعتبار دارد. چطور به دنیا بگوییم این گواهی دیگر معتبر نیست؟
- CRL: CA فهرست امضاشدهای از serialهای ابطالشده منتشر میکند (آدرسش در
CRL Distribution Points). حجیم است و باید مرتب دانلود شود. - OCSP: کلاینت برای همان یک گواهی از responder میپرسد و
good/revoked/unknownمیگیرد. یک رفتوبرگشت در مسیر بحرانی، بهعلاوهی افشای حریم خصوصی — CA میفهمد کدام IP کدام سایت را میبیند. - OCSP Stapling: خودِ سرور پاسخ امضاشده و تازه را از CA میگیرد و در handshake «سنجاق» میکند (extension
status_request). کلاینت هیچ تماسی با CA نمیگیرد.
جریان stapling — OCSP stapling flow:
sequenceDiagram
participant C as Client
participant S as Server
participant O as OCSP responder (CA)
Note over S,O: periodically, off the critical path
S->>O: OCSP request for its own serial
O-->>S: signed response (good, valid ~7 days)
C->>S: ClientHello + status_request extension
S-->>C: ServerHello + Certificate + stapled OCSP response
C->>C: verify signature and freshness locally
اکثر کلاینتها soft-fail هستند: اگر responder در دسترس نبود، اتصال را میپذیرند. یعنی مهاجمی که ترافیک را در دست دارد فقط کافی است پاسخ را drop کند. Let's Encrypt سرویس OCSP خود را در ۶ اوت ۲۰۲۵ کاملاً تعطیل کرد (دلیل اصلی: حریم خصوصی) و به CRL مهاجرت کرد. پاسخ واقعی صنعت به مسئلهی ابطال، گواهی کوتاهعمر است: گواهی ۶ روزه اصلاً اطلاعات ابطال ندارد چون سریعتر از هر CRL منقضی میشود.
اکستنشن «must-staple» (OID 1.3.6.1.5.5.7.1.24) soft-fail را به hard-fail تبدیل میکند: «اگر پاسخ stapled ندیدی، قطع کن». قدرتمند ولی خطرناک — خرابی stapling سایتت را بهجای کند کردن، کاملاً از دسترس خارج میکند.
server {
listen 443 ssl;
http2 on;
server_name api.example.com;
ssl_certificate /etc/ssl/api/fullchain.pem; # leaf + intermediates
ssl_certificate_key /etc/ssl/api/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off; # در TLS 1.3 انتخاب با کلاینت است
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # بدون چرخش کلید، forward secrecy را میشکند
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/ssl/api/chain.pem; # فقط برای تأیید پاسخ OCSP
add_header Strict-Transport-Security "max-age=63072000" always;
}
بخش ۶ — TLS: دستدادن، قدم به قدم
دو نفر در کافهای شلوغ میخواهند حرف بزنند بدون اینکه کسی بشنود، در حالی که همدیگر را ندیدهاند: (۱) هویت طرف مقابل را با پاسپورتش تأیید کن، (۲) زبان رمزی مشترک انتخاب کنید، (۳) کلمهی عبوری بسازید که هیچکدام کاملاً آن را نفرستاده باشید، (۴) از این به بعد همهچیز به آن زبان. handshake دقیقاً همین چهار قدم است.
TLS 1.2 handshake (two round trips before data):
sequenceDiagram
participant C as Client
participant S as Server
C->>S: ClientHello (versions, cipher suites, SNI, ALPN, random)
S-->>C: ServerHello (chosen suite, random)
S-->>C: Certificate (leaf + intermediates)
S-->>C: ServerKeyExchange (ECDHE params, signed) + ServerHelloDone
C->>S: ClientKeyExchange (ECDHE public share)
C->>S: ChangeCipherSpec + Finished
S-->>C: ChangeCipherSpec + Finished
C->>S: Application data (HTTP request)
هزینه: 2-RTT قبل از اولین بایت داده. روی موبایل با ۱۰۰ms تأخیر یعنی ۲۰۰ms فقط برای handshake، بهعلاوهی TCP و DNS.
TLS 1.3 handshake (one round trip, encrypted early) — RFC 8446:
sequenceDiagram
participant C as Client
participant S as Server
C->>S: ClientHello + key_share (guessed group) + supported_versions + SNI/ALPN
S-->>C: ServerHello + key_share
Note over C,S: shared secret derived — everything below is encrypted
S-->>C: {EncryptedExtensions, Certificate, CertificateVerify, Finished}
C->>S: {Finished} + Application data
S-->>C: {NewSessionTicket} for future resumption
| جنبه | TLS 1.2 | TLS 1.3 |
|---|---|---|
| RTT تا اولین داده | ۲ | ۱ (و ۰ با 0-RTT) |
| Forward secrecy | اختیاری (فقط با ECDHE) | اجباری |
| نام suite | TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 |
TLS_AES_128_GCM_SHA256 |
| مذاکرهی الگوریتم امضا | داخل suite | مستقل (signature_algorithms) |
| گواهی روی سیم | متن ساده | رمزشده |
| Resumption | Session ID / Ticket | فقط PSK با NewSessionTicket |
| RC4/3DES/CBC/RSA-kex | هنوز مذاکرهشدنی | حذفشده از استاندارد |
TLS 1.3 با حذف امن شد: RSA key transport، DH ایستا، فشردهسازی، renegotiation و تمام suiteهای غیر AEAD رفتند، و دستهای کامل از حملات (ROBOT، CRIME، Logjam، FREAK) دیگر قابل اجرا نیستند چون آن ویژگیها اصلاً وجود ندارند.
آناتومی یک cipher suite
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 را قطعهقطعه بخوان: ECDHE تبادل کلید است (آن E آخر یعنی ephemeral — کلید موقت که بعد دور ریخته میشود)، RSA الگوریتم امضای سرور (یعنی نوع کلید گواهی)، AES_128_GCM رمز متقارن با حالت AEAD، و SHA256 تابع hash برای KDF. در TLS 1.3 فقط پنج suite وجود دارد و همه AEAD هستند: TLS_AES_128_GCM_SHA256، TLS_AES_256_GCM_SHA384، TLS_CHACHA20_POLY1305_SHA256، TLS_AES_128_CCM_SHA256، TLS_AES_128_CCM_8_SHA256.
با TLS_RSA_WITH_... کلاینت یک secret تصادفی را با کلید عمومی گواهی رمز میکرد و میفرستاد. یعنی هرکس ترافیک را امروز ضبط کند و پنج سال بعد کلید خصوصی سرور را به دست بیاورد، تمام آن ترافیک قدیمی را رمزگشایی میکند. با ECDHE هر اتصال کلید موقت خودش را دارد که هرگز روی سیم نمیرود؛ لو رفتن کلید گواهی، گذشته را لو نمیدهد. در TLS 1.3 این اجباری است.
از JDK 25 (سپتامبر ۲۰۲۵) و بهروزرسانی JDK 21.0.10 (ژانویه ۲۰۲۶)، suiteهای TLS_RSA_* که forward secrecy ندارند بهصورت پیشفرض غیرفعال شدهاند. اگر بعد از ارتقای JDK ناگهان اتصال به یک broker یا سرویس قدیمی با no cipher suites in common یا handshake_failure بشکند، دلیلش همین است. راهحل درست بردن سرور مقابل به ECDHE است؛ بازنویسی jdk.tls.disabledAlgorithms فقط یک مسکّن با تاریخ انقضاست.
SNI، ALPN، ECH و resumption
SNI نام میزبان را در ClientHello و در متن ساده میفرستد تا سروری که صد دامنه روی یک IP دارد بداند کدام گواهی را بدهد؛ بدون آن virtual hosting با HTTPS ممکن نیست. ALPN در همان پیام میگوید کلاینت h2/http/1.1/h3 را میفهمد — HTTP/2 روی TLS بدون ALPN راه نمیافتد و gRPC هم به همین دلیل به آن وابسته است. ECH نسل بعدی است که خود ClientHello را با کلید منتشرشده در DNS رمز میکند تا SNI هم پنهان شود.
اگر اتصال جاواییات پشت proxy با گواهی اشتباه برمیگردد، اول SNI را بررسی کن: با openssl s_client یکبار با -servername و یکبار بدون آن تست کن؛ دو گواهی متفاوت یعنی مشکل SNI است.
resumption اجازه میدهد اتصال بعدی از یک PSK استفاده کند: Session ID (state روی سرور، در معماری چندنودی دردسر) یا Session Ticket (state رمزشده دست کلاینت، سرور stateless میماند). اگر کلید رمزگذاری ticket سالها ثابت بماند و لو برود، تمام sessionهای resumeشدهی گذشته قابل رمزگشایی میشوند — پس یا مرتب بچرخانش یا ssl_session_tickets off;.
در TLS 1.3 کلاینت میتواند دادهی برنامه را همراه اولین پیام بفرستد (early data). این داده در برابر replay محافظتشده نیست: مهاجم میتواند بسته را ضبط و ده بار بازپخش کند. اگر آن بسته POST /payments باشد، ده تراکنش ثبت میشود. قانون: 0-RTT فقط برای درخواستهای idempotent. در nginx یعنی ssl_early_data on; را فقط وقتی فعال کن که اپلیکیشن هدر Early-Data: 1 را میبیند و درخواستهای غیر idempotent را با 425 Too Early رد میکند.
بخش ۷ — mTLS
در TLS معمولی فقط سرور خودش را اثبات میکند. در mTLS سرور هم از کلاینت گواهی میخواهد و همان زنجیرهی اعتماد را روی آن اجرا میکند.
mTLS handshake — both sides present certificates:
sequenceDiagram
participant C as Client (has cert + key)
participant S as Server (has cert + key)
C->>S: ClientHello
S-->>C: ServerHello + Certificate + CertificateVerify
S-->>C: CertificateRequest (acceptable CAs, sig algs)
C->>S: Certificate (client leaf + chain)
C->>S: CertificateVerify (proves possession of client key)
S->>S: verify chain against client-CA truststore + check EKU clientAuth
S-->>C: Finished
| سناریو | mTLS مناسب است؟ | چرا |
|---|---|---|
| کاربر انسانی در مرورگر | خیر | توزیع گواهی روی دستگاه کاربر کابوس است؛ OIDC بهتر است |
| سرویسبهسرویس داخل mesh | بله | هویت workload، صدور خودکار، عمر کوتاه |
| شریک B2B با تعداد ثابت | بله | تعداد محدود و قرارداد رسمی |
| API عمومی با هزاران مصرفکننده | خیر | onboarding و چرخش مقیاسپذیر نیست؛ OAuth2 بهتر است |
| اتصال اپ به DB/broker | معمولاً بله | کلاینتها کم و اتوماسیونپذیرند |
mTLS میگوید «طرف مقابل کیست»، نه «اجازهی چه کاری دارد». الگوی بالغ: mTLS برای احراز هویت workload در لایهی انتقال، و توکن (JWT/OAuth2) برای مجوز کاربر نهایی که میان سرویسها propagate میشود. مدل مجوز در فصل امنیت میکروسرویسها آمده؛ این دو مکملاند نه رقیب.
اگر همهی گواهیهای کلاینت را یک روز و با یک اسکریپت صادر کنی، همه یک روز هم میمیرند و کل سیستم همزمان میایستد. چرخش CA از خود گواهیها سختتر است: باید دورهی گذاری داشته باشی که هر دو طرف هر دو CA را در truststore دارند، سپس برگها را جابهجا کنی، و در انتها CA قدیمی را حذف کنی. ترتیب برعکس یعنی قفل کردن خوشهی خودت.
بخش ۸ — سمت جاوا
keystore کیف پول توست: مدارک شناسایی خودت (گواهی + کلید خصوصی) و باید قفل باشد. truststore دفترچه تلفن است: فهرست کسانی که به آنها اعتماد داری (گواهیهای CA، بدون هیچ کلید خصوصی). اپلیکیشنی که فقط کلاینت است اغلب keystore لازم ندارد؛ سرور TLS حتماً دارد؛ در mTLS هر دو طرف هر دو را دارند.
| ویژگی | JKS | PKCS12 (.p12/.pfx) |
|---|---|---|
| استاندارد | اختصاصی Sun/Oracle | استاندارد بینالمللی (RFC 7292) |
| قابل حمل بین زبانها | خیر | بله (openssl، .NET، مرورگرها) |
| پیشفرض JDK | تا JDK 8 | از JDK 9 به بعد (JEP 229) |
| توصیهی امروز | فقط سازگاری با سیستم قدیمی | پیشفرض همیشگی |
برگهی تقلب keytool
| کار | فرمان |
|---|---|
| جفتکلید + self-signed | keytool -genkeypair -alias api -keyalg EC -groupname secp256r1 -validity 365 -keystore ks.p12 -storetype PKCS12 -dname "CN=api.example.com" -ext "SAN=dns:api.example.com" |
| فهرست محتویات | keytool -list -v -keystore ks.p12 |
| ساخت CSR از کلید موجود | keytool -certreq -alias api -file api.csr -keystore ks.p12 -ext "SAN=dns:api.example.com" |
| وارد کردن گواهی امضاشده | keytool -importcert -alias api -file signed.crt -keystore ks.p12 -trustcacerts |
| افزودن CA به truststore | keytool -importcert -alias corp-root -file corp-root.crt -keystore truststore.p12 -storetype PKCS12 -noprompt |
| صادر کردن به PEM | keytool -exportcert -alias api -rfc -file api.pem -keystore ks.p12 |
| حذف یک ورودی | keytool -delete -alias old-ca -keystore truststore.p12 |
| تغییر رمز store | keytool -storepasswd -keystore ks.p12 |
| تبدیل JKS به PKCS12 | keytool -importkeystore -srckeystore old.jks -srcstoretype JKS -destkeystore new.p12 -deststoretype PKCS12 |
| گرفتن گواهی از یک سرور | keytool -printcert -sslserver api.example.com:443 |
| خواندن فایل گواهی | keytool -printcert -file chain.pem |
| دیدن تنظیمات TLS فعال JDK | keytool -showinfo -tls |
دو تلهی رایج keytool: اول وقتی گواهی امضاشده را برمیگردانی، keytool باید کل زنجیره را بسازد؛ اگر intermediate در همان keystore یا در cacerts نباشد Failed to establish chain from reply میگیری — یا intermediate را اول import کن یا یک PEM زنجیرهای بده. دوم گواهی برگ را باید با همان alias کلید خصوصی import کنی؛ با alias دیگر، بهجای تکمیل ورودی کلید، یک ورودی trusted certificate جدا ساخته میشود و سرور همچنان گواهی self-signed قدیمی را سرو میکند.
cacerts و CA سازمانی
# آیا CA مورد نظر داخل truststore پیشفرض JDK هست؟
keytool -list -cacerts -storepass changeit | grep -i corp-root
# افزودن؛ -cacerts از JDK 9 مستقیماً به truststore پیشفرض اشاره میکند
keytool -importcert -trustcacerts -noprompt -alias corp-root \
-file corp-root.crt -cacerts -storepass changeit
سه راه برای اعتماد به یک CA خصوصی، به ترتیب کیفیت: (۱) truststore جداگانهی اپلیکیشن با -Djavax.net.ssl.trustStore=... — قابل نسخهبندی و چرخش، بدون دستزدن به JDK. (۲) افزودن در Dockerfile هنگام build — تکرارپذیر ولی با ارتقای JDK باید دوباره اجرا شود. (۳) دستکاری دستی cacerts روی سرور — در اولین rollout بعدی از بین میرود؛ انجامش نده.
FROM eclipse-temurin:25-jre
COPY corp-root.crt /tmp/corp-root.crt
RUN keytool -importcert -trustcacerts -noprompt \
-alias corp-root -file /tmp/corp-root.crt \
-cacerts -storepass changeit \
&& rm /tmp/corp-root.crt
ساختن SSLContext با کد
public static SSLContext build(Path trustStore, char[] trustPass,
Path keyStore, char[] keyPass) throws Exception {
KeyStore ts = KeyStore.getInstance("PKCS12");
try (InputStream in = Files.newInputStream(trustStore)) { ts.load(in, trustPass); }
TrustManagerFactory tmf =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ts);
KeyManager[] kms = null; // فقط برای mTLS لازم است
if (keyStore != null) {
KeyStore ks = KeyStore.getInstance("PKCS12");
try (InputStream in = Files.newInputStream(keyStore)) { ks.load(in, keyPass); }
KeyManagerFactory kmf =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, keyPass);
kms = kmf.getKeyManagers();
}
SSLContext ctx = SSLContext.getInstance("TLSv1.3");
ctx.init(kms, tmf.getTrustManagers(), null);
return ctx;
}
// HttpClient تطبیق hostname را خودش انجام میدهد
HttpClient client = HttpClient.newBuilder().sslContext(ctx).build();
خطرناکترین پیشفرض JDK: HttpsURLConnection و java.net.http.HttpClient تطبیق hostname را انجام میدهند، ولی اگر خودت با SSLSocketFactory یا SSLEngine اتصال خام بسازی، JDK فقط زنجیره را تأیید میکند و نگاه نمیکند SAN با هاست میخورد یا نه — یعنی هر گواهی معتبری از هر CAیی برای هر دامنهای پذیرفته میشود. راهحل صریح:
SSLSocket socket = (SSLSocket) ctx.getSocketFactory().createSocket("api.example.com", 443);
SSLParameters p = socket.getSSLParameters();
p.setEndpointIdentificationAlgorithm("HTTPS"); // بدون این خط، تأیید hostname انجام نمیشود
socket.setSSLParameters(p);
socket.startHandshake();
// هرگز، حتی «موقتاً» — این کد TLS را به رمزنگاری بدون احراز هویت تبدیل میکند
TrustManager[] trustAll = { new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] c, String a) { }
public void checkServerTrusted(X509Certificate[] c, String a) { } // ← MITM آزاد
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
}};
این قطعه باید در هر code review رد شود؛ «موقتاً برای تست» همیشه به production میرسد. جایگزین درست: CA داخلی یا mkcert را به یک truststore جداگانه اضافه کن.
خطاهای کلاسیک و درمان واقعی
| پیام خطا | معنی واقعی | درمان |
|---|---|---|
PKIX path building failed … |
JVM نتوانست از گواهی سرور به یک root در truststore برسد | سرور intermediate نمیفرستد (fullchain بگذار) یا CA در truststore نیست (import کن) |
No name matching <host> found |
زنجیره درست است، SAN با hostname نمیخورد | SAN را اصلاح کن؛ SNI را چک کن؛ IP را در IP: بگذار نه DNS: |
CertificateExpiredException |
گواهی منقضی — یا ساعت ماشین اشتباه است | date -u را چک کن؛ در container بدون NTP این خطا فریبنده است |
no cipher suites in common |
همپوشانی نسخه یا suite وجود ندارد | با -Djavax.net.debug=ssl:handshake ببین چه پیشنهاد شده؛ به حذف TLS_RSA_* مشکوک شو |
Received fatal alert: bad_certificate |
سرور گواهی کلاینت را رد کرده (mTLS) | زنجیرهی کلاینت، EKU=clientAuth و truststore سرور را چک کن |
Failed to establish chain from reply |
intermediate هنگام import برگ در دسترس نیست | اول intermediate را import کن یا PEM زنجیرهای بده |
UnrecoverableKeyException: Cannot recover key |
رمز store با رمز key فرق دارد | در PKCS12 هر دو را یکی بگذار یا keyPassword را جدا پیکربندی کن |
TrustAnchor … is not a CA certificate |
یک گواهی برگ را بهعنوان CA import کردهای | root/intermediate واقعی را import کن |
# کاملترین دید به handshake (پرحجم؛ فقط در تست)
java -Djavax.net.debug=ssl:handshake:verbose:trustmanager -jar app.jar
# فقط ساخت مسیر گواهی
java -Djava.security.debug=certpath -jar app.jar
# فعال کردن بررسی ابطال (پیشفرض خاموش است)
java -Dcom.sun.net.ssl.checkRevocation=true \
-Djdk.tls.client.enableStatusRequestExtension=true -jar app.jar
اگر سرور TLS خودت روی JVM اجرا میشود، پشتیبانی از OCSP stapling پیشفرض خاموش است و با -Djdk.tls.server.enableStatusRequestExtension=true روشن میشود (JEP 249). سمت کلاینت jdk.tls.client.enableStatusRequestExtension پیشفرض روشن است ولی تا com.sun.net.ssl.checkRevocation=true نباشد نتیجهی بررسی اعمال نمیشود.
Spring Boot: SSL bundle
از Spring Boot 3.1 بهجای پخشکردن مسیر keystore در ده جا، یک bundle نامدار تعریف میکنی؛ از 3.2 بارگذاری مجدد خودکار هم دارد:
spring:
ssl:
bundle:
pem:
server:
reload-on-update: true # فایلها را watch میکند و bundle را دوباره میسازد
keystore:
certificate: "file:/etc/tls/tls.crt"
private-key: "file:/etc/tls/tls.key"
truststore:
certificate: "file:/etc/tls/ca.crt"
jks:
partner:
keystore:
location: "classpath:client.p12"
password: "${CLIENT_P12_PASSWORD}"
type: "PKCS12"
server:
ssl:
bundle: "server"
در Kubernetes، cert-manager گواهی تازه را در همان Secret مینویسد و kubelet فایلهای mountشده را بهروز میکند — ولی JVM آنها را فقط یکبار در startup خوانده است. بدون reload، pod تا restart بعدی گواهی منقضی را سرو میکند. reload-on-update: true فایلها را watch میکند و symlinkها را دنبال میکند (که برای Secretهای Kubernetes حیاتی است). اگر Spring Boot نداری، معادلش را لازم داری: یک X509ExtendedKeyManager که هر بار از فایل میخواند، یا rollout سادهی pod در زمان تجدید.
بخش ۹ — آشپزخانهی openssl
| هدف | فرمان |
|---|---|
| کلید ECDSA P-256 | openssl ecparam -name prime256v1 -genkey -noout -out key.pem |
| کلید RSA 2048 | openssl genrsa -out key.pem 2048 |
| کلید Ed25519 | openssl genpkey -algorithm ed25519 -out key.pem |
| کلید + CSR یکجا | openssl req -newkey rsa:2048 -noenc -keyout key.pem -out req.csr -subj "/CN=x" |
| self-signed سریع با SAN | openssl req -x509 -newkey rsa:2048 -noenc -keyout key.pem -out cert.pem -days 90 -subj "/CN=x" -addext "subjectAltName=DNS:x" |
| خواندن گواهی | openssl x509 -in cert.pem -noout -text |
| فقط تاریخها / فقط SAN | openssl x509 -in cert.pem -noout -dates · ... -ext subjectAltName |
| اثر انگشت SHA-256 | openssl x509 -in cert.pem -noout -fingerprint -sha256 |
| خواندن CSR | openssl req -in req.csr -noout -text -verify |
| تست یک سرور | openssl s_client -connect host:443 -servername host -showcerts |
| اجبار نسخهی TLS | openssl s_client -connect host:443 -tls1_2 (یا -tls1_3) |
| تست ALPN | openssl s_client -connect host:443 -alpn h2 -servername host |
| تست mTLS | openssl s_client -connect host:8443 -cert client.crt -key client.key -CAfile ca.crt |
| تأیید زنجیره آفلاین | openssl verify -CAfile root.pem -untrusted intermediate.pem leaf.pem |
| PEM → PKCS12 | openssl pkcs12 -export -inkey key.pem -in cert.pem -certfile chain.pem -name api -out bundle.p12 |
| PKCS12 → PEM | openssl pkcs12 -in bundle.p12 -noenc -out all.pem |
| DER ↔ PEM | openssl x509 -inform DER -in cert.der -out cert.pem |
| سرور تست | openssl s_server -accept 8443 -cert cert.pem -key key.pem -www |
| خواندن CRL | openssl crl -in crl.der -inform DER -noout -text |
| پرسوجوی OCSP | openssl ocsp -issuer chain.pem -cert leaf.pem -url http://ocsp.example.com -text |
سه ترکیب که هر روز به کار میآیند:
# ۱) آیا این کلید خصوصی به این گواهی میخورد؟ (دو خروجی باید یکسان باشد)
openssl x509 -noout -pubkey -in cert.pem | openssl sha256
openssl pkey -pubout -in key.pem | openssl sha256
# ۲) زنجیرهای که سرور واقعاً میفرستد چیست؟
openssl s_client -connect api.example.com:443 -servername api.example.com \
-showcerts </dev/null 2>/dev/null | grep -E "^(depth|verify|s:|i:)"
# ۳) یک CA خصوصی کامل در ۶۰ ثانیه (برای dev و تستهای integration)
openssl req -x509 -newkey rsa:4096 -noenc -days 3650 -keyout ca.key -out ca.crt \
-subj "/CN=Dev Root CA" \
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
openssl req -newkey rsa:2048 -noenc -keyout svc.key -out svc.csr -subj "/CN=svc.local"
openssl x509 -req -in svc.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out svc.crt -days 30 -sha256 \
-extfile <(printf "subjectAltName=DNS:svc.local,IP:127.0.0.1\nextendedKeyUsage=serverAuth,clientAuth\nbasicConstraints=CA:FALSE")
openssl verify -CAfile ca.crt svc.crt # → svc.crt: OK
نکتهی نسخه: -nodes (مخفف «no DES») از OpenSSL 3.0 deprecated شده و جایگزین رسمیاش -noenc است. هر دو یعنی «کلید خصوصی را رمز نکن» — پس chmod 600 و مالکیت درست را فراموش نکن و در production کلید را از secret manager بگیر نه از repo.
بخش ۱۰ — pinning، Certificate Transparency و ACME
pinning یعنی «فقط این کلید عمومی (یا این CA) را قبول دارم، حتی اگر CA معتبر دیگری گواهی درستی برای همین دامنه صادر کند». هدفش دفاع در برابر صدور نادرست توسط یک CA است. سه سطح دارد: pin روی گواهی برگ (با هر تجدید میشکند — تقریباً همیشه اشتباه)، pin روی کلید عمومی برگ یعنی SPKI (پایدارتر ولی یعنی چرخش کلید نداری)، و pin روی CA/intermediate (پایدارترین، حفاظت ضعیفتر).
# محاسبهی pin به سبک SPKI برای یک سرور
openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null 2>/dev/null \
| openssl x509 -pubkey -noout | openssl pkey -pubin -outform der \
| openssl dgst -sha256 -binary | openssl enc -base64
هدر Public-Key-Pins بهخاطر ریسک خودتخریبی — سایتی که کلیدش را گم میکند و تا پایان max-age برای همه غیرقابلدسترس میشود — از Chrome 72 حذف شد؛ جانشین آن Expect-CT هم پس از اجباریشدن CT در Chrome 107 حذف شد. برای وب پاسخ صنعت Certificate Transparency است: هر گواهی عمومی باید در لاگهای append-only ثبت شود و رسیدهای SCT را داخل خودش داشته باشد، و صاحب دامنه میتواند صدور غیرمجاز را ظرف چند دقیقه ببیند.
pinning هنوز در اپ موبایل و کلاینت ماشینبهماشین معنا دارد چون هر دو سر را کنترل میکنی. حتی آنجا: همیشه حداقل دو pin (کلید فعلی + پشتیبان آفلاین) و یک مسیر بهروزرسانی سریع، وگرنه pinning فقط بمب ساعتی روز چرخش است.
ACME (RFC 8555) صدور را کاملاً خودکار میکند: کلاینت اثبات میکند دامنه را کنترل میکند و CA گواهی میدهد — بدون انسان و بدون فرم.
ACME issuance with HTTP-01 challenge:
sequenceDiagram
participant A as ACME client (certbot)
participant CA as ACME server (Let's Encrypt)
participant W as Your web server
A->>CA: newOrder for api.example.com
CA-->>A: challenge token + authorization URL
A->>W: publish token at /.well-known/acme-challenge/TOKEN
A->>CA: ready to be validated
CA->>W: GET http://api.example.com/.well-known/acme-challenge/TOKEN
W-->>CA: token + account key thumbprint
CA-->>A: authorization valid
A->>CA: finalize with CSR
CA-->>A: certificate + chain
| challenge | چطور اثبات میکند | محدودیت |
|---|---|---|
HTTP-01 |
فایلی روی پورت ۸۰ در /.well-known/acme-challenge/ |
پورت ۸۰ باید از اینترنت در دسترس باشد؛ wildcard نمیدهد |
DNS-01 |
رکورد TXT روی _acme-challenge.<domain> |
نیاز به API ارائهدهندهی DNS؛ تنها راه wildcard |
TLS-ALPN-01 |
گواهی ویژه روی پورت ۴۴۳ با ALPN acme-tls/1 |
پورت ۴۴۳ اختصاصی؛ مناسب reverse proxy |
# صدور با webroot (سرویس در حال اجرا میماند)
certbot certonly --webroot -w /var/www/html \
-d api.example.com -d www.api.example.com \
--agree-tos -m ops@example.com --non-interactive
# wildcard فقط با DNS-01
certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cf.ini \
-d "*.example.com" -d example.com
# گواهی ۶ روزه (پروفایل shortlived — از Certbot 4.0)
certbot certonly --preferred-profile shortlived --webroot -w /var/www/html -d api.example.com
# تست تجدید بدون مصرف rate limit، و reload بعد از هر تجدید موفق
certbot renew --dry-run
certbot renew --deploy-hook "systemctl reload nginx"
شایعترین حادثهی ACME این نیست که تجدید شکست بخورد؛ این است که تجدید موفق شود ولی nginx/JVM هرگز فایل جدید را نخواند و تا لحظهی انقضا گواهی قدیمی را سرو کند. همیشه --deploy-hook (یا reload-on-update یا rollout) داشته باش و انقضا را از بیرون و روی پورت واقعی مانیتور کن نه با خواندن فایل روی دیسک.
هنگام ساخت اتوماسیون همیشه اول روی staging کار کن — Let's Encrypt محدودیت نرخ دارد و خطاهای مکرر تو را قفل میکند:
certbot certonly --server https://acme-staging-v02.api.letsencrypt.org/directory ...
بخش ۱۱ — چرخش گواهی در Kubernetes
الگوی استاندارد cert-manager است: یک controller که Issuer/ClusterIssuer و Certificate را میفهمد، گواهی میگیرد، در یک Secret از نوع kubernetes.io/tls مینویسد و قبل از انقضا خودکار تجدید میکند.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@example.com
profile: tlsserver # انتخاب پروفایل ACME — از cert-manager v1.18
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
ingressClassName: nginx
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: api-tls
namespace: production
spec:
secretName: api-tls # اینجا tls.crt و tls.key نوشته میشوند
duration: 2160h # 90d
renewBefore: 720h # 30d قبل از انقضا تجدید کن
privateKey:
algorithm: ECDSA
size: 256
rotationPolicy: Always # با هر تجدید کلید جدید بساز
dnsNames:
- api.example.com
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
تا cert-manager v1.17 مقدار پیشفرض rotationPolicy برابر Never بود — گواهی تجدید میشد ولی همان کلید خصوصی قدیمی برای همیشه میماند. از v1.18 پیشفرض Always است، پس در هر تجدید هم کلید و هم گواهی عوض میشوند.
cert-manager rotation and pod reload:
flowchart LR
C["Certificate CR<br/>renewBefore: 30d"] --> CM["cert-manager controller"]
CM -->|ACME order| CA["Let's Encrypt"]
CA -->|cert + chain| CM
CM -->|writes| S["Secret kubernetes.io/tls<br/>tls.crt, tls.key"]
S -->|projected volume| P["Pod filesystem /etc/tls"]
P -->|file watcher / SIGHUP| APP["App reloads SSL context"]
S -.->|read directly| ING["Ingress controller"]
kubelet فایلهای Secretِ mountشده را بهروز میکند (با تأخیری تا حدود یک دقیقه)، ولی این کار را با یک symlink swap انجام میدهد نه با نوشتن روی همان inode — پس watcherهایی که فقط inotify روی خود فایل میگذارند تغییر را نمیبینند. سه راه: کتابخانهای که symlink را دنبال میکند (مثل reload-on-update در Spring Boot)؛ یک hook که با تغییر Secret به فرایند SIGHUP میدهد؛ یا سادهترین و قابلاعتمادترین، rollout در زمان تجدید با annotationی که hash گواهی را دارد.
در service meshها گواهیهای workload عمر بسیار کوتاهی دارند (ساعتی یا کمتر) و در حافظهی sidecar میمانند — هرگز روی دیسک نمیروند. هویت بهجای نام DNS یک SPIFFE ID مثل spiffe://cluster.local/ns/production/sa/api است که در SAN از نوع URI قرار میگیرد. معماری mesh در فصل runtime میکروسرویسها آمده؛ نکتهی این فصل این است که همان زنجیرهی X.509 پشت آن است.
بخش ۱۲ — GPG/PGP: همان مسئله، فلسفهی متفاوت
در دنیای X.509 برای معتبر شدن امضایت باید به دفترخانه (CA) بروی و همه به دفترخانه اعتماد دارند. در دنیای PGP دفترخانهای وجود ندارد: تو مهر شخصی خودت را میسازی و دوستانت با مهر خودشان پای مهر تو را امضا میکنند. اگر من به کسی اعتماد دارم و او مهر تو را امضا کرده، من هم میتوانم به مهر تو اعتماد کنم. به این شبکهی افقی میگویند web of trust.
PGP یک استاندارد است (OpenPGP) و GnuPG/gpg رایجترین پیادهسازی آزادش؛ شاخهی جدید در حال افزودن رمزنگاری پساکوانتومی (Kyber/ML-KEM) است.
| جنبه | X.509 / TLS | OpenPGP / GPG |
|---|---|---|
| مدل اعتماد | سلسلهمراتبی؛ CA ریشه | افقی؛ web of trust یا اعتماد دستی |
| هویت | نام دامنه (SAN) | نام و ایمیل (User ID) |
| کاربرد اصلی | اتصال زنده (transport) | داده در سکون: فایل، ایمیل، artifact، commit |
| ابطال | CRL/OCSP از سمت CA | revocation certificate که خود صاحب کلید منتشر میکند |
| انقضا | اجباری و کوتاه | اختیاری؛ بدون تغییر کلید تمدیدشدنی |
| ساختار کلید | یک کلید در هر گواهی | یک primary + چند subkey با نقشهای متفاوت |
چون TLS فقط کانال را محافظت میکند نه محتوا را. وقتی یک .tar.gz از یک آینه دانلود میکنی، HTTPS فقط میگوید «این بایتها دستنخورده از آن سرور آمدند» — نه اینکه «این بایتها را واقعاً تیم اصلی ساخته است». امضای GPG روی artifact همین شکاف را پر میکند، و به همین دلیل تقریباً تمام توزیعهای لینوکس و مخازن بسته و releaseهای مهم به آن وابستهاند. تفاوت channel security و content security سؤال متداول مصاحبههای Platform است.
ساختار کلید: primary و subkey
flowchart TD
P["Primary key (Ed25519)<br/>usage: Certify — keep offline"]
S["Signing subkey<br/>usage: Sign"]
E["Encryption subkey (Curve25519)<br/>usage: Encrypt"]
A["Authentication subkey<br/>usage: Auth (SSH)"]
U["User IDs — Name and email"]
P -->|certifies| S
P -->|certifies| E
P -->|certifies| A
P -->|binds| U
primary key هویت توست؛ با آن User IDها و subkeyها را تأیید میکنی و کلید دیگران را امضا میکنی. subkeyها کلیدهای کاری روزمرهاند. مزیت بزرگ: اگر لپتاپ را گم کنی فقط subkeyها را ابطال و جایگزین میکنی و هویت و تمام امضاهای گرفتهشده روی primary باقی میماند. الگوی senior: primary را offline نگه دار (درایو رمزشده یا کارت سختافزاری) و روی ماشین کاری فقط subkeyها را داشته باش — gpg آن حالت را با sec# نشان میدهد.
# ساخت کلید؛ future-default امروز یعنی ed25519 برای امضا + cv25519 برای رمز
gpg --quick-generate-key "Jane Dev <jane@example.com>" future-default default 2y
gpg --quick-add-key <FINGERPRINT> ed25519 sign 1y # subkey امضای مجزا
gpg --list-keys --keyid-format=long --with-subkey-fingerprints
gpg --list-secret-keys --keyid-format=long
gpg --armor --export jane@example.com > jane-public.asc # برای دادن به دیگران
gpg --armor --export-secret-subkeys jane@example.com > subs.asc # پشتیبان کاری
gpg --import colleague-public.asc
gpg --quick-set-expire <FINGERPRINT> 2y '*' # تمدید همهی subkeyها
اگر کلید GPG منقضی شود لازم نیست کلید جدید بسازی: کافی است تاریخ انقضا را جلو ببری و کلید عمومی بهروز را دوباره منتشر کنی — تمام امضاها و اعتماد ساختهشده حفظ میشود. بزرگترین اشتباه تازهکارها این است که با دیدن expired یک کلید کاملاً جدید میسازند و کل web of trust خود را دور میریزند. تاریخ انقضا اینجا یک کلید مردهرسان است: اگر روزی دسترسی را از دست بدهی، خودبهخود از اعتبار میافتد.
رمزکردن، امضا و تأیید
# رمزکردن برای چند گیرنده + خودت (وگرنه خودت هم نمیتوانی بازش کنی)
gpg --encrypt -r bob@example.com -r audit@example.com -r jane@example.com \
--output report.pdf.gpg report.pdf
gpg --decrypt report.pdf.gpg > report.pdf
gpg --symmetric --cipher-algo AES256 backup.tar # رمز متقارن با passphrase
gpg --clear-sign announcement.txt # امضای متنی خوانا
# امضای جدا (detached) — استاندارد release artifact
gpg --armor --detach-sign app-1.4.0.tar.gz # → app-1.4.0.tar.gz.asc
gpg --verify app-1.4.0.tar.gz.asc app-1.4.0.tar.gz
امضای detached استاندارد release است چون خود فایل دستنخورده میماند: همان .tar.gz که هر ابزاری میخواند، بهعلاوهی یک .asc کوچک کنارش. اگر امضا را داخل فایل ببری، همه مجبورند برای رسیدن به محتوا از gpg عبور کنند.
خط Good signature فقط میگوید امضا با آن کلید عمومی جور است. هشدار not certified with a trusted signature یعنی «ولی نمیدانم آن کلید عمومی مال آن پروژه است». مهاجمی که آینهی دانلود را در اختیار دارد میتواند فایل، امضا و کلید عمومی خودش را بگذارد و تو Good signature ببینی. تنها راه بستن حلقه: اثر انگشت کامل ۴۰ رقمی را از کانالی مستقل از محل دانلود بگیری و بعد کلید را محلی امضا کنی:
gpg --keyserver hkps://keys.openpgp.org --recv-keys <FULL_40_HEX_FINGERPRINT>
gpg --fingerprint <FULL_40_HEX_FINGERPRINT> # با منبع مستقل مقایسه کن
gpg --lsign-key <FINGERPRINT> # امضای محلی، منتشر نمیشود
هرگز به key ID کوتاه (۸ یا ۱۶ رقم) اعتماد نکن؛ ساختن کلید با key ID کوتاهِ یکسان عملی و ارزان است.
keyserver، WKD و ابطال
keyserverهای قدیمی SKS اجازه میدادند هرکسی هر امضایی به هر کلیدی اضافه کند و چیزی هرگز حذف نشود — که به «key poisoning» و کلیدهایی با دهها هزار امضای بیربط منجر شد؛ آن شبکه امروز متروک است. keys.openpgp.org مدل جدید است: اطلاعات هویتی فقط با تأیید ایمیل و رضایت صاحب کلید قابل جستوجو میشود و امضاهای شخص ثالث حذف میشوند. WKD کلید عمومی را مستقیماً از دامنهی خود شخص روی HTTPS در /.well-known/openpgpkey/... سرو میکند — یعنی PGP اعتماد را از PKI قرض میگیرد.
gpg --keyserver hkps://keys.openpgp.org --send-keys <FINGERPRINT>
gpg --locate-external-keys jane@example.com # از WKD و keyserver
gpg --refresh-keys # بهروزرسانی ابطالها و انقضاها
اگر کلید خصوصی را گم کنی یا passphrase را فراموش کنی، دیگر نمیتوانی گواهی ابطال بسازی و کلید مردهات برای همیشه معتبر میماند. gpg 2.x هنگام ساخت کلید خودکار یکی در ~/.gnupg/openpgp-revocs.d/<FPR>.rev میسازد؛ آن را چاپ کن یا جدا از کلید در جای امن نگه دار. و توجه: هرکس آن فایل را داشته باشد میتواند کلیدت را ابطال کند — یک DoS ساده روی هویت تو.
gpg --output revoke.asc --gen-revoke <FINGERPRINT> # امروز بساز، نه روز حادثه
gpg --import revoke.asc && gpg --keyserver hkps://keys.openpgp.org --send-keys <FINGERPRINT>
امضای commit و tag در گیت
git config --global user.signingkey <SIGNING_SUBKEY_FPR>! # ! یعنی «دقیقاً همین subkey»
git config --global commit.gpgsign true
git config --global tag.gpgsign true
export GPG_TTY=$(tty) # وگرنه passphrase پرسیده نمیشود
git commit -S -m "Fix token refresh race"
git tag -s v1.4.0 -m "Release 1.4.0"
git verify-commit HEAD && git verify-tag v1.4.0
git log --pretty="%h %G? %GS %s" -10 # %G? = G(ood) B(ad) U(ntrusted) N(one) E(rror)
از Git 2.34 میتوانی با git config --global gpg.format ssh و user.signingkey ~/.ssh/id_ed25519.pub از کلید SSH استفاده کنی؛ سادهتر است ولی مدل انقضا و ابطال ندارد و تأیید به فایل gpg.ssh.allowedSignersFile وابسته است. برای امضایی که باید سالها بعد قابل تأیید باشد، OpenPGP پختهتر است.
توجه کن که امضا نویسنده را اثبات نمیکند بلکه امضاکننده را: فیلد Author در گیت متن آزاد است و هرکسی هر چیزی میتواند در آن بنویسد. سیاست معنادار یعنی branch protection که امضای تأییدشده را الزامی کند بهعلاوهی فهرست نگهداریشدهی کلیدهای مجاز؛ وگرنه امضا فقط یک آیکون سبز تزئینی است.
برگهی تقلب gpg
| کار | فرمان |
|---|---|
| ساخت کلید تعاملی | gpg --full-generate-key |
| ساخت سریع | gpg --quick-generate-key "Name <mail>" future-default default 2y |
| افزودن subkey | gpg --quick-add-key <FPR> ed25519 sign 1y |
| فهرست کلیدها | gpg --list-keys --keyid-format=long · gpg --list-secret-keys |
| اثر انگشت | gpg --fingerprint <FPR> |
| صادرکردن عمومی/خصوصی | gpg --armor --export <FPR> · gpg --armor --export-secret-keys <FPR> |
| واردکردن | gpg --import pub.asc |
| رمزکردن / رمزگشایی | gpg -e -r bob@example.com -o f.gpg f · gpg -d f.gpg > f |
| رمز متقارن | gpg -c --cipher-algo AES256 f |
| امضای جدا / متنی | gpg --armor --detach-sign f · gpg --clear-sign f |
| تأیید | gpg --verify f.asc f |
| امضای محلی کلید | gpg --lsign-key <FPR> |
| تنظیم سطح اعتماد | gpg --edit-key <FPR> سپس trust |
| تمدید انقضا | gpg --quick-set-expire <FPR> 2y '*' |
| ساخت گواهی ابطال | gpg --output rev.asc --gen-revoke <FPR> |
| ارسال/دریافت keyserver | gpg --keyserver hkps://keys.openpgp.org --send-keys <FPR> · --recv-keys <FPR> |
| یافتن از WKD | gpg --locate-external-keys mail@example.com |
| حذف کلید | gpg --delete-keys <FPR> · gpg --delete-secret-keys <FPR> |
| اجرای غیرتعاملی در CI | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 ... |
در pipeline نه ترمینالی هست نه انسانی که passphrase بزند. الگوی امن: یک subkey امضای مخصوص CI با عمر کوتاه بساز (نه primary)، آن را در secret manager بگذار، در job واردش کن، و در پایان GNUPGHOME موقت را پاک کن. اگر نشت کند فقط همان subkey را ابطال میکنی.
export GNUPGHOME=$(mktemp -d)
printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import
echo "$GPG_PASSPHRASE" | gpg --batch --yes --pinentry-mode loopback \
--passphrase-fd 0 --armor --detach-sign dist/app-1.4.0.tar.gz
rm -rf "$GNUPGHOME"
بخش ۱۳ — دفترچهی عملیاتی senior
- inventory داشته باش. فهرست هر گواهی، مالکش، محل استقرارش و تاریخ انقضایش. اگر نمیتوانی در ۳۰ ثانیه بگویی کدام گواهی زودتر از همه منقضی میشود، انقضا تو را غافلگیر خواهد کرد.
- از بیرون مانیتور کن. probeای که به پورت واقعی وصل میشود هم انقضا را میگیرد هم «تجدید شد ولی reload نشد» را — که خطای شایعتری است.
- دو آستانهی هشدار: نرم در ۳۰٪ عمر باقیمانده (تیکت)، سخت در ۱۰٪ (paging). با گواهی ۴۷ روزه یعنی ۱۴ و ۵ روز.
- کلید خصوصی از repo بیرون. اگر کلیدی حتی یکبار در تاریخچهی گیت رفت، سوخته است؛ باید ابطال و جایگزین شود، نه اینکه commit را پاک کنی.
- چرخش را تمرین کن. فصلی یکبار در staging، CA را عوض کن. تیمی که هرگز چرخش CA را تمرین نکرده، در روز حادثه آن را برای اولین بار انجام میدهد.
- زمان را جدی بگیر. container بدون NTP دقیقاً همان خطاهایی را میسازد که شبیه مشکل گواهیاند؛
date -uاولین دستور هر debug مربوط به TLS است. - الگوریتمها را جلو ببر. امروز ECDSA P-256 یا Ed25519 برای گواهیهای جدید، TLS 1.2 کف و 1.3 پیشفرض. مسیر بعدی رمزنگاری پساکوانتومی است: تبادل کلید ترکیبی
X25519MLKEM768از OpenSSL 3.5 (آوریل ۲۰۲۵) پیشفرض فعال است و JDK از نسخهی ۲۴ پیادهسازیML-KEMوML-DSAرا دارد. «برداشت امروز، رمزگشایی فردا» تهدیدی واقعی برای دادهی بلندعمر است.
بخش ۱۴ — پرسشهای مصاحبه
یک ساختار X.509 که یک کلید عمومی را به مجموعهای از نامها در SAN گره میزند، و یک CA پای این ادعا را امضا کرده است. گواهی هیچ چیز محرمانهای ندارد و کاملاً عمومی است.
اثبات میکند: «دارندهی کلید خصوصی متناظر، در زمان صدور کنترل این نامها را داشته و یک CA این را تأیید کرده». اثبات نمیکند که سایت بدافزار ندارد، شرکت قانونی است، یا دادهات آنطرف امن نگه داشته میشود. برای گواهیهای DV — یعنی اکثریت مطلق اینترنت — تنها چیز تأییدشده «کنترل دامنه» است.
اول تفکیک میکنم که مشکل زنجیرهی ناقص سرور است یا truststore ناقص کلاینت، با openssl s_client -connect host:443 -servername host -showcerts. اگر فقط یک بلوک CERTIFICATE دیدم، سرور intermediate نمیفرستد → راهحل جایگزینی cert.pem با fullchain.pem و reload.
اگر زنجیره کامل بود ولی openssl هم unable to get local issuer داد، root در سیستم نیست. اگر openssl و curl موفق بودند و فقط جاوا شکست خورد، تقریباً همیشه یعنی CA در cacerts نیست — چون JVM از trust store سیستمعامل استفاده نمیکند. تأیید با -Djava.security.debug=certpath و سپس import در truststore اختصاصی اپلیکیشن، نه دستکاری دستی cacerts که در rollout بعدی از بین میرود.
پرچم قرمز در پاسخ داوطلب: «یک TrustManager مینویسم که همه را قبول کند».
keystore هویت خودت است: گواهی بههمراه کلید خصوصی. truststore فهرست CAهایی است که باور داری و هیچ کلید خصوصی ندارد. از نظر فرمت هر دو میتوانند یک فایل PKCS12 باشند؛ تفاوت در نقش است نه در نوع فایل.
کلاینت HTTPS ساده فقط truststore لازم دارد (اغلب cacerts پیشفرض کافی است)؛ سرور TLS حتماً keystore دارد؛ در mTLS هر دو طرف هر دو را دارند. اشتباه رایج: ریختن CAها در همان keystoreای که کلید سرور در آن است — کار میکند ولی چرخش کلید سرور و چرخش فهرست CAها دو چرخهی کاملاً متفاوتاند و باید جدا بمانند.
سرعت: handshake از 2-RTT به 1-RTT رسید چون کلاینت گروه کلید را حدس میزند و key_share را از قبل در ClientHello میفرستد؛ با PSK حتی 0-RTT ممکن است.
امنیت با حذف: RSA key transport، DH ایستا، renegotiation، فشردهسازی و تمام suiteهای غیر AEAD حذف شدند؛ تعداد suiteها از صدها به پنج رسید و مذاکرهی الگوریتم امضا از suite جدا شد. دستهای کامل از حملات (ROBOT، CRIME، Logjam، FREAK) دیگر قابل اجرا نیستند.
حریم خصوصی: از ServerHello به بعد همهچیز رمز است، از جمله گواهی سرور؛ در 1.2 گواهی در متن ساده میرفت. تنها چیز آشکار باقیمانده SNI است که ECH در حال حل کردنش است.
در suiteهای TLS_RSA_WITH_... کلاینت یک pre-master secret را با کلید عمومی گواهی رمز میکرد؛ یعنی کل امنیت session به کلید بلندمدت سرور گره خورده بود. هرکس ترافیک را امروز ضبط کند و سالها بعد آن کلید را به دست بیاورد، همهی ترافیک گذشته را رمزگشایی میکند.
با ECDHE هر session جفتکلید موقت خودش را دارد که هرگز روی سیم نمیرود و بعد دور ریخته میشود؛ کلید بلندمدت فقط برای امضا بهکار میرود نه انتقال راز. نکتهی عملی: JDK 25 و بهروزرسانی 21.0.10 مجموعهی TLS_RSA_* را پیشفرض غیرفعال کردهاند و همین باعث شکست ناگهانی اتصال به سیستمهای قدیمی با no cipher suites in common میشود.
CRL فهرست امضاشدهی serialهای ابطالشده است — حجیم و همیشه کمی کهنه. OCSP یک پرسوجوی زنده برای یک گواهی است — تازهتر، ولی یک رفتوبرگشت در مسیر بحرانی میگذارد و به CA میگوید چه کسی چه سایتی را میبیند. stapling همان پاسخ را به سرور میسپارد تا در handshake ضمیمه کند: بدون تأخیر و بدون افشای حریم خصوصی.
مشکل ریشهای هر سه: کلاینتها soft-fail هستند، پس مهاجم فقط کافی است پاسخ را مسدود کند. توصیه: stapling را روشن کن چون ارزان است، ولی بهعنوان کنترل امنیتی جدی به آن تکیه نکن. پاسخ واقعی صنعت عمر کوتاه است — Let's Encrypt سرویس OCSP خود را در اوت ۲۰۲۵ تعطیل کرد و گواهیهای ۶ روزهاش اصلاً اطلاعات ابطال ندارند.
CN بخشی از Distinguished Name و صرفاً یک رشته است؛ subjectAltName یک extension با نوع مشخص (DNS, IP, URI) و چندمقداری است. از حدود ۲۰۱۷ مرورگرها CN را برای اعتبارسنجی نادیده میگیرند و CAهای عمومی موظفاند هر نام موجود در CN را در SAN هم تکرار کنند.
wildcard فقط یک برچسب را پوشش میدهد: *.example.com با api.example.com میخورد، با a.b.example.com نه، و با خود example.com هم نه — apex باید جداگانه در SAN باشد. در ACME هم wildcard فقط با DNS-01 صادر میشود. ریسک معماریاش: یک کلید خصوصی مشترک روی هر ماشینی که هر زیردامنهای را سرو میکند.
مناسب است وقتی هر دو سر را کنترل میکنی و تعداد کلاینتها اتوماسیونپذیر است: سرویسبهسرویس داخل خوشه، اتصال اپ به DB و broker، و شرکای B2B مشخص. نامناسب است برای کاربران مرورگری و APIهای عمومی با هزاران مصرفکننده، چون توزیع و چرخش گواهی روی دستگاههای خارج از کنترل تو عملی نیست.
mTLS احراز هویت لایهی انتقال است نه مجوز؛ اینکه بدانی طرف مقابل payment-service است چیزی دربارهی دسترسیاش نمیگوید. دام عملیاتی اصلی چرخش است: اگر همهی گواهیها همزمان صادر شوند همزمان هم میمیرند، و چرخش CA به پنجرهی گذاری نیاز دارد که هر دو طرف هر دو CA را در truststore داشته باشند.
مهار: سریعترین مسیر بازیابی معمولاً صدور فوری گواهی جدید از همان CA و reload سرویس است. اگر CA در دسترس نیست، یک گواهی کوتاهعمر از CA داخلی بهتر از خاموش کردن اعتبارسنجی TLS در کلاینتهاست — آن کار یک قطعی را به یک حادثهی امنیتی تبدیل میکند.
تشخیص: چرا مانیتورینگ هشدار نداد؟ معمولاً یا اصلاً نبود، یا فایل روی دیسک را میخواند در حالی که سرویس گواهی دیگری را سرو میکرد.
پیشگیری: اتوماسیون صدور، reload خودکار پس از تجدید، probe خارجی روی پورت واقعی با دو آستانه، مالک مشخص برای هر گواهی، و یک تست بازیابی فصلی. نکتهای که پاسخ را senior میکند: با روند رسیدن عمر گواهی به ۴۷ روز تا ۲۰۲۹، فرایند دستی از نظر ریاضی قابل دوام نیست.
با استفاده از PSKای که از session قبلی مانده، کلاینت دادهی برنامه را همراه اولین پیام handshake میفرستد و صفر رفتوبرگشت تأخیر میدهد.
خطرش این است که این داده در برابر replay محافظتشده نیست، چون هنوز هیچ تبادل تازهای یکتایی پیام را تضمین نکرده؛ مهاجم میتواند بسته را بارها بازپخش کند. قاعده: فقط برای درخواستهای idempotent، عملاً GETهای قابل cache. سرور باید early data را علامت بزند (در HTTP هدر Early-Data: 1) و اپلیکیشن هر درخواست غیر idempotent را با 425 Too Early رد کند تا کلاینت آن را روی اتصال کامل تکرار کند.
pinning یعنی محدود کردن اعتماد به یک کلید یا CA مشخص بهجای کل trust store، برای دفاع در برابر صدور نادرست توسط یک CA معتبر.
در وب اشتباه است: HPKP از Chrome 72 حذف شد چون یک اشتباه پیکربندی، سایت را تا پایان max-age برای همه غیرقابلدسترس میکرد. جانشین آن Certificate Transparency است — لاگهای عمومی append-only که صدور غیرمجاز را قابل کشف میکنند بدون ریسک خودتخریبی.
در اپ موبایل و کلاینت ماشینبهماشین هنوز منطقی است چون هر دو سر را کنترل میکنی، به سه شرط: pin روی کلید عمومی نه گواهی، حداقل دو pin (فعلی + پشتیبان آفلاین)، و مسیر بهروزرسانی سریع. بدون اینها pinning فقط یک بمب ساعتی برای روز چرخش کلید است.
مدل اعتماد. X.509 سلسلهمراتبی است: تعداد محدودی root CA در trust store و اعتماد از بالا به پایین. مقیاسپذیر و خودکار، ولی هر CA یک نقطهی شکست است و یک CA دچار نفوذ میتواند برای هر دامنهای گواهی صادر کند. OpenPGP افقی است: هیچ مرکزی وجود ندارد و هر کاربر خودش تصمیم میگیرد کدام کلید را باور کند، اغلب بر پایهی امضای کسانی که به آنها اعتماد دارد. نقطهی شکست مرکزی ندارد ولی onboarding و کشف کلید سخت است — و به همین دلیل در وب هرگز نگرفت.
تفاوت کاربردی: TLS داده در حال حرکت را محافظت میکند و GPG داده در حال سکون را — فایل، ایمیل، artifact، commit — با امضایی که سالها بعد و مستقل از هر کانالی قابل تأیید است.
کافی نیست. Good signature فقط میگوید امضا با کلید عمومی موجود در keyring من جور است، نه اینکه آن کلید مال پروژه است؛ مهاجمی که آینهی دانلود را دارد فایل، امضا و کلید خودش را میگذارد.
روال درست: (۱) اثر انگشت کامل ۴۰ رقمی کلید انتشار را از کانالی مستقل از محل دانلود بگیر، (۲) کلید را وارد کن و با gpg --fingerprint مقایسه کن، (۳) gpg --verify file.asc file را اجرا کن و هشدار «کلید تأیید نشده» را آگاهانه با --lsign-key برطرف کن. هرگز به key ID کوتاه ۸ یا ۱۶ رقمی اعتماد نکن.
همین منطق دربارهی امضای commit هم صدق میکند: امضا فقط امضاکننده را اثبات میکند، نه فیلد Author را که متن آزاد است. بدون branch protection و فهرست نگهداریشدهی کلیدهای مجاز، امضا فقط یک نشان سبز تزئینی است.
گواهی چیزی جز یک ادعای امضاشده نیست: «این کلید عمومی متعلق به این نامهاست». تمام PKI ماشینی است برای دنبال کردن آن امضا تا یک root مورد اعتماد؛ و بیشتر خرابیهای واقعی نه در رمزنگاری، بلکه در زنجیرهی ناقص، تاریخ انقضا و trust store اشتباه رخ میدهند.
TLS همان اعتماد را به یک اتصال زنده تبدیل میکند: نسخهی 1.3 با یک رفتوبرگشت، forward secrecy اجباری و حذف کامل الگوریتمهای شکسته. mTLS همان مکانیزم را دوطرفه میکند و برای هویت workload عالی است — به شرطی که چرخش را حل کرده باشی.
در جاوا فقط دو مفهوم را نگه دار: keystore هویت من، truststore کسانی که باور دارم. PKIX path building failed تقریباً همیشه یعنی یکی از این دو ناقص است، و پاسخش هرگز یک TrustManager بیقید نیست.
روند صنعت روشن است: عمر گواهیها به سمت ۴۷ روز و کمتر میرود، ابطال جای خود را به انقضای سریع میدهد، و هر چیز دستی بهزودی میشکند. اتوماسیون صدور، reload خودکار و مانیتورینگ از بیرون سه ستون بقا هستند. و در کنار همهی اینها GPG لایهای را پوشش میدهد که TLS هرگز نمیپوشاند: اعتماد به محتوا مستقل از کانال — امضای release، تأیید artifact، و امضای commit و tag.
Behind that little padlock next to a URL sits a multi-layer trust machine that most engineers only know one layer deep. Nobody asks questions while it works — but the moment this line shows up in a production log:
PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException:
unable to find valid certification path to requested target
the difference between a mid and a senior engineer becomes visible. One copies a TrustManager that "accepts everything"; the other knows within thirty seconds which link of the chain is missing.
This chapter takes the machine apart from the first screw: what a certificate actually is, the handshake, the daily tooling, automated rotation in Kubernetes, and finally the parallel universe of PGP/GPG, which solves the same problem with a completely different philosophy.
- Foundations: public/private keys, hashing, signatures. 2. The certificate: X.509 fields, SAN, EKU. 3. Chain of trust: root, intermediate, leaf, trust stores. 4. Issuance lifecycle: CSR, expiry, the global shortening of certificate lifetimes. 5. Revocation: CRL, OCSP, stapling. 6. TLS: 1.2 vs 1.3, cipher suites, forward secrecy, SNI, ALPN, resumption, 0-RTT. 7. mTLS. 8. Java: keystore/truststore, keytool, cacerts,
SSLContext, the classic errors. 9. openssl cookbook. 10. Operations: pinning, CT, ACME, Kubernetes. 11. GPG: web of trust, subkeys, signing artifacts and commits, keyservers, revocation.
Part 1 — Three foundations
A mailbox with a slot in the door: anyone can drop a letter in (public key), but only the holder of the metal key can open it (private key). Now invert it: if the owner has a personal seal that only they possess, anyone who sees the seal can confirm the letter came from them — without being able to forge it. The first is encryption, the second is signing.
Asymmetric cryptography means a key pair where the private key cannot be derived from the public one. Encrypt with the public key so only the private key opens it; sign with the private key so anyone can verify with the public one. Today's algorithms: RSA (slow, large keys, still everywhere), ECDSA on P-256/secp256r1 (fast, small), and Ed25519 (the most modern; the new OpenPGP default).
Hashing is a one-way function turning any input into a fixed-length fingerprint (SHA-256 = 256 bits). Flipping one input bit changes the whole output, and finding two inputs with the same hash (a collision) must be infeasible. MD5 and SHA-1 are both broken; that is why the JDK has disabled SHA-1 handshake signatures by default since version 25.
A digital signature = hash the document, "encrypt" that hash with the private key, put the result next to the document. Verification = hash it yourself, open the signature with the public key, compare. Equal hashes give you both integrity and authenticity.
All of PKI and all of GPG solve exactly one sentence: "whose public key is this really?" PKI answers "because a trusted authority vouched for it"; PGP answers "because people I trust vouched for it".
Part 2 — What a certificate really is
A passport is a piece of paper claiming "this photo and this name belong to a real person", with a government stamp on it. The border officer does not trust you; they trust the stamp, because they have a reference copy. A TLS certificate is exactly that: a document saying "this public key belongs to api.example.com", with a CA's signature on it.
A certificate is a standard X.509 v3 structure encoded with ASN.1/DER. Base64-wrap that DER between -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- and you get PEM — what you see inside .crt and .pem files.
| Field | Meaning | Practical note |
|---|---|---|
Serial Number |
Unique ID at the issuer | Needed to revoke or escalate to the CA |
Signature Algorithm |
e.g. sha256WithRSAEncryption |
If you see SHA-1, the certificate is dead |
Issuer |
Issuer DN | Must match the Subject of the certificate above it |
Validity |
notBefore / notAfter |
The source of most production incidents |
Subject |
Holder DN (CN=...) |
No longer used for hostname matching |
SubjectPublicKeyInfo |
The public key plus its algorithm | The thing actually being signed |
Subject Alternative Name |
The real list of names/IPs | The only source of truth for hostnames |
Basic Constraints |
CA:TRUE/FALSE and pathlen |
What separates a CA from a leaf |
Key Usage |
digitalSignature, keyCertSign … |
What the key may be used for |
Extended Key Usage |
serverAuth, clientAuth, codeSigning |
mTLS does not work without clientAuth |
AKI / SKI |
Issuer key ID / own key ID | How the chain is assembled |
CRL Distribution Points |
Where the revocation list lives | May be absent entirely |
Authority Information Access |
OCSP URL and issuer certificate URL | caIssuers can rescue an incomplete chain |
SCT List |
Certificate Transparency receipts | Browsers reject public certificates without them |
# Fetch a server's certificate and read all of it
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -text
# Only what you need 90% of the time
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
For years the domain went into Subject CN. Since roughly 2017 every major browser completely ignores CN for hostname matching and reads only subjectAltName. If you mint an internal certificate with just -subj "/CN=api.internal", browsers, modern curl and modern Java clients will reject it with a hostname mismatch. Always add -addext "subjectAltName=DNS:api.internal" as well.
A self-signed certificate means Issuer == Subject. Cryptographically it is perfectly valid and TLS works fine with it; the only problem is that nobody has a reason to trust it unless it is manually added to a trust store. Teams mint self-signed certificates for internal services and add an exception in each client; three months later they have forty certificates with no way to revoke or rotate anything. The right move is one internal CA: distribute its root once into every trust store, then issue a leaf per service — after that, rotating leaves requires zero client changes.
Part 3 — The chain of trust
A CEO does not sign every small contract; they delegate to deputies who sign instead. The counterparty sees the deputy's signature, checks the deputy's power of attorney, and arrives at the CEO's signature, of which they hold a reference copy. The root CA is the CEO, the intermediate is the deputy, and the server certificate is the contract.
- Root CA: self-signed, key in an offline HSM, 15–25 year lifetime, and present in the OS/browser/JDK trust store.
- Intermediate CA: signed by the root, online, issuing millions of certificates. A compromise burns only the intermediate, not the root.
- Leaf: your server certificate,
CA:FALSE, unable to sign anything.
Chain of trust from root to leaf — زنجیره از root تا برگ:
flowchart TD
R["Root CA<br/>self-signed, offline HSM<br/>in OS/JDK trust store"]
I["Intermediate CA<br/>online, signs millions<br/>sent by the server"]
L["Leaf certificate<br/>SAN = api.example.com<br/>CA:FALSE"]
K["Server private key<br/>never leaves the host"]
R -->|signs| I
I -->|signs| L
L -.-> K
Golden rule: the server must send the leaf plus every intermediate, and must not send the root. The client uses AKI/Issuer to build the chain until it reaches a root in its own trust store.
The server sends only the leaf. It works in a desktop browser (which cached the intermediate or fetches it via AIA) but breaks in curl, in Java, in mobile apps and in backend services. The symptoms are unable to get local issuer certificate from openssl and PKIX path building failed from Java. The definitive test:
openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts </dev/null
# You should see two or more CERTIFICATE blocks and, at the end: Verify return code: 0 (ok)
The fix: in nginx, ssl_certificate must point at the fullchain, not just cert.pem.
| Environment | Default trust store |
|---|---|
| Debian/Ubuntu | /etc/ssl/certs/ca-certificates.crt (update-ca-certificates) |
| RHEL/Fedora | /etc/pki/tls/certs/ca-bundle.crt (update-ca-trust) |
| JDK | $JAVA_HOME/lib/security/cacerts (default password changeit) |
| Python/requests | the certifi package |
| Node.js | built-in list plus NODE_EXTRA_CA_CERTS |
This one burns hours of debugging: you add the corporate CA with update-ca-certificates, curl works, and the Java application still throws PKIX path building failed because the JVM has its own cacerts. Some distributions keep the two in sync via a ca-certificates-java package, but official JDK Docker images usually do not.
Part 4 — CSR, issuance and expiry
A CSR is a signed request saying "here is my public key, here are the names I want, please sign". The CSR is signed with your own private key so the CA can confirm you actually hold it.
Certificate issuance flow — فرایند صدور گواهی:
sequenceDiagram
participant S as Server / Ops
participant CA as Certificate Authority
participant DNS as DNS / HTTP endpoint
S->>S: generate private key (never leaves)
S->>S: build CSR (public key + SANs), self-sign it
S->>CA: submit CSR
CA->>DNS: domain control validation (HTTP-01 / DNS-01)
DNS-->>CA: challenge satisfied
CA->>CA: apply policy, add SCT, sign with intermediate key
CA-->>S: leaf certificate + chain
S->>S: install cert + chain, reload server
Three CSR truths beginners get wrong: (1) the private key is never in the CSR and must never be sent to the CA — if a provider generates the key for you, that key is no longer private. (2) The CA is not obliged to honour what is in the CSR; in practice everything except the public key and the SANs is decided by the CA. (3) Every key rotation needs a fresh CSR.
# 1) A modern private key (ECDSA P-256)
openssl ecparam -name prime256v1 -genkey -noout -out api.key
chmod 600 api.key
# 2) CSR with a SAN — note: -noenc replaces the old -nodes in OpenSSL 3.x
openssl req -new -key api.key -out api.csr \
-subj "/CN=api.example.com/O=Example Inc" \
-addext "subjectAltName=DNS:api.example.com,DNS:www.api.example.com"
# 3) Always read the CSR before sending it
openssl req -in api.csr -noout -text -verify
SAN, wildcards and picking a strategy
DNS:api.example.com is that exact name. DNS:*.example.com covers exactly one label: it matches a.example.com, does not match a.b.example.com, and does not match the apex example.com. For IPs you must write IP:10.0.0.5; putting an IP into a DNS entry does nothing.
| Option | Benefit | The risk a senior should name |
|---|---|---|
| Single-name cert | Smallest blast radius, independent rotation | Many certificates → automation becomes mandatory |
| Multi-SAN cert | One certificate for related services | Leaks your internal domain list; everything rotates together |
| Wildcard | No new issuance for a new subdomain | One private key on every host; DNS-01 only |
| Short-lived (6-day) | Revocation becomes essentially irrelevant | Automation must be flawless |
| mTLS from a private CA | Full control, no public CA cost | You own rotation, CRLs and HSMs |
Wildcards are tempting, but they mean the same private key gets copied to every machine serving any subdomain — from the load balancer down to that experimental service nobody patches. With ACME, issuing per name is essentially free. Keep wildcards for cases where subdomains are genuinely dynamic (multi-tenant naming) and on-demand issuance is impractical.
Expiry: the biggest recurring incident class in engineering
Public certificate lifetimes are shrinking fast. Per CA/Browser Forum ballot SC-081v3 (April 2025):
| From | Maximum public TLS certificate lifetime |
|---|---|
| Today | 398 days |
| 15 March 2026 | 200 days |
| 15 March 2027 | 100 days |
| 15 March 2029 | 47 days |
Let's Encrypt has gone further: its shortlived profile, valid for 160 hours (just over six days), became generally available in January 2026, and it has announced that the default classic profile drops to 64 days in February 2027 and to 45 days in February 2028.
At 47-day lifetimes, "a calendar reminder" is not a process. You need three things: an inventory of every certificate with its expiry, automated renewal (ACME/cert-manager/Vault), and alerting independent of the issuing tool that connects to the port directly — because the common failure is not that issuance fails, it is that issuance succeeds while the service never reloads and keeps serving the old certificate.
If you keep that inventory in a database, the alerting query looks like this:
SELECT common_name, environment, not_after,
(not_after::date - CURRENT_DATE) AS days_left
FROM cert_inventory
WHERE not_after < CURRENT_DATE + INTERVAL '21 days'
ORDER BY not_after;SELECT common_name, environment, not_after,
TRUNC(not_after) - TRUNC(SYSDATE) AS days_left
FROM cert_inventory
WHERE not_after < SYSDATE + 21
ORDER BY not_after;In PostgreSQL subtracting two date values yields an integer number of days and INTERVAL is explicit; in Oracle subtracting two DATE values yields a fractional number of days, so TRUNC is required and adding a plain number to SYSDATE means days. If the Oracle column is a TIMESTAMP, the result becomes an INTERVAL DAY TO SECOND rather than a number — a classic trap.
Part 5 — Revocation: CRL, OCSP and stapling
Suppose a server's private key leaks while the certificate still has 40 days left. How do we tell the world it is no longer valid?
- CRL: the CA publishes a signed list of revoked serial numbers (URL in
CRL Distribution Points). It is bulky and must be re-downloaded regularly. - OCSP: the client asks a responder about one certificate and gets
good/revoked/unknown. That is a round trip on the critical path plus a privacy leak — the CA learns which IP is visiting which site. - OCSP Stapling: the server itself fetches a fresh signed response from the CA and "staples" it into the handshake (the
status_requestextension). The client never contacts the CA.
OCSP stapling flow — جریان stapling:
sequenceDiagram
participant C as Client
participant S as Server
participant O as OCSP responder (CA)
Note over S,O: periodically, off the critical path
S->>O: OCSP request for its own serial
O-->>S: signed response (good, valid ~7 days)
C->>S: ClientHello + status_request extension
S-->>C: ServerHello + Certificate + stapled OCSP response
C->>C: verify signature and freshness locally
Most clients are soft-fail: if the responder is unreachable, the connection is accepted anyway. An attacker who controls the traffic only has to drop the response. Let's Encrypt shut its OCSP service down entirely on 6 August 2025 (primarily for privacy reasons) and moved to CRLs. The industry's real answer to revocation is short lifetimes: a six-day certificate carries no revocation information at all, because it expires faster than any CRL refresh.
The "must-staple" extension (OID 1.3.6.1.5.5.7.1.24) turns soft-fail into hard-fail: "if you do not see a stapled response, abort". Powerful but dangerous — a stapling failure then takes your site fully offline instead of merely slowing it down.
server {
listen 443 ssl;
http2 on;
server_name api.example.com;
ssl_certificate /etc/ssl/api/fullchain.pem; # leaf + intermediates
ssl_certificate_key /etc/ssl/api/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off; # in TLS 1.3 the client chooses
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # unrotated ticket keys break forward secrecy
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/ssl/api/chain.pem; # only to verify the OCSP response
add_header Strict-Transport-Security "max-age=63072000" always;
}
Part 6 — TLS: the handshake, step by step
Two people in a crowded café want to talk privately, having never met: (1) verify who the other is via their passport, (2) agree on a shared secret language, (3) build a shared password neither of them ever transmitted in full, (4) speak only that language from now on. The handshake is exactly those four steps.
TLS 1.2 handshake (two round trips before data) — دستدادن TLS 1.2:
sequenceDiagram
participant C as Client
participant S as Server
C->>S: ClientHello (versions, cipher suites, SNI, ALPN, random)
S-->>C: ServerHello (chosen suite, random)
S-->>C: Certificate (leaf + intermediates)
S-->>C: ServerKeyExchange (ECDHE params, signed) + ServerHelloDone
C->>S: ClientKeyExchange (ECDHE public share)
C->>S: ChangeCipherSpec + Finished
S-->>C: ChangeCipherSpec + Finished
C->>S: Application data (HTTP request)
Cost: 2-RTT before the first byte of data. On a mobile link with 100 ms latency that is 200 ms of pure handshake, on top of TCP and DNS.
TLS 1.3 handshake (one round trip, encrypted early), RFC 8446 — دستدادن TLS 1.3:
sequenceDiagram
participant C as Client
participant S as Server
C->>S: ClientHello + key_share (guessed group) + supported_versions + SNI/ALPN
S-->>C: ServerHello + key_share
Note over C,S: shared secret derived — everything below is encrypted
S-->>C: {EncryptedExtensions, Certificate, CertificateVerify, Finished}
C->>S: {Finished} + Application data
S-->>C: {NewSessionTicket} for future resumption
| Aspect | TLS 1.2 | TLS 1.3 |
|---|---|---|
| RTT to first data | 2 | 1 (0 with 0-RTT) |
| Forward secrecy | Optional (only with ECDHE) | Mandatory |
| Suite name | TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 |
TLS_AES_128_GCM_SHA256 |
| Signature algorithm negotiation | Baked into the suite | Independent (signature_algorithms) |
| Certificate on the wire | Cleartext | Encrypted |
| Resumption | Session ID / ticket | PSK only, via NewSessionTicket |
| RC4/3DES/CBC/RSA-kex | Still negotiable | Removed from the standard |
TLS 1.3 got safer by deleting things: RSA key transport, static DH, compression, renegotiation and every non-AEAD suite are gone, so an entire family of attacks (ROBOT, CRIME, Logjam, FREAK) is simply not expressible any more.
Anatomy of a cipher suite
Read TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 piece by piece: ECDHE is the key exchange (that trailing E means ephemeral — a temporary key thrown away afterwards), RSA is the server's signature algorithm (i.e. the certificate key type), AES_128_GCM is the symmetric cipher in AEAD mode, and SHA256 is the hash used by the KDF. TLS 1.3 has exactly five suites and all are AEAD: TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, TLS_AES_128_CCM_SHA256, TLS_AES_128_CCM_8_SHA256.
With TLS_RSA_WITH_... the client encrypted a random secret with the certificate's public key and sent it. So anyone who records traffic today and obtains the server's private key five years later can decrypt all of that old traffic. With ECDHE each connection derives its own ephemeral key that never travels on the wire; compromising the certificate key does not expose the past. TLS 1.3 makes this mandatory.
Starting with JDK 25 (September 2025) and the JDK 21.0.10 update (January 2026), the TLS_RSA_* suites — which have no forward secrecy — are disabled by default. If a connection to an old broker or service suddenly breaks after a JDK upgrade with no cipher suites in common or handshake_failure, this is why. The correct fix is moving the peer to ECDHE; rewriting jdk.tls.disabledAlgorithms is a painkiller that needs an expiry date.
SNI, ALPN, ECH and resumption
SNI sends the hostname in cleartext inside ClientHello so a server hosting a hundred domains on one IP knows which certificate to present; without it, HTTPS virtual hosting is impossible. ALPN, in the same message, declares that the client speaks h2/http/1.1/h3 — HTTP/2 over TLS does not start without it, and gRPC depends on it for the same reason. ECH is the next step: it encrypts ClientHello itself with a key published in DNS so even the SNI is hidden.
If a Java connection behind a proxy comes back with the wrong certificate, check SNI first: run openssl s_client once with -servername and once without; two different certificates means SNI is the problem.
Resumption lets a later connection reuse a PSK: Session ID (state kept on the server, painful across nodes) or Session Ticket (encrypted state held by the client, server stays stateless). If the ticket encryption key stays constant for years and then leaks, every past resumed session becomes decryptable — so rotate it frequently or set ssl_session_tickets off;.
In TLS 1.3 a client can send application data alongside its first message (early data). That data is not replay-protected: an attacker can capture the packet and resend it ten times. If it was POST /payments, that is ten transactions. Rule: 0-RTT only for idempotent requests. In nginx, enable ssl_early_data on; only when the application inspects the Early-Data: 1 header and rejects non-idempotent requests with 425 Too Early.
Part 7 — mTLS
In ordinary TLS only the server proves itself. In mTLS the server also demands a certificate from the client and runs the same chain validation on it.
mTLS handshake — both sides present certificates:
sequenceDiagram
participant C as Client (has cert + key)
participant S as Server (has cert + key)
C->>S: ClientHello
S-->>C: ServerHello + Certificate + CertificateVerify
S-->>C: CertificateRequest (acceptable CAs, sig algs)
C->>S: Certificate (client leaf + chain)
C->>S: CertificateVerify (proves possession of client key)
S->>S: verify chain against client-CA truststore + check EKU clientAuth
S-->>C: Finished
| Scenario | Is mTLS right? | Why |
|---|---|---|
| Human user in a browser | No | Certificate distribution on user devices is a nightmare; use OIDC |
| Service-to-service inside a mesh | Yes | Workload identity, automated issuance, short lifetimes |
| B2B partner, small fixed set | Yes | Bounded population, formal contract |
| Public API with thousands of consumers | No | Onboarding and rotation do not scale; use OAuth2 |
| App to database/broker | Usually yes | Few, well-known clients; automatable rotation |
mTLS tells you who the peer is, not what it may do. The mature pattern: mTLS for workload authentication at the transport layer, plus a token (JWT/OAuth2) carrying the end-user's identity and permissions across services. The authorization model itself lives in the microservice security chapter; here, just note the two are complements, not competitors.
If every client certificate is issued on the same day by the same script, they all die on the same day and the whole system stops at once. CA rotation is harder still: you need a transition window where both sides trust both CAs, then you move the leaves, and only then remove the old CA. Do it in the wrong order and you have locked yourself out of your own cluster.
Part 8 — The Java side
A keystore is your wallet: it holds your own identity documents (certificate plus private key) and must be locked. A truststore is an address book: the list of parties you believe (CA certificates, no private keys at all). A client-only application often needs no keystore; a TLS server always needs one; in mTLS both sides need both.
| Property | JKS | PKCS12 (.p12/.pfx) |
|---|---|---|
| Standard | Sun/Oracle proprietary | International standard (RFC 7292) |
| Portable across languages | No | Yes (openssl, .NET, browsers) |
| JDK default | Up to JDK 8 | JDK 9 onwards (JEP 229) |
| Today's recommendation | Legacy compatibility only | Always the default |
keytool cheat sheet
| Task | Command |
|---|---|
| Key pair + self-signed cert | keytool -genkeypair -alias api -keyalg EC -groupname secp256r1 -validity 365 -keystore ks.p12 -storetype PKCS12 -dname "CN=api.example.com" -ext "SAN=dns:api.example.com" |
| List contents | keytool -list -v -keystore ks.p12 |
| CSR from an existing key | keytool -certreq -alias api -file api.csr -keystore ks.p12 -ext "SAN=dns:api.example.com" |
| Import the signed certificate | keytool -importcert -alias api -file signed.crt -keystore ks.p12 -trustcacerts |
| Add a CA to a truststore | keytool -importcert -alias corp-root -file corp-root.crt -keystore truststore.p12 -storetype PKCS12 -noprompt |
| Export to PEM | keytool -exportcert -alias api -rfc -file api.pem -keystore ks.p12 |
| Delete an entry | keytool -delete -alias old-ca -keystore truststore.p12 |
| Change the store password | keytool -storepasswd -keystore ks.p12 |
| Convert JKS to PKCS12 | keytool -importkeystore -srckeystore old.jks -srcstoretype JKS -destkeystore new.p12 -deststoretype PKCS12 |
| Grab a certificate from a server | keytool -printcert -sslserver api.example.com:443 |
| Read a certificate file | keytool -printcert -file chain.pem |
| Show the JDK's active TLS settings | keytool -showinfo -tls |
Two classic keytool traps. First, when you import the CA-signed certificate, keytool must be able to build the whole chain; if the intermediate is neither in that keystore nor in cacerts you get Failed to establish chain from reply — import the intermediate first, or feed it a concatenated PEM chain. Second, the leaf must be imported under the same alias as the private key; under a different alias you create a separate trusted-certificate entry instead of completing the key entry, and the server keeps serving the old self-signed certificate.
cacerts and the corporate CA
# Is the CA already in the JDK's default truststore?
keytool -list -cacerts -storepass changeit | grep -i corp-root
# Add it; since JDK 9, -cacerts points straight at the default truststore
keytool -importcert -trustcacerts -noprompt -alias corp-root \
-file corp-root.crt -cacerts -storepass changeit
Three ways to trust a private CA, best first: (1) a dedicated application truststore via -Djavax.net.ssl.trustStore=... — versionable, rotatable, no JDK surgery. (2) Adding it in the Dockerfile at build time — reproducible, but must re-run on every JDK upgrade. (3) Editing cacerts by hand on a server — gone on the next rollout; do not do this.
FROM eclipse-temurin:25-jre
COPY corp-root.crt /tmp/corp-root.crt
RUN keytool -importcert -trustcacerts -noprompt \
-alias corp-root -file /tmp/corp-root.crt \
-cacerts -storepass changeit \
&& rm /tmp/corp-root.crt
Building an SSLContext in code
public static SSLContext build(Path trustStore, char[] trustPass,
Path keyStore, char[] keyPass) throws Exception {
KeyStore ts = KeyStore.getInstance("PKCS12");
try (InputStream in = Files.newInputStream(trustStore)) { ts.load(in, trustPass); }
TrustManagerFactory tmf =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ts);
KeyManager[] kms = null; // only needed for mTLS
if (keyStore != null) {
KeyStore ks = KeyStore.getInstance("PKCS12");
try (InputStream in = Files.newInputStream(keyStore)) { ks.load(in, keyPass); }
KeyManagerFactory kmf =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, keyPass);
kms = kmf.getKeyManagers();
}
SSLContext ctx = SSLContext.getInstance("TLSv1.3");
ctx.init(kms, tmf.getTrustManagers(), null);
return ctx;
}
// HttpClient performs hostname verification for you
HttpClient client = HttpClient.newBuilder().sslContext(ctx).build();
The JDK's most dangerous default: HttpsURLConnection and java.net.http.HttpClient do hostname matching, but if you build a raw connection with SSLSocketFactory or SSLEngine, the JDK validates only the chain and never checks that the SAN matches the host you dialled — meaning any valid certificate from any CA for any domain is accepted. Fix it explicitly:
SSLSocket socket = (SSLSocket) ctx.getSocketFactory().createSocket("api.example.com", 443);
SSLParameters p = socket.getSSLParameters();
p.setEndpointIdentificationAlgorithm("HTTPS"); // without this line, no hostname check happens
socket.setSSLParameters(p);
socket.startHandshake();
// Never, not even "temporarily" — this turns TLS into unauthenticated encryption
TrustManager[] trustAll = { new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] c, String a) { }
public void checkServerTrusted(X509Certificate[] c, String a) { } // ← MITM welcome
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
}};
This snippet should fail every code review; "temporary, just for testing" always reaches production. The correct alternative is adding your internal CA or mkcert root to a separate truststore.
The classic errors and their real fixes
| Error message | What it really means | Fix |
|---|---|---|
PKIX path building failed … |
The JVM could not reach a trusted root from the server certificate | Server omits the intermediate (use fullchain) or the CA is not in the truststore (import it) |
No name matching <host> found |
Chain is fine; the SAN does not match the hostname | Fix the SAN; check SNI; put IPs in IP: not DNS: |
CertificateExpiredException |
Certificate expired — or the machine clock is wrong | Check date -u; in a container without NTP this error is misleading |
no cipher suites in common |
No overlapping version or suite | Inspect with -Djavax.net.debug=ssl:handshake; suspect the removal of TLS_RSA_* |
Received fatal alert: bad_certificate |
The server rejected the client certificate (mTLS) | Check the client chain, EKU=clientAuth, and the server's truststore |
Failed to establish chain from reply |
The intermediate is missing while importing the leaf | Import the intermediate first, or supply a PEM chain |
UnrecoverableKeyException: Cannot recover key |
Store password differs from key password | In PKCS12 keep both identical, or configure keyPassword separately |
TrustAnchor … is not a CA certificate |
You imported a leaf as if it were a CA | Import the real root/intermediate |
# The fullest view of the handshake (verbose; test environments only)
java -Djavax.net.debug=ssl:handshake:verbose:trustmanager -jar app.jar
# Certification path building only
java -Djava.security.debug=certpath -jar app.jar
# Turn on revocation checking (off by default)
java -Dcom.sun.net.ssl.checkRevocation=true \
-Djdk.tls.client.enableStatusRequestExtension=true -jar app.jar
If your own TLS server runs on the JVM, OCSP stapling support is off by default and is enabled with -Djdk.tls.server.enableStatusRequestExtension=true (JEP 249). On the client side jdk.tls.client.enableStatusRequestExtension is on by default, but the result is not enforced until com.sun.net.ssl.checkRevocation=true.
Spring Boot: SSL bundles
Since Spring Boot 3.1, instead of scattering keystore paths across ten properties you define a named bundle; since 3.2 it can hot-reload:
spring:
ssl:
bundle:
pem:
server:
reload-on-update: true # watches the files and rebuilds the bundle
keystore:
certificate: "file:/etc/tls/tls.crt"
private-key: "file:/etc/tls/tls.key"
truststore:
certificate: "file:/etc/tls/ca.crt"
jks:
partner:
keystore:
location: "classpath:client.p12"
password: "${CLIENT_P12_PASSWORD}"
type: "PKCS12"
server:
ssl:
bundle: "server"
In Kubernetes, cert-manager writes the fresh certificate into the same Secret and kubelet updates the mounted files — but the JVM read them exactly once at startup. Without a reload, the pod serves an expired certificate until its next restart. reload-on-update: true watches the files and follows symlinks, which is essential for Kubernetes Secrets. Without Spring Boot you need the equivalent: an X509ExtendedKeyManager that re-reads from disk, or simply a pod rollout at renewal time.
Part 9 — The openssl cookbook
| Goal | Command |
|---|---|
| ECDSA P-256 key | openssl ecparam -name prime256v1 -genkey -noout -out key.pem |
| RSA 2048 key | openssl genrsa -out key.pem 2048 |
| Ed25519 key | openssl genpkey -algorithm ed25519 -out key.pem |
| Key + CSR in one shot | openssl req -newkey rsa:2048 -noenc -keyout key.pem -out req.csr -subj "/CN=x" |
| Quick self-signed with SAN | openssl req -x509 -newkey rsa:2048 -noenc -keyout key.pem -out cert.pem -days 90 -subj "/CN=x" -addext "subjectAltName=DNS:x" |
| Read a certificate | openssl x509 -in cert.pem -noout -text |
| Dates only / SAN only | openssl x509 -in cert.pem -noout -dates · ... -ext subjectAltName |
| SHA-256 fingerprint | openssl x509 -in cert.pem -noout -fingerprint -sha256 |
| Read a CSR | openssl req -in req.csr -noout -text -verify |
| Probe a server | openssl s_client -connect host:443 -servername host -showcerts |
| Force a TLS version | openssl s_client -connect host:443 -tls1_2 (or -tls1_3) |
| Test ALPN | openssl s_client -connect host:443 -alpn h2 -servername host |
| Test mTLS | openssl s_client -connect host:8443 -cert client.crt -key client.key -CAfile ca.crt |
| Verify a chain offline | openssl verify -CAfile root.pem -untrusted intermediate.pem leaf.pem |
| PEM → PKCS12 | openssl pkcs12 -export -inkey key.pem -in cert.pem -certfile chain.pem -name api -out bundle.p12 |
| PKCS12 → PEM | openssl pkcs12 -in bundle.p12 -noenc -out all.pem |
| DER ↔ PEM | openssl x509 -inform DER -in cert.der -out cert.pem |
| Throwaway test server | openssl s_server -accept 8443 -cert cert.pem -key key.pem -www |
| Read a CRL | openssl crl -in crl.der -inform DER -noout -text |
| Manual OCSP query | openssl ocsp -issuer chain.pem -cert leaf.pem -url http://ocsp.example.com -text |
Three combinations you will use constantly:
# 1) Does this private key match this certificate? (both outputs must be identical)
openssl x509 -noout -pubkey -in cert.pem | openssl sha256
openssl pkey -pubout -in key.pem | openssl sha256
# 2) What chain is the server actually sending?
openssl s_client -connect api.example.com:443 -servername api.example.com \
-showcerts </dev/null 2>/dev/null | grep -E "^(depth|verify|s:|i:)"
# 3) A complete private CA in 60 seconds (for dev and integration tests)
openssl req -x509 -newkey rsa:4096 -noenc -days 3650 -keyout ca.key -out ca.crt \
-subj "/CN=Dev Root CA" \
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
openssl req -newkey rsa:2048 -noenc -keyout svc.key -out svc.csr -subj "/CN=svc.local"
openssl x509 -req -in svc.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out svc.crt -days 30 -sha256 \
-extfile <(printf "subjectAltName=DNS:svc.local,IP:127.0.0.1\nextendedKeyUsage=serverAuth,clientAuth\nbasicConstraints=CA:FALSE")
openssl verify -CAfile ca.crt svc.crt # → svc.crt: OK
A version note: -nodes (short for "no DES") is deprecated since OpenSSL 3.0 and its official replacement is -noenc. Both mean "do not encrypt the private key" — so remember chmod 600 and correct ownership, and in production pull the key from a secret manager rather than a repository.
Part 10 — Pinning, Certificate Transparency and ACME
Pinning means "I accept only this public key (or this CA), even if another legitimate CA issues a perfectly valid certificate for the same domain". Its purpose is defending against mis-issuance by a CA. It has three levels: pinning the leaf certificate (breaks on every renewal — almost always wrong), pinning the leaf public key or SPKI (more stable, but it means you never rotate the key), and pinning the CA/intermediate (most stable, weakest protection).
# Compute an SPKI-style pin for a server
openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null 2>/dev/null \
| openssl x509 -pubkey -noout | openssl pkey -pubin -outform der \
| openssl dgst -sha256 -binary | openssl enc -base64
The Public-Key-Pins header was removed in Chrome 72 because of its self-destruction risk — a site that loses its key becomes unreachable for everyone until max-age expires. Its successor Expect-CT was also removed once CT became mandatory in Chrome 107. On the web, the industry's answer is Certificate Transparency: every public certificate must be logged in append-only public logs and carry SCT receipts, so a domain owner can spot unauthorized issuance within minutes.
Pinning still makes sense in mobile apps and machine-to-machine clients, where you control both ends. Even there: always keep at least two pins (current key plus an offline backup) and a fast update path, otherwise pinning is just a time bomb set for rotation day.
ACME (RFC 8555) fully automates issuance: the client proves control of the domain and the CA issues — no humans, no forms.
ACME issuance with HTTP-01 challenge — صدور با ACME:
sequenceDiagram
participant A as ACME client (certbot)
participant CA as ACME server (Let's Encrypt)
participant W as Your web server
A->>CA: newOrder for api.example.com
CA-->>A: challenge token + authorization URL
A->>W: publish token at /.well-known/acme-challenge/TOKEN
A->>CA: ready to be validated
CA->>W: GET http://api.example.com/.well-known/acme-challenge/TOKEN
W-->>CA: token + account key thumbprint
CA-->>A: authorization valid
A->>CA: finalize with CSR
CA-->>A: certificate + chain
| Challenge | How it proves control | Limitation |
|---|---|---|
HTTP-01 |
A file on port 80 under /.well-known/acme-challenge/ |
Port 80 must be reachable from the internet; cannot issue wildcards |
DNS-01 |
A TXT record on _acme-challenge.<domain> |
Needs DNS provider API access; the only wildcard path |
TLS-ALPN-01 |
A special certificate on port 443 with ALPN acme-tls/1 |
Requires exclusive use of 443; suits reverse proxies |
# Issue via webroot (the service keeps running)
certbot certonly --webroot -w /var/www/html \
-d api.example.com -d www.api.example.com \
--agree-tos -m ops@example.com --non-interactive
# Wildcards require DNS-01
certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cf.ini \
-d "*.example.com" -d example.com
# Six-day certificate (the shortlived profile — Certbot 4.0 and later)
certbot certonly --preferred-profile shortlived --webroot -w /var/www/html -d api.example.com
# Dry-run renewal without burning rate limits, and reload after every success
certbot renew --dry-run
certbot renew --deploy-hook "systemctl reload nginx"
The most common ACME incident is not a failed renewal; it is a successful renewal where nginx or the JVM never re-reads the file and keeps serving the old certificate right up to expiry. Always wire a --deploy-hook (or reload-on-update, or a rollout), and monitor expiry from outside, on the real port rather than by reading a file on disk.
When building the automation, always start on staging — Let's Encrypt enforces rate limits and repeated failures will lock you out:
certbot certonly --server https://acme-staging-v02.api.letsencrypt.org/directory ...
Part 11 — Certificate rotation in Kubernetes
The standard pattern is cert-manager: a controller that understands Issuer/ClusterIssuer and Certificate resources, obtains the certificate, writes it into a kubernetes.io/tls Secret, and renews automatically before expiry.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@example.com
profile: tlsserver # ACME profile selection — cert-manager v1.18+
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
ingressClassName: nginx
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: api-tls
namespace: production
spec:
secretName: api-tls # tls.crt and tls.key land here
duration: 2160h # 90d
renewBefore: 720h # renew 30d before expiry
privateKey:
algorithm: ECDSA
size: 256
rotationPolicy: Always # generate a new key on every renewal
dnsNames:
- api.example.com
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
Up to cert-manager v1.17 the default rotationPolicy was Never — the certificate was renewed but the same old private key lived forever. From v1.18 the default is Always, so both key and certificate change at every renewal.
cert-manager rotation and pod reload — چرخش و بارگذاری مجدد:
flowchart LR
C["Certificate CR<br/>renewBefore: 30d"] --> CM["cert-manager controller"]
CM -->|ACME order| CA["Let's Encrypt"]
CA -->|cert + chain| CM
CM -->|writes| S["Secret kubernetes.io/tls<br/>tls.crt, tls.key"]
S -->|projected volume| P["Pod filesystem /etc/tls"]
P -->|file watcher / SIGHUP| APP["App reloads SSL context"]
S -.->|read directly| ING["Ingress controller"]
kubelet does update mounted Secret files (with up to about a minute of delay), but it does so with a symlink swap rather than writing into the same inode — so watchers that only put an inotify watch on the file itself never see the change. Three options: use a library that follows symlinks (Spring Boot's reload-on-update does); run a hook that sends SIGHUP when the Secret changes; or, simplest and most reliable, trigger a rollout at renewal time via an annotation carrying the certificate hash.
In service meshes, workload certificates typically live for hours or less and are held in the sidecar's memory — they never touch disk. Identity is not a DNS name but a SPIFFE ID such as spiffe://cluster.local/ns/production/sa/api, carried in a URI-type SAN. Mesh architecture belongs to the microservice runtime chapter; the point here is that the same X.509 chain sits underneath it.
Part 12 — GPG/PGP: same problem, different philosophy
In the X.509 world, to make your signature meaningful you go to a notary (a CA) and everyone trusts the notary. In the PGP world there is no notary: you make your own personal seal, and your friends stamp their seal onto yours. If I trust someone and they have signed your seal, I can trust your seal too. That horizontal network is the web of trust.
PGP is a standard (OpenPGP) and GnuPG/gpg is its most common free implementation; the current development branch is adding post-quantum encryption (Kyber/ML-KEM).
| Aspect | X.509 / TLS | OpenPGP / GPG |
|---|---|---|
| Trust model | Hierarchical; root CAs | Horizontal; web of trust or manual trust |
| Identity | Domain name (SAN) | Name and email (User ID) |
| Main use | Live connections (transport) | Data at rest: files, email, artifacts, commits |
| Revocation | CRL/OCSP published by the CA | A revocation certificate published by the key owner |
| Expiry | Mandatory and short | Optional; extendable without changing the key |
| Key structure | One key per certificate | One primary key plus role-specific subkeys |
Because TLS protects the channel, not the content. When you download a .tar.gz from a mirror, HTTPS only says "these bytes arrived unmodified from that server" — not "these bytes were actually produced by the project team". A GPG signature on the artifact closes that gap, which is why virtually every Linux distribution, package repository and serious open-source release depends on it. Channel security versus content security is a standard platform-interview question.
Key structure: primary and subkeys
flowchart TD
P["Primary key (Ed25519)<br/>usage: Certify — keep offline"]
S["Signing subkey<br/>usage: Sign"]
E["Encryption subkey (Curve25519)<br/>usage: Encrypt"]
A["Authentication subkey<br/>usage: Auth (SSH)"]
U["User IDs — Name and email"]
P -->|certifies| S
P -->|certifies| E
P -->|certifies| A
P -->|binds| U
The primary key is your identity: it certifies your User IDs and subkeys and signs other people's keys. Subkeys do the day-to-day work. The big win: lose your laptop and you revoke and replace only the subkeys, while your identity and every signature ever collected on the primary survive. The senior pattern is to keep the primary offline (encrypted drive or a hardware token) and carry only subkeys on the working machine — gpg shows that state as sec#.
# Create a key; future-default today means ed25519 for signing + cv25519 for encryption
gpg --quick-generate-key "Jane Dev <jane@example.com>" future-default default 2y
gpg --quick-add-key <FINGERPRINT> ed25519 sign 1y # a dedicated signing subkey
gpg --list-keys --keyid-format=long --with-subkey-fingerprints
gpg --list-secret-keys --keyid-format=long
gpg --armor --export jane@example.com > jane-public.asc # to hand to others
gpg --armor --export-secret-subkeys jane@example.com > subs.asc # working backup
gpg --import colleague-public.asc
gpg --quick-set-expire <FINGERPRINT> 2y '*' # extend all subkeys
If your GPG key expires you do not need a new key: just push the expiry date forward and republish the updated public key — every signature and all accumulated trust survive. The classic beginner mistake is seeing expired and generating a brand-new key, throwing away the whole web of trust. Here the expiry date is a dead-man switch: if you ever lose access to the key, it retires itself.
Encrypting, signing and verifying
# Encrypt for several recipients plus yourself (otherwise you cannot open it either)
gpg --encrypt -r bob@example.com -r audit@example.com -r jane@example.com \
--output report.pdf.gpg report.pdf
gpg --decrypt report.pdf.gpg > report.pdf
gpg --symmetric --cipher-algo AES256 backup.tar # symmetric, passphrase only
gpg --clear-sign announcement.txt # human-readable inline signature
# Detached signature — the release-artifact standard
gpg --armor --detach-sign app-1.4.0.tar.gz # → app-1.4.0.tar.gz.asc
gpg --verify app-1.4.0.tar.gz.asc app-1.4.0.tar.gz
Detached signatures are the release standard because the file itself stays untouched: the same .tar.gz any tool can read, plus a small .asc beside it. Embed the signature inside the file and everyone is forced through gpg just to reach the content.
Good signature only says the signature matches the public key in your keyring. The not certified with a trusted signature warning means "but I have no idea whether that public key belongs to the project". An attacker who controls the mirror can serve their own file, their own signature and their own public key, and you will still see Good signature. The only way to close the loop is to obtain the full 40-hex-digit fingerprint from a channel independent of the download and then locally sign the key:
gpg --keyserver hkps://keys.openpgp.org --recv-keys <FULL_40_HEX_FINGERPRINT>
gpg --fingerprint <FULL_40_HEX_FINGERPRINT> # compare against the independent source
gpg --lsign-key <FINGERPRINT> # local signature, never published
Never trust a short key ID (8 or 16 hex digits); manufacturing a key with a colliding short ID is cheap and practical.
Keyservers, WKD and revocation
The old SKS keyservers let anyone attach any signature to any key and never deleted anything, which led to "key poisoning" — keys with tens of thousands of junk signatures that broke gpg outright. That network is effectively abandoned. keys.openpgp.org is the new model: identity information is only searchable after email verification and with the owner's consent, and third-party signatures are stripped. WKD serves the public key directly from the person's own domain over HTTPS under /.well-known/openpgpkey/... — PGP borrowing trust from PKI.
gpg --keyserver hkps://keys.openpgp.org --send-keys <FINGERPRINT>
gpg --locate-external-keys jane@example.com # via WKD and keyservers
gpg --refresh-keys # pick up revocations and expiry changes
If you lose the private key or forget the passphrase, you can no longer produce a revocation certificate, and your dead key stays valid on keyservers forever. gpg 2.x automatically writes one to ~/.gnupg/openpgp-revocs.d/<FPR>.rev at key creation; print it or store it somewhere safe and separate from the key. Note the flip side: anyone holding that file can revoke your key — a trivial denial of service against your identity.
gpg --output revoke.asc --gen-revoke <FINGERPRINT> # create it today, not on the bad day
gpg --import revoke.asc && gpg --keyserver hkps://keys.openpgp.org --send-keys <FINGERPRINT>
Signing git commits and tags
git config --global user.signingkey <SIGNING_SUBKEY_FPR>! # the ! means "exactly this subkey"
git config --global commit.gpgsign true
git config --global tag.gpgsign true
export GPG_TTY=$(tty) # otherwise no passphrase prompt
git commit -S -m "Fix token refresh race"
git tag -s v1.4.0 -m "Release 1.4.0"
git verify-commit HEAD && git verify-tag v1.4.0
git log --pretty="%h %G? %GS %s" -10 # %G? = G(ood) B(ad) U(ntrusted) N(one) E(rror)
Since Git 2.34 you can use an SSH key instead, with git config --global gpg.format ssh and user.signingkey ~/.ssh/id_ed25519.pub. It is simpler, but it has no expiry or revocation model and verification depends on a gpg.ssh.allowedSignersFile you maintain yourself. For signatures that must still verify years later, OpenPGP remains the more mature choice.
Note that a signature does not prove authorship, only who signed: the git Author field is free text anyone can set. A meaningful policy means branch protection requiring verified signatures plus a maintained list of allowed keys; without that, the signature is just a decorative green badge.
gpg cheat sheet
| Task | Command |
|---|---|
| Interactive key generation | gpg --full-generate-key |
| Quick generation | gpg --quick-generate-key "Name <mail>" future-default default 2y |
| Add a subkey | gpg --quick-add-key <FPR> ed25519 sign 1y |
| List keys | gpg --list-keys --keyid-format=long · gpg --list-secret-keys |
| Fingerprint | gpg --fingerprint <FPR> |
| Export public / secret | gpg --armor --export <FPR> · gpg --armor --export-secret-keys <FPR> |
| Import | gpg --import pub.asc |
| Encrypt / decrypt | gpg -e -r bob@example.com -o f.gpg f · gpg -d f.gpg > f |
| Symmetric encryption | gpg -c --cipher-algo AES256 f |
| Detached / inline signature | gpg --armor --detach-sign f · gpg --clear-sign f |
| Verify | gpg --verify f.asc f |
| Locally sign a key | gpg --lsign-key <FPR> |
| Set trust level | gpg --edit-key <FPR> then trust |
| Extend expiry | gpg --quick-set-expire <FPR> 2y '*' |
| Generate a revocation cert | gpg --output rev.asc --gen-revoke <FPR> |
| Send / receive from keyserver | gpg --keyserver hkps://keys.openpgp.org --send-keys <FPR> · --recv-keys <FPR> |
| Locate via WKD | gpg --locate-external-keys mail@example.com |
| Delete keys | gpg --delete-keys <FPR> · gpg --delete-secret-keys <FPR> |
| Non-interactive use in CI | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 ... |
A pipeline has neither a terminal nor a human to type a passphrase. The safe pattern: create a CI-only signing subkey with a short lifetime (never the primary), store it in a secret manager, import it in the job, and wipe the temporary GNUPGHOME afterwards. If it leaks you revoke only that subkey.
export GNUPGHOME=$(mktemp -d)
printf '%s' "$GPG_PRIVATE_KEY" | gpg --batch --import
echo "$GPG_PASSPHRASE" | gpg --batch --yes --pinentry-mode loopback \
--passphrase-fd 0 --armor --detach-sign dist/app-1.4.0.tar.gz
rm -rf "$GNUPGHOME"
Part 13 — The senior operations playbook
- Keep an inventory. Every certificate, its owner, where it is deployed and when it expires. If you cannot say within thirty seconds which certificate expires next, expiry will ambush you.
- Monitor from the outside. A probe that connects to the real port catches both expiry and the far more common "renewed but never reloaded" failure.
- Use two alert thresholds: soft at 30% of remaining lifetime (ticket), hard at 10% (page). On a 47-day certificate that is 14 and 5 days.
- Keep private keys out of the repository. If a key ever entered git history it is burned; it must be revoked and replaced, not merely deleted from the commit.
- Rehearse rotation. Once a quarter, rotate the CA in staging. A team that has never practised CA rotation will be doing it for the first time on incident day.
- Take clocks seriously. A container without NTP produces exactly the errors that look like certificate problems;
date -uis the first command of any TLS investigation. - Move algorithms forward. Today that means ECDSA P-256 or Ed25519 for new certificates, TLS 1.2 as the floor and 1.3 as the default. The next step is post-quantum: the hybrid
X25519MLKEM768key exchange is enabled by default from OpenSSL 3.5 (April 2025), and the JDK has shippedML-KEMandML-DSAsince version 24. "Harvest now, decrypt later" is a genuine threat to long-lived data.
Part 14 — Interview questions
An X.509 structure binding a public key to a set of names in the SAN, with a CA's signature over that claim. It contains nothing secret and is entirely public.
It proves: "the holder of the corresponding private key controlled these names at issuance time, and a CA verified that". It does not prove that the site is malware-free, that the company is legitimate, or that your data is handled safely on the other side. For DV certificates — the overwhelming majority on the internet — the only thing validated is domain control.
First I separate an incomplete server chain from an incomplete client truststore, using openssl s_client -connect host:443 -servername host -showcerts. If I only see one CERTIFICATE block, the server is not sending the intermediate → the fix is replacing cert.pem with fullchain.pem and reloading.
If the chain is complete but openssl also reports unable to get local issuer, the root is missing from the system. If openssl and curl both succeed and only Java fails, it is almost always the CA missing from cacerts, because the JVM does not use the OS trust store. I confirm with -Djava.security.debug=certpath and then import into a dedicated application truststore rather than hand-editing cacerts, which disappears at the next rollout.
Red flag in a candidate's answer: "I'd write a TrustManager that accepts everything".
A keystore holds your own identity: certificate plus private key. A truststore holds the CA certificates you believe, with no private keys. Format-wise both can be a PKCS12 file; the difference is the role, not the file type.
A plain HTTPS client only needs a truststore (usually the default cacerts suffices); a TLS server always needs a keystore; in mTLS both sides need both. A common mistake is dumping CA certificates into the same keystore that holds the server key — it works, but server-key rotation and CA-list rotation are two completely different cycles and should stay separate.
Speed: the handshake went from 2-RTT to 1-RTT, because the client guesses the key-exchange group and sends key_share up front in ClientHello; with a PSK, 0-RTT becomes possible.
Security by deletion: RSA key transport, static DH, renegotiation, compression and all non-AEAD suites were removed; the suite count dropped from hundreds to five, and signature-algorithm negotiation was split out of the suite. A whole family of attacks (ROBOT, CRIME, Logjam, FREAK) is no longer expressible.
Privacy: everything after ServerHello is encrypted, including the server certificate, which travelled in cleartext under 1.2. The remaining exposure is SNI, which ECH is designed to close.
In TLS_RSA_WITH_... suites the client encrypted a pre-master secret with the certificate's public key, tying the whole session's security to the server's long-term key. Record the traffic today, obtain that key years later, and all the past traffic decrypts.
With ECDHE every session derives its own ephemeral key pair which never travels on the wire and is discarded afterwards; the long-term key is only used to sign, never to transport a secret. The practical detail: JDK 25 and the 21.0.10 update disable TLS_RSA_* by default, which is exactly why connections to legacy systems suddenly fail with no cipher suites in common.
A CRL is a signed list of revoked serials — bulky and always slightly stale. OCSP is a live query about one certificate — fresher, but it adds a round trip on the critical path and tells the CA who is visiting which site. Stapling moves that response to the server, which attaches it during the handshake: no added latency and no privacy leak.
The root problem for all three: clients are soft-fail, so an attacker only needs to block the response. Recommendation: enable stapling because it is cheap, but do not treat it as a serious security control. The industry's real answer is short lifetimes — Let's Encrypt shut down its OCSP service in August 2025 and its six-day certificates carry no revocation information at all.
CN is part of the Distinguished Name and is just a string; subjectAltName is a typed, multi-valued extension (DNS, IP, URI). Since about 2017 browsers ignore CN for validation, and public CAs are required to repeat any CN name in the SAN.
A wildcard covers exactly one label: *.example.com matches api.example.com, does not match a.b.example.com, and does not match the apex example.com — the apex must be listed separately in the SAN. In ACME, wildcards can only be issued via DNS-01. The architectural risk: one shared private key on every host serving any subdomain.
It fits when you control both ends and the client population is automatable: service-to-service inside a cluster, apps connecting to databases and brokers, and a defined set of B2B partners. It does not fit browser users or public APIs with thousands of consumers, because distributing and rotating certificates on devices you do not control is impractical.
mTLS is transport-layer authentication, not authorization; knowing the peer is payment-service says nothing about what it may access. The main operational trap is rotation: certificates issued together die together, and CA rotation requires a transition window where both sides trust both CAs.
Contain: the fastest recovery is usually issuing a fresh certificate from the same CA and reloading. If the CA is unreachable, a short-lived certificate from an internal CA beats disabling TLS validation in clients — that turns an outage into a security incident.
Diagnose: why did monitoring not warn? Usually either there was none, or it read a file on disk while the service was serving something else.
Prevent: automated issuance, automated reload after renewal, an external probe on the real port with two thresholds, a named owner per certificate, and a quarterly recovery drill. The detail that makes the answer senior: with lifetimes heading to 47 days by 2029, a manual process is mathematically unsustainable.
Using a PSK left over from a previous session, the client sends application data alongside its very first handshake message, giving zero round trips of latency.
The risk is that this data is not replay-protected, because no fresh exchange has yet guaranteed message uniqueness; an attacker can replay the packet repeatedly. The rule is: idempotent requests only — in practice cacheable GETs. The server must flag early data (in HTTP, the Early-Data: 1 header) and the application must reject any non-idempotent request with 425 Too Early so the client retries on the fully established connection.
Pinning restricts trust to a specific key or CA instead of the whole trust store, defending against mis-issuance by a legitimate CA.
On the web it is wrong: HPKP was removed in Chrome 72 because one configuration mistake made a site unreachable for everyone until max-age expired. Its successor is Certificate Transparency — append-only public logs that make mis-issuance detectable without any self-destruction risk.
In mobile apps and machine-to-machine clients it still makes sense, under three conditions: pin the public key rather than the certificate, keep at least two pins (current plus an offline backup), and have a fast update path. Without those, pinning is just a time bomb set for rotation day.
The trust model. X.509 is hierarchical: a bounded set of root CAs in the trust store, trust flowing top-down. It scales and automates well, but every CA is a single point of failure — one compromised CA can issue for any domain. OpenPGP is horizontal: there is no centre, and each user decides which keys to believe, often based on signatures from people they already trust. There is no central point of failure, but onboarding and key discovery are hard — which is why it never took over the web.
The functional difference: TLS protects data in motion, GPG protects data at rest — files, email, artifacts, commits — with a signature that is verifiable years later and independent of any channel.
It is not. Good signature only says the signature matches a public key already in my keyring, not that the key belongs to the project; an attacker controlling the mirror serves their own file, signature and key.
The correct routine: (1) obtain the full 40-hex-digit fingerprint of the release key from a channel independent of the download, (2) import the key and compare with gpg --fingerprint, (3) run gpg --verify file.asc file and deliberately resolve the "key not certified" warning with --lsign-key. Never trust a short 8- or 16-digit key ID.
The same logic applies to commit signing: a signature proves who signed, not the Author field, which is free text. Without branch protection and a maintained list of allowed keys, the signature is only a decorative green badge.
A certificate is nothing more than a signed claim: "this public key belongs to these names". All of PKI is the machinery for following that signature up to a trusted root — and most real failures happen not in the cryptography but in incomplete chains, expiry dates and the wrong trust store.
TLS turns that trust into a live connection: 1.3 with one round trip, mandatory forward secrecy and the wholesale removal of broken algorithms. mTLS makes the same mechanism mutual and is excellent for workload identity — provided you have solved rotation.
In Java, hold on to just two ideas: the keystore is who I am, the truststore is who I believe. PKIX path building failed almost always means one of those two is incomplete, and the answer is never a permissive TrustManager.
The industry's direction is unambiguous: lifetimes are heading to 47 days and below, revocation is being replaced by fast expiry, and anything manual will break soon. Automated issuance, automated reload and external monitoring are the three pillars of survival. Alongside all of that, GPG covers the layer TLS never will: trust in the content independent of the channel — signing releases, verifying artifacts, and signing commits and tags.