Перейти к содержимому

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.

  1. Базовая концепция
  2. Глубокое погружение (под капотом)
  3. Gotchas (15+)
  4. Production-практики
  5. Вопросы (25+)
  6. Practice
  7. Источники

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.2TLS 1.3
Handshake RTT2 RTT1 RTT (0 RTT с resumption)
Cipher suites~70+ (часто weak)5 modern (AEAD only)
Key exchangeRSA, DHE, ECDHEТолько ECDHE (forward secrecy)
Hash в handshakeMD5+SHA1 → SHA256SHA256/SHA384
Encrypted handshakeНет (handshake plain)Да (после ClientHello)
Session resumptionSession ID или ticketPSK + 0-RTT
RenegotiationДа (источник атак)Нет

В Go 1.22+ default tls.Config.MinVersion = tls.VersionTLS12, рекомендуется ставить tls.VersionTLS13.

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 сразу после.

Client Server
| ──── ClientHello ─────────────────────────> |
| <─── ServerHello ──────────────────────── |
| <─── Certificate ──────────────────────── |
| <─── ServerKeyExchange ─────────────────── |
| <─── ServerHelloDone ────────────────────── |
| ──── ClientKeyExchange ────────────────────> |
| ──── ChangeCipherSpec ────────────────────> |
| ──── Finished ────────────────────────────> |
| <─── ChangeCipherSpec ──────────────────── |
| <─── Finished ─────────────────────────── |
| ──── Application data ────────────────────> |

2 RTT до первой application data.

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
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
openssl 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 = 2048
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no
[req_distinguished_name]
CN = localhost
[req_ext]
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
DNS.2 = *.local
IP.1 = 127.0.0.1
EOF
openssl 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 (mutual TLS) — TLS, где обе стороны аутентифицируют друг друга через сертификаты. Сервер требует client certificate.

Client Server
| ── ClientHello ──────────────────────────> |
| <── ServerHello + Certificate ─────────── |
| <── CertificateRequest ────────────────── | (server просит client cert)
| ── ClientCertificate ────────────────────> |
| ── ClientCertificateVerify ──────────────> | (доказательство владения)
| ── Finished ─────────────────────────────> |
| ...
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"))
}

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;
}

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).

Каждое соединение использует ephemeral key exchange (Diffie-Hellman). Даже если приватный ключ сервера скомпрометирован завтра, прошлые сессии остаются нечитаемыми (forward secrecy).

В TLS 1.3 — единственный режим (RSA key exchange удалён).

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).

В 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.

Клиент шлёт список протоколов в 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"},
}

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 в этом же flight
Server: если accepted → читает 0-RTT data сразу

⚠️ 0-RTT replay-vulnerable. Атакующий может записать пакет и replay’ить → дубликат запроса. Использовать только для idempotent методов.

Root CA (self-signed, в trust store ОС)
└── Intermediate CA (подписан Root)
└── Server Cert (подписан Intermediate)

Сервер при handshake шлёт:

  1. Server Cert
  2. Intermediate CA cert(s)

Клиент валидирует chain:

  1. Server Cert signature ↔ Intermediate’s public key
  2. Intermediate signature ↔ Root public key
  3. Root в local trust store?

Если chain incomplete → клиент x509: certificate signed by unknown authority.

⚠️ Не забывайте включать intermediate certs в server’s chain! Браузеры могут кэшировать intermediates, но Go HTTP client — нет.

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
},
}
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
}
const (
NoClientCert ClientAuthType = iota // server не просит cert
RequestClientCert // просит, но не валидирует
RequireAnyClientCert // требует, но не валидирует chain
VerifyClientCertIfGiven // если предоставлен — валидирует
RequireAndVerifyClientCert // требует + валидирует
)

Для mTLS — почти всегда RequireAndVerifyClientCert.

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 (Secure Production Identity Framework) — стандарт workload identity. SVID (SPIFFE Verifiable Identity Document) — X.509 cert или JWT с URI SAN:

URI: spiffe://example.com/ns/prod/sa/payments

SPIRE — реализация 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))
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 challenge
srv.ListenAndServeTLS("", "")

⚠️ В Kubernetes лучше использовать cert-manager — он управляет cert lifecycle через CRDs.

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.GetCertificate

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 = 0x08
VARINT 150: 0x96 0x01 (varint encoding)
Tag for name: (2 << 3) | 2 = 0x12
LEN 3: 0x03
"Bob": 0x42 0x6f 0x62
Итого: 08 96 01 12 03 42 6f 62 (8 байт)

JSON эквивалент: {"id":150,"name":"Bob"} = 22 байта.

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.

150 in binary: 10010110
Split в группы по 7 бит: 0000001 0010110
MSB = 1 если есть продолжение:
10010110 (continuation) 00000001 (end)
= 0x96 0x01

