Service Account: ให้โค้ดเรียกบริการอื่นโดยไม่ต้องเก็บรหัสผ่าน

ตัวตนสำหรับบริการและโปรแกรมของคุณ ให้สิทธิ์ทีละบริการ แนวคิดเดียวกับ Managed Identity และ App Registration ของ Microsoft Entra ID

มี 2 แบบ

แบบขอ Token จากเหมาะกับ
ฝังกับบริการเครื่องที่บริการรันอยู่ ไม่มี Secret และเรียกจากภายนอกไม่ได้โค้ดใน Container หรือ Function
Client ID และ Secretที่ไหนก็ได้ด้วย Secret บริการก็เลือกใช้ได้เช่นกันเซิร์ฟเวอร์ของคุณ CI หรือ Cloud อื่น

ทุก Project มี Service Account แบบฝังเป็น ค่าเริ่มต้น 1 อัน ทุกบริการใช้อันนี้จนกว่าจะเลือกอันอื่นตอนสร้างหรือในหน้าตั้งค่า และทำอะไรไม่ได้จนกว่าจะให้สิทธิ์

ให้สิทธิ์

  1. เปิด Service Account ใน Console แล้วเลือกหรือสร้างใหม่
  2. ที่หัวข้อ สิทธิ์ เลือกบริการ เลือกตัวที่ต้องการ (หรือทุกตัวใน Project) และระดับสิทธิ์
  3. ใช้ได้ภายในไม่กี่วินาที บริการที่ใช้ Service Account นี้จะเข้าถึง Database, Cache หรือ Container นั้นผ่านเครือข่ายของ Project ได้ด้วย เอาสิทธิ์ออกก็มีผลภายในไม่กี่วินาทีเช่นกัน แม้ Token ที่ออกไปแล้ว
บริการสิทธิ์โค้ดใช้งานอย่างไร
Databaseอ่านอย่างเดียว อ่านและเขียนกด Connect ไม่ต้องมีรหัสผ่าน หรือใช้ Login ชั่วคราวจากภายนอก
Cacheอ่านอย่างเดียว อ่านและเขียนกด Connect ไม่ต้องมีรหัสผ่าน หรือใช้ Login ชั่วคราวจากภายนอก
Object Storageอ่าน เขียน ลบส่ง Token ไปที่ REST API เครื่องมือแบบ S3 ยังต้องใช้ Access Key
Queueส่ง รับ หรือทั้งสองอย่างและล้างคิวส่ง Token ไปที่ REST API
Container และ Functionเรียกใช้ส่ง Token ที่ออกให้บริการนั้นไปที่ที่อยู่สาธารณะของมัน

Database และ Cache: กด Connect

ในหน้า Container หรือ Function เปิดแท็บ เชื่อมต่อ เลือกปลายทางและสิทธิ์ แล้วกด Connect ระบบเพิ่มตัวแปร DATABASE_URL หรือ REDIS_URL ที่ไม่มีรหัสผ่าน โค้ดใช้ URL นี้กับ driver ได้ตามปกติ เครื่องที่รันอยู่จะ Login ให้ ไม่มีรหัสผ่านอยู่ในตัวแปร ในโค้ด หรือในหน้า Console และ URL ใช้ได้เฉพาะจากใน Service นั้น

Dev ที่ Deploy ได้ยังอ่านข้อมูลผ่าน connection นี้ได้ ถ้าไม่ต้องการให้เห็นข้อมูลจริง ให้แยก Production เป็นอีก Project รหัสผ่านหลักของฐานข้อมูลดูได้เฉพาะ Owner และ Admin

โปรแกรมนอกระบบใช้ Service Account แบบมี Client ID และ Secret แลก Token เป็น Login ชั่วคราวตามตัวอย่างด้านล่าง โค้ดใน Container หรือ Function แลกไม่ได้

SDK

ภาษาต้องการติดตั้ง
Node.js18.17+npm install @thailandhosting/identity
Python3.9+pip install thailandhosting-identity
Go1.23+go get github.com/mrzack99s/thailandhosting-identity-sdk-go
Node.js
import { defaultCredential } from '@thailandhosting/identity';
import { Client as Queues } from '@thailandhosting/queue-service';

// In a Container or Function this needs nothing. Elsewhere set
// TH_CLIENT_ID and TH_CLIENT_SECRET.
const cred = defaultCredential();

const queues = new Queues({ region: 'th-bkk-1', tokenProvider: () => cred.getToken() });

// From outside the platform. In a Container or Function, click Connect instead.
const login = await cred.databaseCredentials('orders');
const pool = new pg.Pool({ connectionString: login.private.url });
Python
from thailandhosting_identity import default_credential
from thailandhosting_queueservice import Client as Queues

# In a Container or Function this needs nothing. Elsewhere set
# TH_CLIENT_ID and TH_CLIENT_SECRET.
cred = default_credential()

queues = Queues(region="th-bkk-1", token_provider=cred.get_token)

# From outside the platform. In a Container or Function, click Connect instead.
login = cred.database_credentials("orders")
conn = psycopg.connect(login["private"]["url"])
Go
import identity "github.com/mrzack99s/thailandhosting-identity-sdk-go"

// In a Container or Function this needs nothing. Elsewhere set
// TH_CLIENT_ID and TH_CLIENT_SECRET.
cred, err := identity.Default()

queues, err := queueservice.New(queueservice.Config{Region: "th-bkk-1", TokenSource: cred.TokenSource("")})

