Security & Auth
สองคำถาม สองชั้น
หัวข้อที่มีชื่อว่า “สองคำถาม สองชั้น”security ใน gRPC ตอบคำถามที่ต่างกันสองข้อ และสำคัญมากที่จะไม่รวมสองข้อนี้เข้าด้วยกัน:
- connection เป็นส่วนตัวไหม และ server เป็นตัวที่อ้างจริงหรือเปล่า? → transport security (TLS)
- ใครเป็นคน เรียก และเขาได้รับอนุญาตให้เรียก call นี้ไหม? → call authentication (token ปกติอยู่ใน metadata)
gRPC จัดการสองอย่างนี้เป็น credentials สองชนิด: channel credentials (ชั้น transport) และ call credentials (auth ต่อ RPC) เกือบทุกครั้งคุณต้องการทั้งคู่
flowchart TB
subgraph transport["Channel credentials — TLS"]
t["เข้ารหัส connection
+ ยืนยันตัวตน server
(mTLS ยืนยัน client ด้วย)"]
end
subgraph call["Call credentials — ต่อ RPC"]
c["แนบ token ใน metadata
ระบุตัวคนเรียก
ตรวจโดย server interceptor"]
end
transport --> call TLS: เข้ารหัสและระบุตัวตน channel
หัวข้อที่มีชื่อว่า “TLS: เข้ารหัสและระบุตัวตน channel”โดย default channel แบบ plaintext (insecure) โอเคสำหรับ demo บนเครื่อง แต่รับไม่ได้บน production TLS เข้ารหัส connection และให้ client ยืนยัน certificate ของ server ได้ ทำให้ไม่มีใครดักฟังหรือปลอมเป็น server ได้
mTLS (mutual TLS) ไปอีกขั้น: server ก็ยืนยัน certificate ของ client ด้วย นี่คือ pattern ที่พบบ่อยสำหรับ auth แบบ service-to-service ภายใน mesh — ทั้งสองฝั่งพิสูจน์ตัวตนด้วย certificate และไม่ต้องใช้ bearer token สำหรับ machine identity
call credentials: ใครเป็นคนเรียก
หัวข้อที่มีชื่อว่า “call credentials: ใครเป็นคนเรียก”TLS พิสูจน์ตัวตนของ server และของเครื่อง แต่บ่อยครั้งคุณต้องการตัวตนของ ผู้ใช้ ด้วย — bearer token (JWT, OAuth) ที่ส่งต่อ call gRPC แนบ token นี้เป็น call credentials: client ใส่ token authorization ใน metadata ของทุก RPC แล้ว server interceptor ตรวจสอบและ reject ตัวที่ไม่ถูกต้องด้วย UNAUTHENTICATED
call credentials เป็นแบบ ต่อ RPC ดังนั้น call ต่าง ๆ บน channel เดียวกันสามารถถือ token คนละตัวได้ (เช่น channel เดียว ผู้ใช้ปลายทางหลายคน)
เอามารวมกัน
หัวข้อที่มีชื่อว่า “เอามารวมกัน”// Channel credentials: TLS. Call credentials: a token on every RPC.creds := credentials.NewClientTLSFromCert(certPool, "")perRPC := oauth.TokenSource{TokenSource: myTokenSource}
conn, _ := grpc.NewClient( "api.example.com:443", grpc.WithTransportCredentials(creds), // TLS grpc.WithPerRPCCredentials(perRPC), // token per call)# Channel credentials: TLS. Call credentials: a token per RPC.channel_creds = grpc.ssl_channel_credentials(root_certificates=root_certs)call_creds = grpc.access_token_call_credentials(access_token)composite = grpc.composite_channel_credentials(channel_creds, call_creds)
channel = grpc.secure_channel("api.example.com:443", composite)// TLS channel credentials + a per-call metadata token.const ssl = credentials.createSsl(rootCert);const callCreds = credentials.createFromMetadataGenerator((_, cb) => { const md = new Metadata(); md.set('authorization', `Bearer ${token}`); cb(null, md);});const combined = credentials.combineChannelCredentials(ssl, callCreds);const client = new ApiClient('api.example.com:443', combined);ความผิดพลาดที่ต้องเลี่ยง
หัวข้อที่มีชื่อว่า “ความผิดพลาดที่ต้องเลี่ยง”อย่าส่ง bearer token ผ่าน channel ที่ insecure เด็ดขาด gRPC บังคับเรื่องนี้: call credentials จะไม่ยอมแนบกับ connection แบบ plaintext เพราะ token ที่ส่งแบบ cleartext คือ token ที่ใครก็ตามบนเส้นทางขโมยได้ call credentials ต้องมี channel credentials (TLS) อยู่ข้างล่าง — framework กำลังปกป้องคุณจากบั๊ก security ที่พบบ่อยที่สุดของ gRPC