Маленькие числа = меньше байт. Отрицательные — лучше использовать sint32/sint64 (zigzag encoding), иначе int32 -1 = 10 байт (varint of 0xFFFFFFFFFFFFFFFF).

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;
}
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. Установка одного очищает другие.

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
}
ДействиеСовместимо?
Добавить новое поле (любого типа)ДА
Удалить полеДА (но 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 номера (и старые сериализованные данные не будут «всплывать» в новых полях).

Окно терминала
# Стандартный proto-генератор
protoc --go_out=. --go_opt=paths=source_relative *.proto
# gRPC stubs
protoc --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 stubs
protoc --connect-go_out=. *.proto

buf.yaml:

version: v2
modules:
- path: proto
lint:
use:
- STANDARD
except:
- PACKAGE_VERSION_SUFFIX
breaking:
use:
- FILE
deps:
- buf.build/googleapis/googleapis

buf.gen.yaml:

version: v2
plugins:
- 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 check
buf generate # генерация кода
buf push buf.build/myorg/mymodule # push в BSR
FormatSize (vs JSON)Encode speedDecode speedSchema
JSONbaseline (100%)baselinebaselineoptional
Protobuf20-30%2-5x2-5xrequired
MessagePack35-50%2-3x2-3xoptional
Avro25-35%2-3x2-3xrequired
FlatBufferssimilar to PB5-10x5-10xrequired, zero-copy
Cap’n Protosimilarsimilar to FBsimilar to FBrequired, zero-copy

  1. ⚠️ InsecureSkipVerify: true в продакшене — катастрофа. MITM возможна. Использовать только для разовых тестов с self-signed cert.

  2. ⚠️ Cert expiration → outage. Без monitoring expiry вы узнаёте об истёкшем сертификате от пользователей. Метрика cert_expiry_seconds < 7d → alert.

  3. ⚠️ Hostname verification. Если cert на api.example.com, а вы connecting на localhost:443tls.Config.ServerName = "api.example.com" обязателен, иначе verify fails.

  4. ⚠️ TLS 1.2 default cipher suites включают weak ones. В Go 1.22+ default unsafe suites убраны, но для compat custom CipherSuites должны быть осознанные.

  5. ⚠️ SNI отправляется plain text. Eavesdropper видит, какой host вы запрашиваете. ECH в pipeline, но пока не в Go стандартно.

  6. ⚠️ Intermediate certs обязательны в server chain. Без них клиенты с пустыми intermediate caches падают.

  7. ⚠️ tls.Config.PreferServerCipherSuites deprecated в TLS 1.3. В TLS 1.3 cipher выбирается из server’s order только; в TLS 1.2 этот флаг работает.

  8. ⚠️ Если Renegotiation ≠ Never — protocol attack possible. Default Never — оставьте.

  9. ⚠️ MinVersion: tls.VersionTLS10 — небезопасно (POODLE, BEAST). Default TLS 1.2 в Go 1.22+ — ОК, но рекомендуется TLS 1.3.

  10. ⚠️ Cert key permissions. chmod 0600 server.key (otherwise other users on machine могут читать). В k8s — Secret.

  11. ⚠️ HSTS header (Strict-Transport-Security) — обязателен на public HTTPS. Без него браузер может попасться в downgrade attack.

  1. ⚠️ Verify только trust chain, не identity. Если ClientCAs включает «всё», любой клиент с валидным cert этой CA пройдёт. Используйте VerifyPeerCertificate для проверки identity (CN, SAN, SPIFFE ID).

  2. ⚠️ Cert rotation в mTLS пairs: server и client должны обновляться согласованно. Иначе outage. Лучше — SPIRE / cert-manager.

  1. ⚠️ Изменение field type ломает старые данные. int32 → int64 обычно ОК, но int32 → sint32 — нет (другой varint).

  2. ⚠️ Repeated proto3 scalars packed по умолчанию. Если у вас старый код, ожидающий unpacked → ошибка. Решение: [packed=false] (но для proto3 это редко нужно).

  3. ⚠️ optional в proto3 нужен явный keyword. Без него вы не можете отличить «не задано» от «default value». Раньше использовали wrapper types (StringValue) — теперь optional.

  4. ⚠️ Default value «0» для enum. Первое значение enum должно быть UNSPECIFIED = 0. Без этого «0» путается с реальным enum value.

    enum Status {
    STATUS_UNSPECIFIED = 0; // ← обязательно для гигиены
    STATUS_OK = 1;
    STATUS_ERROR = 2;
    }
  5. ⚠️ JSON-сериализация proto: имена меняются. field_name в .proto → fieldName в JSON (camelCase). Используйте option (gogoproto.json_name). Стандартный protojson следует этим правилам.

  6. ⚠️ google.protobuf.Any опасен. Содержит type_url + bytes. Раскрытие требует знания зарегистрированного типа. Часто причина hidden типов в чужих сервисах.

  7. ⚠️ Map ordering не гарантирован. При сериализации map — итерация в произвольном порядке. Для deterministic — используйте repeated KeyValue.

  8. ⚠️ Buf breaking check не ловит всего. Например, переименование field (name change, не number) — не breaking на wire, но breaking для JSON/code. Buf проверяет преимущественно wire compat.

  9. ⚠️ Размер «schema-less» Any/Struct payload. Если ваш RPC принимает Any — фактически вы теряете schema enforcement.


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"},
}
}
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))
}
// Server
func 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")
},
},
}
}
// Client
func 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
┌──────────────┐
│ 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).

apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: letsencrypt-prod
spec:
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/v1
kind: Certificate
metadata:
name: api-cert
spec:
secretName: api-cert-tls
issuerRef:
name: letsencrypt-prod
kind: Issuer
dnsNames:
- api.example.com
renewBefore: 720h
Окно терминала
# Inspect cert
openssl x509 -in cert.pem -noout -text
openssl x509 -in cert.pem -noout -subject -issuer -dates -ext subjectAltName
# Check chain
openssl verify -CAfile ca.pem -untrusted intermediate.pem server.pem
# Live connection inspect
openssl 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 settings
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.go
package user.v1; // первая версия
package user.v2; // breaking changes — новый package
package 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
}
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}
];
}
// generated
if err := req.Validate(); err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
Окно терминала
buf push buf.build/myorg/api # push schemas в BSR
# Другие могут потреблять:
# buf.gen.yaml:
# inputs: [{module: "buf.build/myorg/api"}]

Преимущества: централизованное хранилище, breaking change checks в CI, шаринг между командами.


  1. Чем 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.

  2. Что такое forward secrecy? Свойство, при котором компрометация long-term private key не позволяет расшифровать прошлые сессии. Достигается через ephemeral key exchange (ECDHE).

  3. Зачем нужен SNI? Server Name Indication — extension в ClientHello, позволяющий server выбрать правильный сертификат для запрашиваемого hostname (когда один IP обслуживает несколько HTTPS-сайтов).

  4. Что такое ALPN? Application-Layer Protocol Negotiation — расширение TLS, клиент шлёт список протоколов (h2, http/1.1, h3), server выбирает один. Без ALPN включить HTTP/2 over TLS невозможно.

  5. Что такое OCSP stapling? Сервер периодически запрашивает у CA статус сертификата (OCSP) и прикладывает signed response к TLS handshake. Клиенту не нужно отдельно запрашивать.

  6. Что в TLS chain validation? Клиент проверяет цепочку: server cert ← intermediate(s) ← root CA. Root должен быть в trust store. Каждое звено проверяется через signature verification.

  7. Что плохого в InsecureSkipVerify: true? Отключает hostname verification и chain validation → MITM possible. Только для теста с self-signed.

  8. Как настроить TLS 1.3 в Go?

    tls.Config{MinVersion: tls.VersionTLS13}

    Cipher suites выбираются автоматически из 5 встроенных.

  9. Что такое 0-RTT и какие риски? PSK-based resumption, application data в первом flight (0 RTT to first byte). Уязвимость: replay attack — атакующий повторяет пакет → дубликат запроса. Только для idempotent методов.

  10. Зачем PreferServerCipherSuites? В TLS 1.2 — серверу выбирать suite (а не клиенту). В TLS 1.3 эта настройка deprecated.

  1. Что такое mTLS? TLS, где обе стороны (client и server) аутентифицируют друг друга через сертификаты. Server требует client cert через CertificateRequest.

  2. Какой ClientAuth выбрать для production mTLS? RequireAndVerifyClientCert — обязательный cert + полная chain validation.

  3. Как кастомизировать verification в mTLS? VerifyPeerCertificate — после default chain check, ваша логика (например, проверить SPIFFE URI).

  4. Что такое SPIFFE/SPIRE? SPIFFE — стандарт workload identity, SVID — X.509 cert или JWT с URI вида spiffe://trust-domain/path. SPIRE — реализация (server + agent в каждом узле).

  5. Чем mTLS лучше bearer tokens?

    • Identity встроена в transport (нельзя «забыть» отправить).
    • Ротация автоматическая через SPIRE / cert-manager.
    • Двусторонняя аутентификация.
    • Меньше attack surface (token leakage less impactful).
  6. Чем service mesh облегчает mTLS? Istio/Linkerd автоматически выпускают и ротируют certs, конфигурируют sidecar proxy → приложение не знает про TLS. Mesh CA автоматизирует trust chain.

  1. Чем wire format protobuf отличается от JSON? Binary, schema-required, varint encoding, поля идентифицируются по числу (не имени). Размер 20-30% от JSON, парсинг 2-5x быстрее.

  2. Что такое field number и почему важен 1-15? Уникальный идентификатор поля в wire format. Tag = (num << 3) | wire_type. Для num 1-15 — tag 1 байт, для 16+ — 2+ байт. Горячие поля кладут 1-15.

  3. Какие wire types в proto? 0 — VARINT, 1 — I64, 2 — LEN (string/bytes/message), 5 — I32. Wire type определяет, как parser читает payload.

  4. Чем proto3 отличается от proto2? proto3 убрал required и default values, ввёл оптимизированную сериализацию для default (zero) values, упростил синтаксис. proto3 v3.15+ вернул optional keyword.

  5. Что такое well-known types? Предопределённые messages: Timestamp, Duration, Empty, Any, Struct, StringValue (и др wrappers). Они нативно поддерживаются разными языками и JSON-сериализацией.

  6. Когда использовать optional в proto3? Когда нужно различать «не задано» от default (0, "", false). Без optional эта разница теряется. Например, для int32 score = 1: 0 = «не задано» или «реально 0»?

  7. Какие schema evolution правила?

    • Можно: добавить новое поле, удалить поле (с reserved!), переименовать field name.
    • Нельзя: изменить field_number, изменить wire-incompatible тип.
    • Запасные правила: всегда reserve старые номера и имена.
  8. Зачем reserved? Защита от случайного reuse удалённого field number или имени. Иначе старые сериализованные данные «всплывут» в новых полях.

  9. Чем protoc отличается от Buf? protoc — компилятор. Buf — build tool с lint, breaking change check, schema registry, унифицированной конфигурацией. Использует те же протобуф-плагины, но удобнее обвязка.

  10. Что такое gRPC reflection и связано ли это с protobuf? Reflection — runtime API, через который клиент может получить .proto schema от server. Позволяет grpcurl/Postman работать без локального .proto файла.

  11. Какие альтернативы protobuf? JSON (текст, медленнее), MessagePack (binary, schema-less), Avro (schema через registry), FlatBuffers (zero-copy), Cap’n Proto (zero-copy), Arrow (для аналитики).

  12. Как тестировать backward compatibility схемы? buf breaking --against в CI. Unit-тест: сериализация новой версией → десериализация старой → проверка остальных полей.

  13. Что делать, если нужен «динамический» тип в proto? google.protobuf.Any — содержит type_url + serialized bytes. Клиент должен знать тип для распаковки. Альтернатива — google.protobuf.Struct (JSON-like Value).

  14. Почему первое значение enum рекомендуется UNSPECIFIED? В proto3 default = 0. Если первое — реальный value, нельзя отличить «не задано» от «реально это значение». UNSPECIFIED = 0 решает.


