TLS, mTLS и Protobuf продвинуто
Зачем знать: TLS — основа любой защищённой коммуникации. mTLS — стандарт internal service-to-service в современных meshes (Istio, Linkerd, Consul Connect). Protobuf — wire format для gRPC, Connect, Apache Pulsar и многих внутренних протоколов. Middle 2 Go-разработчик обязан уметь настроить tls.Config правильно (handshake, ALPN, cert reload), понимать mTLS (verification of client cert, SPIFFE), читать proto schema (field numbers, evolution rules) и работать с protoc/buf.
Содержание
Заголовок раздела «Содержание»- Базовая концепция
- Глубокое погружение (под капотом)
- Gotchas (15+)
- Production-практики
- Вопросы (25+)
- Practice
- Источники
1. Базовая концепция
Заголовок раздела «1. Базовая концепция»TLS — это что
Заголовок раздела «TLS — это что»TLS (Transport Layer Security) — криптографический протокол, обеспечивающий confidentiality, integrity, authenticity для transport-уровня. Предшественник — SSL (deprecated). Современные версии: TLS 1.2 (2008), TLS 1.3 (2018).
Без TLS С TLS Client ─── plain ──── Server Client ─── ⚿ ──── Server (anyone reads) (encrypted, authenticated)TLS 1.2 vs TLS 1.3
Заголовок раздела «TLS 1.2 vs TLS 1.3»| Свойство | TLS 1.2 | TLS 1.3 |
|---|---|---|
| Handshake RTT | 2 RTT | 1 RTT (0 RTT с resumption) |
| Cipher suites | ~70+ (часто weak) | 5 modern (AEAD only) |
| Key exchange | RSA, DHE, ECDHE | Только ECDHE (forward secrecy) |
| Hash в handshake | MD5+SHA1 → SHA256 | SHA256/SHA384 |
| Encrypted handshake | Нет (handshake plain) | Да (после ClientHello) |
| Session resumption | Session ID или ticket | PSK + 0-RTT |
| Renegotiation | Да (источник атак) | Нет |
В Go 1.22+ default tls.Config.MinVersion = tls.VersionTLS12, рекомендуется ставить tls.VersionTLS13.
Handshake TLS 1.3 упрощённо
Заголовок раздела «Handshake TLS 1.3 упрощённо»Client Server | ────── ClientHello ──────────────────────> | | (cipher_suites, key_share, ALPN) | | | | <───── ServerHello, Certificate, ──────── | | CertificateVerify, Finished | | (всё после ServerHello зашифровано) | | | | ────── Finished ──────────────────────────> | | (HTTP request — already in same flight) | | <───── Application data ────────────────── |1 RTT для handshake, application data сразу после.
Handshake TLS 1.2 (для сравнения)
Заголовок раздела «Handshake TLS 1.2 (для сравнения)»Client Server | ──── ClientHello ─────────────────────────> | | <─── ServerHello ──────────────────────── | | <─── Certificate ──────────────────────── | | <─── ServerKeyExchange ─────────────────── | | <─── ServerHelloDone ────────────────────── | | ──── ClientKeyExchange ────────────────────> | | ──── ChangeCipherSpec ────────────────────> | | ──── Finished ────────────────────────────> | | <─── ChangeCipherSpec ──────────────────── | | <─── Finished ─────────────────────────── | | ──── Application data ────────────────────> |2 RTT до первой application data.
Сертификаты: X.509, PEM, DER
Заголовок раздела «Сертификаты: X.509, PEM, DER»X.509 — формат сертификатов.
Encoding:
- DER — binary
- PEM — base64-encoded DER + BEGIN/END markers (текстовый)
-----BEGIN CERTIFICATE-----MIIDXTCCAkWgAwIBAgIJAKy7zS...-----END CERTIFICATE-----Файлы:
.crt,.cer,.pem— обычно сертификат.key— приватный ключ.pfx,.p12— PKCS#12, содержит cert + private key + chain (Windows-like).csr— Certificate Signing Request
Минимальный HTTPS сервер
Заголовок раздела «Минимальный HTTPS сервер»package main
import ( "crypto/tls" "log" "net/http")
func main() { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello over TLS")) })
srv := &http.Server{ Addr: ":8443", Handler: mux, TLSConfig: &tls.Config{ MinVersion: tls.VersionTLS13, CipherSuites: []uint16{ // TLS 1.3 cipher suites выбираются автоматически, // эти — только для TLS 1.2 fallback tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, }, PreferServerCipherSuites: true, NextProtos: []string{"h2", "http/1.1"}, }, } log.Fatal(srv.ListenAndServeTLS("cert.pem", "key.pem"))}Self-signed cert для dev
Заголовок раздела «Self-signed cert для dev»# Простой self-signedopenssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \ -sha256 -days 365 -nodes -subj "/CN=localhost"
# С Subject Alternative Names (SAN) — обязательны для современных браузеровcat > openssl.cnf <<'EOF'[req]default_bits = 2048distinguished_name = req_distinguished_namereq_extensions = req_extprompt = no[req_distinguished_name]CN = localhost[req_ext]subjectAltName = @alt_names[alt_names]DNS.1 = localhostDNS.2 = *.localIP.1 = 127.0.0.1EOFopenssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem \ -days 365 -nodes -config openssl.cnf -extensions req_ext⚠️ Современные браузеры/клиенты требуют SAN (Subject Alternative Names), Common Name больше не используется.
mTLS — что это
Заголовок раздела «mTLS — что это»mTLS (mutual TLS) — TLS, где обе стороны аутентифицируют друг друга через сертификаты. Сервер требует client certificate.
Client Server | ── ClientHello ──────────────────────────> | | <── ServerHello + Certificate ─────────── | | <── CertificateRequest ────────────────── | (server просит client cert) | ── ClientCertificate ────────────────────> | | ── ClientCertificateVerify ──────────────> | (доказательство владения) | ── Finished ─────────────────────────────> | | ...Minimal mTLS server в Go
Заголовок раздела «Minimal mTLS server в Go»package main
import ( "crypto/tls" "crypto/x509" "log" "net/http" "os")
func main() { caBytes, _ := os.ReadFile("ca.pem") pool := x509.NewCertPool() pool.AppendCertsFromPEM(caBytes)
srv := &http.Server{ Addr: ":8443", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 { w.Write([]byte("hello, " + r.TLS.PeerCertificates[0].Subject.CommonName)) } }), TLSConfig: &tls.Config{ MinVersion: tls.VersionTLS13, ClientAuth: tls.RequireAndVerifyClientCert, ClientCAs: pool, }, } log.Fatal(srv.ListenAndServeTLS("server.pem", "server.key"))}Protobuf — что это
Заголовок раздела «Protobuf — что это»Protocol Buffers (protobuf) — schema-based binary serialization формат от Google. Меньше JSON в ~5-10 раз, парсинг быстрее в ~2-5 раз. Используется в gRPC, Apache Pulsar, многих внутренних API Google.
proto3 syntax (default для gRPC):
syntax = "proto3";package user.v1;option go_package = "example.com/user/v1;userv1";
message User { string id = 1; string email = 2; int32 age = 3; repeated string roles = 4; optional string nickname = 5; // proto3 optional с keyword map<string, string> attrs = 6; oneof source { string web = 7; string mobile = 8; } google.protobuf.Timestamp created_at = 9;}2. Глубокое погружение (под капотом)
Заголовок раздела «2. Глубокое погружение (под капотом)»Что внутри TLS handshake (детально)
Заголовок раздела «Что внутри TLS handshake (детально)»TLS 1.3 ClientHello (RFC 8446 §4.1.2):
ClientHello { version = TLS 1.2 (legacy, real version в extension) random[32] legacy_session_id[0..32] cipher_suites[2..2^16-2] legacy_compression_methods[1..2^8-1] = [null] extensions: supported_versions = [TLS 1.3, TLS 1.2] key_share = [ECDHE point for X25519] signature_algorithms = [...] supported_groups = [X25519, secp256r1, ...] server_name = "example.com" (SNI) application_layer_protocol_negotiation = ["h2", "http/1.1"] (ALPN) psk_key_exchange_modes = [...] pre_shared_key = <если 0-RTT>}ServerHello + EncryptedExtensions + Certificate + CertificateVerify + Finished
Всё после ServerHello — encrypted handshake (TLS 1.3 feature, TLS 1.2 был plain).
ECDHE: forward secrecy
Заголовок раздела «ECDHE: forward secrecy»Каждое соединение использует ephemeral key exchange (Diffie-Hellman). Даже если приватный ключ сервера скомпрометирован завтра, прошлые сессии остаются нечитаемыми (forward secrecy).
В TLS 1.3 — единственный режим (RSA key exchange удалён).
Cipher suites
Заголовок раздела «Cipher suites»TLS 1.3 (только 5 suites):
- 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
Все AEAD (Authenticated Encryption with Associated Data) — combined encryption + MAC.
TLS 1.2 имел много «плохих» вариантов: RC4, 3DES, RSA key exchange, CBC mode with MAC-then-encrypt (vulnerable to padding oracle).
SNI (Server Name Indication)
Заголовок раздела «SNI (Server Name Indication)»В TLS handshake клиент шлёт server_name extension → сервер знает, какой сертификат подгружать. Это позволяет hosting нескольких HTTPS-сайтов на одном IP.
Client (Hello): server_name = "blog.example.com"Server: подгружает соответствующий cert (например, blog.example.com.pem)⚠️ SNI отправляется plain text (даже в TLS 1.3). Решение: ECH (Encrypted Client Hello) — пока experimental.
ALPN (Application-Layer Protocol Negotiation)
Заголовок раздела «ALPN (Application-Layer Protocol Negotiation)»Клиент шлёт список протоколов в extension, сервер выбирает один:
Client: ALPN = ["h2", "http/1.1"]Server: ALPN = "h2" → HTTP/2 over TLS
Client: ALPN = ["h3", "h2", "http/1.1"]Server: ALPN = "h3" → HTTP/3 over QUIC (если поверх QUIC)tlsConfig := &tls.Config{ NextProtos: []string{"h2", "http/1.1"},}Session resumption
Заголовок раздела «Session resumption»TLS 1.2: Session ID или Session Ticket → 1-RTT при повторе.
TLS 1.3: PSK (Pre-Shared Key) → 0-RTT возможен:
Client (Hello): pre_shared_key = <ticket> + early_data extension + 0-RTT application data в этом же flightServer: если accepted → читает 0-RTT data сразу⚠️ 0-RTT replay-vulnerable. Атакующий может записать пакет и replay’ить → дубликат запроса. Использовать только для idempotent методов.
Certificate chain validation
Заголовок раздела «Certificate chain validation»Root CA (self-signed, в trust store ОС) └── Intermediate CA (подписан Root) └── Server Cert (подписан Intermediate)Сервер при handshake шлёт:
- Server Cert
- Intermediate CA cert(s)
Клиент валидирует chain:
- Server Cert signature ↔ Intermediate’s public key
- Intermediate signature ↔ Root public key
- Root в local trust store?
Если chain incomplete → клиент x509: certificate signed by unknown authority.
⚠️ Не забывайте включать intermediate certs в server’s chain! Браузеры могут кэшировать intermediates, но Go HTTP client — нет.
OCSP, CRL, certificate revocation
Заголовок раздела «OCSP, CRL, certificate revocation»CRL (Certificate Revocation List) — список отозванных certs. Большой, обновляется редко.
OCSP (Online Certificate Status Protocol) — запрос статуса конкретного cert.
OCSP stapling — сервер сам периодически запрашивает OCSP и прикладывает response к handshake. Клиенту не нужно отдельно запрашивать.
В Go:
tlsConfig := &tls.Config{ GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { cert, _ := tls.LoadX509KeyPair("cert.pem", "key.pem") ocspResp, _ := fetchOCSP(&cert) cert.OCSPStaple = ocspResp return &cert, nil },}crypto/tls пакет: ключевые типы
Заголовок раздела «crypto/tls пакет: ключевые типы»type Config struct { Rand io.Reader Time func() time.Time Certificates []tls.Certificate // обычно один или несколько по SNI GetCertificate func(*ClientHelloInfo) (*Certificate, error) ClientAuth ClientAuthType // RequireAndVerify... и т.д. ClientCAs *x509.CertPool // для mTLS RootCAs *x509.CertPool // override system trust store NextProtos []string // ALPN ServerName string // для client SNI MinVersion uint16 MaxVersion uint16 CipherSuites []uint16 InsecureSkipVerify bool // ⚠️ только для теста VerifyPeerCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error VerifyConnection func(ConnectionState) error}ClientAuth types
Заголовок раздела «ClientAuth types»const ( NoClientCert ClientAuthType = iota // server не просит cert RequestClientCert // просит, но не валидирует RequireAnyClientCert // требует, но не валидирует chain VerifyClientCertIfGiven // если предоставлен — валидирует RequireAndVerifyClientCert // требует + валидирует)Для mTLS — почти всегда RequireAndVerifyClientCert.
Custom verification
Заголовок раздела «Custom verification»tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, chains [][]*x509.Certificate) error { if len(chains) == 0 { return errors.New("no verified chain") } leaf := chains[0][0] // Проверяем, например, что CN matches expected service identity if !strings.HasSuffix(leaf.Subject.CommonName, ".internal") { return fmt.Errorf("unexpected CN: %s", leaf.Subject.CommonName) } return nil}SPIFFE & SVID
Заголовок раздела «SPIFFE & SVID»SPIFFE (Secure Production Identity Framework) — стандарт workload identity. SVID (SPIFFE Verifiable Identity Document) — X.509 cert или JWT с URI SAN:
URI: spiffe://example.com/ns/prod/sa/paymentsSPIRE — реализация SPIFFE. Workload получает SVID при старте, ротация автоматическая.
// Использование go-spiffe:import "github.com/spiffe/go-spiffe/v2/workloadapi"import "github.com/spiffe/go-spiffe/v2/spiffeid"import "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
source, _ := workloadapi.NewX509Source(context.Background())expected := spiffeid.RequireFromString("spiffe://example.com/ns/prod/sa/api")tlsConfig := tlsconfig.MTLSServerConfig(source, source, tlsconfig.AuthorizeID(expected))autocert (Let’s Encrypt)
Заголовок раздела «autocert (Let’s Encrypt)»import "golang.org/x/crypto/acme/autocert"
m := &autocert.Manager{ Cache: autocert.DirCache("/var/cache/acme"), Prompt: autocert.AcceptTOS, HostPolicy: autocert.HostWhitelist("example.com", "www.example.com"),}srv := &http.Server{ Addr: ":443", TLSConfig: &tls.Config{ GetCertificate: m.GetCertificate, MinVersion: tls.VersionTLS13, },}go http.ListenAndServe(":80", m.HTTPHandler(nil)) // HTTP-01 challengesrv.ListenAndServeTLS("", "")⚠️ В Kubernetes лучше использовать cert-manager — он управляет cert lifecycle через CRDs.
Cert reload без рестарта
Заголовок раздела «Cert reload без рестарта»type certReloader struct { certPath, keyPath string cert atomic.Pointer[tls.Certificate]}
func (c *certReloader) load() error { cert, err := tls.LoadX509KeyPair(c.certPath, c.keyPath) if err != nil { return err } c.cert.Store(&cert) return nil}
func (c *certReloader) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) { return c.cert.Load(), nil}
// Periodic reload (или через fsnotify)go func() { for range time.Tick(1 * time.Hour) { if err := r.load(); err != nil { log.Println("reload:", err) } }}()
tlsConfig.GetCertificate = r.GetCertificateProtobuf под капотом
Заголовок раздела «Protobuf под капотом»Wire format: каждое поле кодируется как (field_number << 3) | wire_type:
Wire types: 0 — VARINT (int32, int64, uint32, uint64, bool, enum) 1 — I64 (fixed64, sfixed64, double) 2 — LEN (string, bytes, embedded message, packed repeated) 5 — I32 (fixed32, sfixed32, float) 3, 4 — Group (deprecated)Пример: message Foo { int32 id = 1; string name = 2; } с id=150, name=“Bob”:
Tag for id: (1 << 3) | 0 = 0x08VARINT 150: 0x96 0x01 (varint encoding)
Tag for name: (2 << 3) | 2 = 0x12LEN 3: 0x03"Bob": 0x42 0x6f 0x62
Итого: 08 96 01 12 03 42 6f 62 (8 байт)JSON эквивалент: {"id":150,"name":"Bob"} = 22 байта.
Field numbers: почему 1-15 особенные
Заголовок раздела «Field numbers: почему 1-15 особенные»Tag = (field_num << 3) | wire_type.
- field_num 1-15: tag в 1 байт (5 бит для field_num + 3 бита wire_type).
- field_num 16-2047: 2 байта (varint).
- field_num 2048+: 3 байта.
⚠️ Самые горячие поля (часто отправляемые) кладите 1-15.
Varint encoding
Заголовок раздела «Varint encoding»150 in binary: 10010110Split в группы по 7 бит: 0000001 0010110MSB = 1 если есть продолжение: 10010110 (continuation) 00000001 (end) = 0x96 0x01Маленькие числа = меньше байт. Отрицательные — лучше использовать sint32/sint64 (zigzag encoding), иначе int32 -1 = 10 байт (varint of 0xFFFFFFFFFFFFFFFF).
Field types
Заголовок раздела «Field types»message AllTypes { int32 a = 1; // varint, signed (но отрицательные = 10 байт!) sint32 b = 2; // varint zigzag (-1 → 1 байт) uint32 c = 3; // varint, unsigned fixed32 d = 4; // fixed 4 байта, всегда sfixed32 e = 5; // fixed 4 байта signed int64 f = 6; string g = 7; // UTF-8, length-prefixed bytes h = 8; // raw bytes bool i = 9; float j = 10; // 4 байта (IEEE 754) double k = 11; // 8 байт enum Status { OK = 0; ERR = 1; } Status m = 12;}Repeated, optional, map, oneof
Заголовок раздела «Repeated, optional, map, oneof»message Demo { repeated string tags = 1; // packed encoding для scalar (default proto3) optional string nickname = 2; // proto3 optional — explicit "set" tracking map<string, int32> counts = 3; // syntactic sugar над repeated MapEntry oneof source { string url = 4; bytes blob = 5; }}Repeated proto3: scalar fields по умолчанию packed (один tag + length + serialized values).
Optional in proto3: возвращён как keyword (PROTO3 v3.15+). Без него — нельзя отличить unset от default value (0, "", false).
Map: сахар над repeated MapEntry { key, value }. Порядок не сохраняется!
Oneof: только одно из полей может быть set. Установка одного очищает другие.
Well-known types
Заголовок раздела «Well-known types»import "google/protobuf/timestamp.proto";import "google/protobuf/duration.proto";import "google/protobuf/empty.proto";import "google/protobuf/any.proto";import "google/protobuf/struct.proto";import "google/protobuf/wrappers.proto";
message Order { google.protobuf.Timestamp created_at = 1; // seconds + nanos google.protobuf.Duration ttl = 2; google.protobuf.Empty metadata = 3; google.protobuf.Any payload = 4; // dynamic type google.protobuf.Struct raw_json = 5; // JSON-like (Value/ListValue) google.protobuf.StringValue note = 6; // nullable string}Schema evolution rules
Заголовок раздела «Schema evolution rules»| Действие | Совместимо? |
|---|---|
| Добавить новое поле (любого типа) | ДА |
| Удалить поле | ДА (но reserve field_number и name!) |
| Переименовать поле (имя) | ДА (важен только field_number) |
| Изменить field_number | НЕТ |
| Изменить тип int32 ↔ uint32, bool, enum | ДА (wire compat) |
| Изменить sint32 ↔ int32 | НЕТ (zigzag отличается) |
| Изменить fixed32 ↔ int32 | НЕТ (wire type другой) |
| Изменить string ↔ bytes | ДА |
| repeated ↔ singular | Частично (packed!) |
| Добавить поле в oneof | ДА |
| Удалить или переместить из oneof | Опасно |
Зарезервируйте номера/имена удалённых полей:
message User { reserved 3, 5, 7 to 9; reserved "old_field_name"; string id = 1; string email = 2;}Это защищает от случайного reuse номера (и старые сериализованные данные не будут «всплывать» в новых полях).
protoc и плагины
Заголовок раздела «protoc и плагины»# Стандартный proto-генераторprotoc --go_out=. --go_opt=paths=source_relative *.proto
# gRPC stubsprotoc --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto
# Validate (validateRequest tag)protoc --validate_out="lang=go:." *.proto
# OpenAPI v2 (для gRPC-Gateway)protoc --openapiv2_out=. *.proto
# Connect-Go stubsprotoc --connect-go_out=. *.protoBuf — modern build tool
Заголовок раздела «Buf — modern build tool»buf.yaml:
version: v2modules: - path: protolint: use: - STANDARD except: - PACKAGE_VERSION_SUFFIXbreaking: use: - FILEdeps: - buf.build/googleapis/googleapisbuf.gen.yaml:
version: v2plugins: - remote: buf.build/protocolbuffers/go out: gen opt: paths=source_relative - remote: buf.build/grpc/go out: gen opt: paths=source_relative - remote: buf.build/grpc-ecosystem/gateway out: gen opt: paths=source_relativeКоманды:
buf lint # lint proto файловbuf format -w # форматированиеbuf breaking --against '.git#branch=main' # CI checkbuf generate # генерация кодаbuf push buf.build/myorg/mymodule # push в BSRPerformance benchmarks
Заголовок раздела «Performance benchmarks»| Format | Size (vs JSON) | Encode speed | Decode speed | Schema |
|---|---|---|---|---|
| JSON | baseline (100%) | baseline | baseline | optional |
| Protobuf | 20-30% | 2-5x | 2-5x | required |
| MessagePack | 35-50% | 2-3x | 2-3x | optional |
| Avro | 25-35% | 2-3x | 2-3x | required |
| FlatBuffers | similar to PB | 5-10x | 5-10x | required, zero-copy |
| Cap’n Proto | similar | similar to FB | similar to FB | required, zero-copy |
3. Gotchas (⚠️)
Заголовок раздела «3. Gotchas (⚠️)»-
⚠️
InsecureSkipVerify: trueв продакшене — катастрофа. MITM возможна. Использовать только для разовых тестов с self-signed cert. -
⚠️ Cert expiration → outage. Без monitoring expiry вы узнаёте об истёкшем сертификате от пользователей. Метрика
cert_expiry_seconds < 7d→ alert. -
⚠️ Hostname verification. Если cert на
api.example.com, а вы connecting наlocalhost:443—tls.Config.ServerName = "api.example.com"обязателен, иначе verify fails. -
⚠️ TLS 1.2 default cipher suites включают weak ones. В Go 1.22+ default unsafe suites убраны, но для compat custom CipherSuites должны быть осознанные.
-
⚠️ SNI отправляется plain text. Eavesdropper видит, какой host вы запрашиваете. ECH в pipeline, но пока не в Go стандартно.
-
⚠️ Intermediate certs обязательны в server chain. Без них клиенты с пустыми intermediate caches падают.
-
⚠️
tls.Config.PreferServerCipherSuitesdeprecated в TLS 1.3. В TLS 1.3 cipher выбирается из server’s order только; в TLS 1.2 этот флаг работает. -
⚠️ Если
Renegotiation≠ Never — protocol attack possible. Default Never — оставьте. -
⚠️
MinVersion: tls.VersionTLS10— небезопасно (POODLE, BEAST). Default TLS 1.2 в Go 1.22+ — ОК, но рекомендуется TLS 1.3. -
⚠️ Cert key permissions.
chmod 0600 server.key(otherwise other users on machine могут читать). В k8s — Secret. -
⚠️ HSTS header (
Strict-Transport-Security) — обязателен на public HTTPS. Без него браузер может попасться в downgrade attack.
-
⚠️ Verify только trust chain, не identity. Если ClientCAs включает «всё», любой клиент с валидным cert этой CA пройдёт. Используйте
VerifyPeerCertificateдля проверки identity (CN, SAN, SPIFFE ID). -
⚠️ Cert rotation в mTLS пairs: server и client должны обновляться согласованно. Иначе outage. Лучше — SPIRE / cert-manager.
Protobuf
Заголовок раздела «Protobuf»-
⚠️ Изменение field type ломает старые данные.
int32 → int64обычно ОК, ноint32 → sint32— нет (другой varint). -
⚠️ Repeated proto3 scalars packed по умолчанию. Если у вас старый код, ожидающий unpacked → ошибка. Решение:
[packed=false](но для proto3 это редко нужно). -
⚠️ optional в proto3 нужен явный keyword. Без него вы не можете отличить «не задано» от «default value». Раньше использовали wrapper types (StringValue) — теперь optional.
-
⚠️ Default value «0» для enum. Первое значение enum должно быть UNSPECIFIED = 0. Без этого «0» путается с реальным enum value.
enum Status {STATUS_UNSPECIFIED = 0; // ← обязательно для гигиеныSTATUS_OK = 1;STATUS_ERROR = 2;} -
⚠️ JSON-сериализация proto: имена меняются.
field_nameв .proto →fieldNameв JSON (camelCase). Используйтеoption (gogoproto.json_name). Стандартный protojson следует этим правилам. -
⚠️ google.protobuf.Any опасен. Содержит type_url + bytes. Раскрытие требует знания зарегистрированного типа. Часто причина hidden типов в чужих сервисах.
-
⚠️ Map ordering не гарантирован. При сериализации map — итерация в произвольном порядке. Для deterministic — используйте repeated KeyValue.
-
⚠️ Buf breaking check не ловит всего. Например, переименование field (name change, не number) — не breaking на wire, но breaking для JSON/code. Buf проверяет преимущественно wire compat.
-
⚠️ Размер «schema-less» Any/Struct payload. Если ваш RPC принимает Any — фактически вы теряете schema enforcement.
4. Production-практики
Заголовок раздела «4. Production-практики»Безопасная конфигурация tls.Config
Заголовок раздела «Безопасная конфигурация tls.Config»func tlsConfig() *tls.Config { return &tls.Config{ MinVersion: tls.VersionTLS13, // CipherSuites не нужен для TLS 1.3 (autoselected) // Для legacy TLS 1.2 fallback: CipherSuites: []uint16{ tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, }, CurvePreferences: []tls.CurveID{ tls.X25519, tls.CurveP256, }, Renegotiation: tls.RenegotiateNever, NextProtos: []string{"h2", "http/1.1"}, }}TLS reload через fsnotify
Заголовок раздела «TLS reload через fsnotify»import "github.com/fsnotify/fsnotify"
func watchCerts(certPath, keyPath string, r *certReloader) error { w, err := fsnotify.NewWatcher() if err != nil { return err } go func() { for ev := range w.Events { if ev.Op&(fsnotify.Write|fsnotify.Create) != 0 { if err := r.load(); err != nil { log.Println("reload err:", err) } else { log.Println("certs reloaded") } } } }() return w.Add(filepath.Dir(certPath))}mTLS internal service
Заголовок раздела «mTLS internal service»// Serverfunc mtlsServer() *http.Server { cert, _ := tls.LoadX509KeyPair("server.crt", "server.key") caBytes, _ := os.ReadFile("ca.crt") pool := x509.NewCertPool() pool.AppendCertsFromPEM(caBytes)
return &http.Server{ Addr: ":8443", TLSConfig: &tls.Config{ MinVersion: tls.VersionTLS13, Certificates: []tls.Certificate{cert}, ClientAuth: tls.RequireAndVerifyClientCert, ClientCAs: pool, VerifyConnection: func(cs tls.ConnectionState) error { if len(cs.PeerCertificates) == 0 { return errors.New("no client cert") } leaf := cs.PeerCertificates[0] allowed := map[string]bool{ "spiffe://prod/api": true, "spiffe://prod/worker": true, } for _, uri := range leaf.URIs { if allowed[uri.String()] { return nil } } return errors.New("untrusted peer identity") }, }, }}
// Clientfunc mtlsClient() *http.Client { cert, _ := tls.LoadX509KeyPair("client.crt", "client.key") caBytes, _ := os.ReadFile("ca.crt") pool := x509.NewCertPool() pool.AppendCertsFromPEM(caBytes)
return &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ MinVersion: tls.VersionTLS13, Certificates: []tls.Certificate{cert}, RootCAs: pool, }, }, }}TLS termination — где?
Заголовок раздела «TLS termination — где?» Внешний клиент │ ▼ TLS ┌──────────────┐ │ LB / WAF │ <-- terminate TLS (популярный вариант) └──────┬───────┘ ▼ HTTP cleartext ┌──────────────┐ │ App Server │ └──────────────┘vs
Внешний клиент │ ▼ TLS ┌──────────────┐ │ LB │ <-- TLS passthrough (L4) └──────┬───────┘ ▼ TLS (untouched) ┌──────────────┐ │ App Server │ <-- terminate здесь └──────────────┘В service mesh: mTLS между всеми pods, terminate в sidecar (Envoy/Linkerd).
cert-manager в Kubernetes
Заголовок раздела «cert-manager в Kubernetes»apiVersion: cert-manager.io/v1kind: Issuermetadata: name: letsencrypt-prodspec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: admin@example.com privateKeySecretRef: name: letsencrypt-prod solvers: - http01: ingress: class: nginx---apiVersion: cert-manager.io/v1kind: Certificatemetadata: name: api-certspec: secretName: api-cert-tls issuerRef: name: letsencrypt-prod kind: Issuer dnsNames: - api.example.com renewBefore: 720hDiagnostics
Заголовок раздела «Diagnostics»# Inspect certopenssl x509 -in cert.pem -noout -textopenssl x509 -in cert.pem -noout -subject -issuer -dates -ext subjectAltName
# Check chainopenssl verify -CAfile ca.pem -untrusted intermediate.pem server.pem
# Live connection inspectopenssl s_client -connect example.com:443 -servername example.com \ -alpn h2,http/1.1 -tlsextdebug -tls1_3
# Расшифровать pcap (если есть private key, не PFS):ssldump -A -d -k server.key -i eth0 port 443# Wireshark: Edit → Preferences → Protocols → TLS → (Pre)-Master-Secret log
# Для TLS 1.3 (forward secret) — нужны session keys:# Запустите client с SSLKEYLOGFILE=keys.log# В Wireshark укажите keys.log в TLS settingsProtobuf code generation pipeline
Заголовок раздела «Protobuf code generation pipeline»proto/ gen/├── chat/v1/ ├── chat/v1/│ ├── chat.proto ─→ │ ├── chat.pb.go│ └── messages.proto │ ├── chat_grpc.pb.go└── user/v1/ │ ├── chat.openapi.yaml └── user.proto ─→ │ └── messages.pb.go └── user/v1/ └── user.pb.goVersioning protobuf
Заголовок раздела «Versioning protobuf»package user.v1; // первая версияpackage user.v2; // breaking changes — новый packagepackage user.v1beta1; // экспериментальная⚠️ Не редактируйте опубликованные v1 — добавляйте v2.
Тестирование совместимости
Заголовок раздела «Тестирование совместимости»func TestProtoForwardCompat(t *testing.T) { // Сериализуем новой версией newMsg := &userv2.User{Id: "1", Email: "a@b", NewField: "value"} data, _ := proto.Marshal(newMsg)
// Десериализуем старой версией oldMsg := &userv1.User{} err := proto.Unmarshal(data, oldMsg) require.NoError(t, err) require.Equal(t, "1", oldMsg.Id) // unknown fields сохраняются для re-serialization}Validation
Заголовок раздела «Validation»import "validate/validate.proto";
message CreateUserRequest { string email = 1 [(validate.rules).string.email = true]; int32 age = 2 [(validate.rules).int32 = {gte: 0, lte: 150}]; repeated string roles = 3 [ (validate.rules).repeated = {min_items: 1, max_items: 10} ];}// generatedif err := req.Validate(); err != nil { return nil, status.Error(codes.InvalidArgument, err.Error())}Schema registry (Buf BSR)
Заголовок раздела «Schema registry (Buf BSR)»buf push buf.build/myorg/api # push schemas в BSR# Другие могут потреблять:# buf.gen.yaml:# inputs: [{module: "buf.build/myorg/api"}]Преимущества: централизованное хранилище, breaking change checks в CI, шаринг между командами.
5. Вопросы (25+)
Заголовок раздела «5. Вопросы (25+)»-
Чем TLS 1.2 отличается от TLS 1.3? 1.3: 1-RTT handshake (0-RTT при resumption), encrypted handshake, только AEAD cipher suites, forward secrecy mandatory (ECDHE), нет renegotiation, нет RSA key exchange, упрощённый список suites.
-
Что такое forward secrecy? Свойство, при котором компрометация long-term private key не позволяет расшифровать прошлые сессии. Достигается через ephemeral key exchange (ECDHE).
-
Зачем нужен SNI? Server Name Indication — extension в ClientHello, позволяющий server выбрать правильный сертификат для запрашиваемого hostname (когда один IP обслуживает несколько HTTPS-сайтов).
-
Что такое ALPN? Application-Layer Protocol Negotiation — расширение TLS, клиент шлёт список протоколов (h2, http/1.1, h3), server выбирает один. Без ALPN включить HTTP/2 over TLS невозможно.
-
Что такое OCSP stapling? Сервер периодически запрашивает у CA статус сертификата (OCSP) и прикладывает signed response к TLS handshake. Клиенту не нужно отдельно запрашивать.
-
Что в TLS chain validation? Клиент проверяет цепочку: server cert ← intermediate(s) ← root CA. Root должен быть в trust store. Каждое звено проверяется через signature verification.
-
Что плохого в InsecureSkipVerify: true? Отключает hostname verification и chain validation → MITM possible. Только для теста с self-signed.
-
Как настроить TLS 1.3 в Go?
tls.Config{MinVersion: tls.VersionTLS13}Cipher suites выбираются автоматически из 5 встроенных.
-
Что такое 0-RTT и какие риски? PSK-based resumption, application data в первом flight (0 RTT to first byte). Уязвимость: replay attack — атакующий повторяет пакет → дубликат запроса. Только для idempotent методов.
-
Зачем PreferServerCipherSuites? В TLS 1.2 — серверу выбирать suite (а не клиенту). В TLS 1.3 эта настройка deprecated.
-
Что такое mTLS? TLS, где обе стороны (client и server) аутентифицируют друг друга через сертификаты. Server требует client cert через CertificateRequest.
-
Какой ClientAuth выбрать для production mTLS?
RequireAndVerifyClientCert— обязательный cert + полная chain validation. -
Как кастомизировать verification в mTLS?
VerifyPeerCertificate— после default chain check, ваша логика (например, проверить SPIFFE URI). -
Что такое SPIFFE/SPIRE? SPIFFE — стандарт workload identity, SVID — X.509 cert или JWT с URI вида
spiffe://trust-domain/path. SPIRE — реализация (server + agent в каждом узле). -
Чем mTLS лучше bearer tokens?
- Identity встроена в transport (нельзя «забыть» отправить).
- Ротация автоматическая через SPIRE / cert-manager.
- Двусторонняя аутентификация.
- Меньше attack surface (token leakage less impactful).
-
Чем service mesh облегчает mTLS? Istio/Linkerd автоматически выпускают и ротируют certs, конфигурируют sidecar proxy → приложение не знает про TLS. Mesh CA автоматизирует trust chain.
Protobuf
Заголовок раздела «Protobuf»-
Чем wire format protobuf отличается от JSON? Binary, schema-required, varint encoding, поля идентифицируются по числу (не имени). Размер 20-30% от JSON, парсинг 2-5x быстрее.
-
Что такое field number и почему важен 1-15? Уникальный идентификатор поля в wire format. Tag =
(num << 3) | wire_type. Для num 1-15 — tag 1 байт, для 16+ — 2+ байт. Горячие поля кладут 1-15. -
Какие wire types в proto? 0 — VARINT, 1 — I64, 2 — LEN (string/bytes/message), 5 — I32. Wire type определяет, как parser читает payload.
-
Чем proto3 отличается от proto2? proto3 убрал required и default values, ввёл оптимизированную сериализацию для default (zero) values, упростил синтаксис. proto3 v3.15+ вернул
optionalkeyword. -
Что такое well-known types? Предопределённые messages: Timestamp, Duration, Empty, Any, Struct, StringValue (и др wrappers). Они нативно поддерживаются разными языками и JSON-сериализацией.
-
Когда использовать optional в proto3? Когда нужно различать «не задано» от default (0, "", false). Без optional эта разница теряется. Например, для
int32 score = 1: 0 = «не задано» или «реально 0»? -
Какие schema evolution правила?
- Можно: добавить новое поле, удалить поле (с reserved!), переименовать field name.
- Нельзя: изменить field_number, изменить wire-incompatible тип.
- Запасные правила: всегда reserve старые номера и имена.
-
Зачем
reserved? Защита от случайного reuse удалённого field number или имени. Иначе старые сериализованные данные «всплывут» в новых полях. -
Чем protoc отличается от Buf? protoc — компилятор. Buf — build tool с lint, breaking change check, schema registry, унифицированной конфигурацией. Использует те же протобуф-плагины, но удобнее обвязка.
-
Что такое gRPC reflection и связано ли это с protobuf? Reflection — runtime API, через который клиент может получить .proto schema от server. Позволяет grpcurl/Postman работать без локального .proto файла.
-
Какие альтернативы protobuf? JSON (текст, медленнее), MessagePack (binary, schema-less), Avro (schema через registry), FlatBuffers (zero-copy), Cap’n Proto (zero-copy), Arrow (для аналитики).
-
Как тестировать backward compatibility схемы?
buf breaking --againstв CI. Unit-тест: сериализация новой версией → десериализация старой → проверка остальных полей. -
Что делать, если нужен «динамический» тип в proto? google.protobuf.Any — содержит type_url + serialized bytes. Клиент должен знать тип для распаковки. Альтернатива — google.protobuf.Struct (JSON-like Value).
-
Почему первое значение enum рекомендуется UNSPECIFIED? В proto3 default = 0. Если первое — реальный value, нельзя отличить «не задано» от «реально это значение». UNSPECIFIED = 0 решает.
6. Practice
Заголовок раздела «6. Practice»Задача 1 — Self-signed HTTPS server
Заголовок раздела «Задача 1 — Self-signed HTTPS server»Сгенерируйте cert/key через openssl. Запустите Go HTTPS server на :8443 с TLS 1.3. Проверьте через curl --cacert ca.pem https://localhost:8443/.
Задача 2 — mTLS
Заголовок раздела «Задача 2 — mTLS»Создайте CA, выпустите два cert’а (server, client). Сервер: requires client cert. Клиент: с cert делает запрос. Без cert — отказ.
Задача 3 — Cert hot reload
Заголовок раздела «Задача 3 — Cert hot reload»Реализуйте tls.Config.GetCertificate, который читает cert каждый раз из файла + кэш. Через fsnotify обнаруживайте изменение и инвалидируйте кэш.
Задача 4 — SNI
Заголовок раздела «Задача 4 — SNI»Поднимите server, обслуживающий 2 hosts (a.local, b.local) с разными сертами. Через GetCertificate выбирайте cert по ClientHelloInfo.ServerName. Проверьте через curl.
Задача 5 — Custom verification
Заголовок раздела «Задача 5 — Custom verification»В mTLS сервере добавьте VerifyPeerCertificate: разрешите только client cert с CN, содержащим prod-. Проверьте: cert с CN prod-api-1 проходит, dev-api-1 — отказ.
Задача 6 — autocert
Заголовок раздела «Задача 6 — autocert»Используйте golang.org/x/crypto/acme/autocert для получения сертификата от Let’s Encrypt (staging environment). HTTP-01 challenge на :80.
Задача 7 — proto schema
Заголовок раздела «Задача 7 — proto schema»Напишите .proto с message User (id, email, age, optional nickname, repeated roles, map attrs). Сгенерируйте Go код через protoc. Сериализуйте и десериализуйте.
Задача 8 — Schema evolution
Заголовок раздела «Задача 8 — Schema evolution»V1: message User { string id=1; string email=2; }. Сериализуйте {id:"1", email:"a@b"}.
V2: добавьте int32 age=3 и reserved 5. Десериализуйте старые байты — should work, age=0.
V3: удалите email → добавьте reserved 2, "email". Десериализуйте V1-байты в V3 — should work, fields ignored.
Задача 9 — Buf workflow
Заголовок раздела «Задача 9 — Buf workflow»Создайте buf.yaml, buf.gen.yaml. Запустите buf lint, buf generate, buf breaking --against '.'. Внесите breaking change (изменить field_number) — buf должен ругаться.
Задача 10 — Validate
Заголовок раздела «Задача 10 — Validate»Используйте protoc-gen-validate. В .proto добавьте (validate.rules).string.email = true. В Go валидируйте requested и возвращайте codes.InvalidArgument если email невалидный.
Задача 11 — TLS keylog для дебага
Заголовок раздела «Задача 11 — TLS keylog для дебага»Установите SSLKEYLOGFILE для Go-клиента (tls.Config.KeyLogWriter). Запишите session keys, откройте pcap в Wireshark с импортированным keylog → видны декодированные TLS записи.
Задача 12 — SPIFFE
Заголовок раздела «Задача 12 — SPIFFE»Установите SPIRE локально. Получите SVID для workload. Используйте go-spiffe для mTLS server/client. Проверьте, что unauthorized SPIFFE ID получает denied.
7. Источники
Заголовок раздела «7. Источники»- RFC 8446 — TLS 1.3: https://datatracker.ietf.org/doc/html/rfc8446
- RFC 5246 — TLS 1.2 (legacy)
- RFC 5280 — X.509 Certificate Profile
- Mozilla Server-Side TLS Recommendations: https://wiki.mozilla.org/Security/Server_Side_TLS
- Go crypto/tls docs: https://pkg.go.dev/crypto/tls
- SPIFFE specification: https://github.com/spiffe/spiffe/blob/main/standards/SPIFFE.md
- SPIRE docs: https://spiffe.io/docs/latest/spire-about/
- cert-manager: https://cert-manager.io/docs/
- Let’s Encrypt / ACME: https://datatracker.ietf.org/doc/html/rfc8555
- Protocol Buffers Language Guide (proto3): https://protobuf.dev/programming-guides/proto3/
- Protobuf encoding (wire format): https://protobuf.dev/programming-guides/encoding/
- Buf documentation: https://buf.build/docs/
- Google API Design Guide (proto best practices): https://google.aip.dev/
- High Performance Browser Networking (Ilya Grigorik), глава 4 — Transport Layer Security