Service accounts: let your code use other services without passwords

An identity for your services and programs. Grant it access service by service, like managed identities and app registrations in Microsoft Entra ID.

Two kinds

KindGets tokensUse it for
Embedded in a serviceFrom the machine the service runs on. There is no secret, and it cannot be used from outside.Code in a Container or Function
Client ID and secretFrom anywhere, with the secret. A service can also use one.Your own server, CI, another cloud

Every Project has a default embedded account, and every service uses it unless you pick another when creating it or in its settings. It can do nothing until you grant it something.

Grant access

  1. Open Service Accounts in the Console and pick an account, or create one.
  2. Under Access, choose a service, which one (or all in this Project), and the access.
  3. It works within seconds. The services using this account can also reach that Database, Cache or Container over the Project network from then on. Removing access takes effect within seconds too, even for tokens already issued.
ServiceAccessHow your code uses it
DatabaseRead only, read and writeConnects with no password, or a temporary login from outside
CacheRead only, read and writeConnects with no password, or a temporary login from outside
Object StorageRead, write, deleteSends the token to the REST API. S3 tools still need an access key.
QueueSend, receive, or both and purgeSends the token to the REST API
Containers and FunctionsInvokeSends a token made for that service to its public address

Database and Cache: click Connect

On a Container or Function, open Connections, pick the target and the access, and click Connect. It adds a DATABASE_URL or REDIS_URL variable with no password, which your code uses with any driver. The machine it runs on logs in for it, so no password is in the variables, the code or the Console, and the URL works only from inside that service.

Developers who can deploy can still read data through that connection. To keep real data from them, put production in a separate Project. A database's main password is visible only to the Owner and Admins.

Programs outside the platform use a service account with a client ID and secret, exchanging the token for a temporary login as shown below. Code in a Container or Function cannot.

SDKs

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

Other languages: call HTTP directly

From inside a Container or Function, ask the machine it runs on. The request must carry Metadata: true. Keep the answer until shortly before 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-..."}

With a client ID and secret, ask the 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

Then use the 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

The same request in a few languages:

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']

Check a token someone sent you

The SDKs have a Verifier. A token is made for one audience, so check that it is yours: a token sent to another service then cannot be replayed at yours. The platform checks tokens only at a Container's public address, so check them in your code for traffic that comes directly over the Project network.

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.

A token lasts an hour. Anyone holding one can use it until then, even from outside, so keep it out of logs and do not send it to services you do not trust.

If something goes wrong

403 or no_identity from the identity address

Only Containers and Functions get tokens there. Websites, Databases, Caches and Mail do not run your code, so they do not.

Could not connect to 169.254.169.254

The code is not running in a Container or Function. On your own machine use an account with a client ID and secret, and set TH_CLIENT_ID and TH_CLIENT_SECRET.

A database login stopped working

Logins last hours. Ask for credentials again; open connections keep working in the meantime.

Still stuck? Let our team help

Open a ticket from the Console. Tell us the site or service, what you did and the message on screen. Our reply comes back in the same ticket, so everything stays in one place.