// From outside the platform. In a Container or Function, click Connect instead.
login, err := cred.DatabaseCredentials(ctx, "orders")
db, err := sql.Open("pgx", login.Private.URL)

ภาษาอื่น: เรียก HTTP ตรง

จากใน Container หรือ Function ให้ถามเครื่องที่รันอยู่ คำขอต้องมี Metadata: true เก็บคำตอบไว้ใช้จนใกล้ถึง expires_on

Shell
# Inside a Container or Function. The address answers only there.
curl -H 'Metadata: true' http://169.254.169.254/identity/oauth2/token

# For calling another Container, name it as the audience:
curl -H 'Metadata: true' 'http://169.254.169.254/identity/oauth2/token?audience=https://orders.containers.th-bkk-1.thailandhosting.com'

# {"access_token":"eyJ...","token_type":"Bearer","expires_in":3600,"expires_on":1793355792,"client_id":"sa-..."}

ถ้าใช้ Client ID และ Secret ให้ขอจาก Token Endpoint:

Shell
# From anywhere, with a client ID and secret (OAuth 2.0 client credentials).
curl https://api.thailandhosting.com/v1/identity/token \
  -d grant_type=client_credentials \
  -d client_id=$TH_CLIENT_ID \
  -d client_secret=$TH_CLIENT_SECRET

แล้วนำ Token ไปใช้:

Shell
# Queue Service and Object Storage: send the token as a bearer.
curl -H "Authorization: Bearer $TOKEN" https://queues.thailandhosting.com/th-bkk-1/v1/queues/jobs/messages -d '{"body":"hello"}'

# Database or Cache, from outside the platform: exchange the token for a login.
# In a Container or Function, click Connect and use the URL it adds instead.
curl -H "Authorization: Bearer $TOKEN" https://api.thailandhosting.com/v1/identity/database-credentials -d '{"database":"orders"}'
# {"username":"sa_...","password":"...","database":"orders","expiresAt":"...","private":{"host":"orders.internal","port":5432,"url":"postgresql://..."}}

# Another Container or Function: a token made for its address.
curl -H "Authorization: Bearer $TOKEN" https://orders.containers.th-bkk-1.thailandhosting.com/items

คำขอเดียวกันในภาษาอื่น:

Shell
<?php // PHP, inside a Container or Function
$ctx = stream_context_create(['http' => ['header' => "Metadata: true\r\n"]]);
$token = json_decode(file_get_contents('http://169.254.169.254/identity/oauth2/token', false, $ctx))->access_token;
Shell
// Java 11+, inside a Container or Function
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder(URI.create("http://169.254.169.254/identity/oauth2/token"))
    .header("Metadata", "true").build();
var body = client.send(request, HttpResponse.BodyHandlers.ofString()).body();
// Read access_token and expires_on with your JSON library.
Shell
// C#, inside a Container or Function
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("Metadata", "true");
var body = await http.GetStringAsync("http://169.254.169.254/identity/oauth2/token");
var token = JsonDocument.Parse(body).RootElement.GetProperty("access_token").GetString();
Shell
# Ruby, inside a Container or Function
require 'net/http'; require 'json'
uri = URI('http://169.254.169.254/identity/oauth2/token')
token = JSON.parse(Net::HTTP.get(uri, 'Metadata' => 'true'))['access_token']

ตรวจ Token ที่คนอื่นส่งมา

SDK มี Verifier ให้ Token ออกให้ผู้รับ (audience) คนเดียว จึงต้องตรวจว่าเป็นของคุณ Token ที่ส่งให้บริการอื่นจะได้นำมาใช้กับบริการของคุณไม่ได้ ระบบตรวจ Token ให้เฉพาะที่ที่อยู่สาธารณะของ Container ถ้าเรียกตรงผ่านเครือข่ายของ Project ให้ตรวจในโค้ดของคุณ

Shell
# Published keys (JWKS) and discovery, for any JWT library:
https://api.thailandhosting.com/v1/identity/keys
https://api.thailandhosting.com/v1/identity/.well-known/openid-configuration
# Check: alg ES256, iss https://api.thailandhosting.com/v1/identity, aud your own name, exp, nbf.

Token ใช้ได้ 1 ชั่วโมง ใครได้ไปก็ใช้ได้จนหมดอายุแม้จากภายนอก อย่าเขียนลง Log และอย่าส่งให้บริการที่ไม่ไว้ใจ

ถ้าเจอปัญหา

ได้ 403 หรือ no_identity จากที่อยู่ของ Identity

เฉพาะ Container และ Function ที่ขอ Token ที่นี่ได้ เว็บไซต์ Database Cache และ Mail ไม่ได้รันโค้ดของคุณจึงขอไม่ได้

เชื่อมต่อ 169.254.169.254 ไม่ได้

โค้ดไม่ได้รันใน Container หรือ Function ถ้ารันบนเครื่องของคุณ ให้ใช้แบบ Client ID และ Secret แล้วตั้ง TH_CLIENT_ID และ TH_CLIENT_SECRET

Login ของฐานข้อมูลใช้ไม่ได้แล้ว

Login มีอายุเป็นชั่วโมง ให้ขอใหม่ ส่วน Connection ที่เปิดอยู่ยังใช้ต่อได้

ยังแก้ไม่ได้? ให้ทีมงานช่วย

เปิด ticket จากหน้าจัดการ บอกชื่อเว็บหรือบริการ สิ่งที่ทำ และข้อความที่เห็นบนจอ ทีมงานจะตอบกลับในเรื่องเดียวกัน ติดตามได้ในที่เดียว