Сгенерируйте cert/key через openssl. Запустите Go HTTPS server на :8443 с TLS 1.3. Проверьте через curl --cacert ca.pem https://localhost:8443/.

Создайте CA, выпустите два cert’а (server, client). Сервер: requires client cert. Клиент: с cert делает запрос. Без cert — отказ.

Реализуйте tls.Config.GetCertificate, который читает cert каждый раз из файла + кэш. Через fsnotify обнаруживайте изменение и инвалидируйте кэш.

Поднимите server, обслуживающий 2 hosts (a.local, b.local) с разными сертами. Через GetCertificate выбирайте cert по ClientHelloInfo.ServerName. Проверьте через curl.

В mTLS сервере добавьте VerifyPeerCertificate: разрешите только client cert с CN, содержащим prod-. Проверьте: cert с CN prod-api-1 проходит, dev-api-1 — отказ.

Используйте golang.org/x/crypto/acme/autocert для получения сертификата от Let’s Encrypt (staging environment). HTTP-01 challenge на :80.

Напишите .proto с message User (id, email, age, optional nickname, repeated roles, map attrs). Сгенерируйте Go код через protoc. Сериализуйте и десериализуйте.

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.

Создайте buf.yaml, buf.gen.yaml. Запустите buf lint, buf generate, buf breaking --against '.'. Внесите breaking change (изменить field_number) — buf должен ругаться.

Используйте protoc-gen-validate. В .proto добавьте (validate.rules).string.email = true. В Go валидируйте requested и возвращайте codes.InvalidArgument если email невалидный.

Установите SSLKEYLOGFILE для Go-клиента (tls.Config.KeyLogWriter). Запишите session keys, откройте pcap в Wireshark с импортированным keylog → видны декодированные TLS записи.

Установите SPIRE локально. Получите SVID для workload. Используйте go-spiffe для mTLS server/client. Проверьте, что unauthorized SPIFFE ID получает denied.