Skip to main content
BlogSecurity

Least Privilege AI Agent: Credential, Token, dan Secret

Desain credential untuk AI agent yang memakai tool: jauhkan secret dari context window, pakai domain_secrets di sandbox, token MCP per request yang terikat audience, satu identitas per tool, dan approval untuk write.

Matthews Wong

Software Engineer

Oktober 202611 menit baca
Least Privilege AI Agent: Credential, Token, dan Secret

Ringkasan Utama

Least privilege untuk AI agent berarti merancang credential dengan asumsi bahwa prompt injection pasti berhasil. Jauhkan semua secret dari context model, ganti credential di luar model memakai placeholder, kirim OAuth token yang terikat audience di setiap request, beri setiap tool identitas dengan scope sempit, dan wajibkan approval manusia sebelum tool apa pun melakukan write.

Bayangkan sebuah agent helpdesk accounts payable yang terhubung ke ERP. Agent ini membaca invoice vendor, menjawab pertanyaan status, dan membuat draft pembayaran. Suatu sore masuk PDF dari vendor yang berisi satu baris teks putih di atas latar putih, menyuruh agent mengekspor daftar master vendor ke alamat luar. Apakah baris itu berakhir menjadi insiden hampir tidak ditentukan oleh prompt. Yang menentukan adalah apa saja yang bisa dijangkau credential agent tersebut, dan apakah ada secret yang pernah berada di tempat yang bisa dibaca model.

Artikel ini membahas lapisan identitas dan secret pada agent, bagian yang membatasi kerusakan setelah injection berhasil lolos. Rujukannya adalah entri OWASP LLM06 Excessive Agency, panduan hosted shell dan remote MCP dari OpenAI, serta spesifikasi authorization MCP 2026-07-28, dan ditutup dengan checklist review yang bisa dijalankan pada agent mana pun sebelum masuk production. Sandboxing dan deteksi injection adalah topik terpisah; pertanyaan di sini lebih sempit: ketika model berhasil dikelabui, apa yang sebenarnya bisa ia lakukan?

Least privilege AI agent dimulai dengan asumsi injection berhasil

OWASP menyebut kegagalan ini Excessive Agency dan membaginya menjadi tiga akar masalah: fungsionalitas berlebih, permission berlebih, dan otonomi berlebih. Masing-masing berpasangan langsung dengan satu keputusan credential, dan karena itulah lapisan identitas menjadi penentu keamanan agent. Filter dan classifier mengurangi seberapa sering injection berhasil; credential menentukan berapa besar kerugiannya ketika itu terjadi.

Akar masalah OWASPWujudnya pada agentKontrol credential yang membatasinya
Fungsionalitas berlebihPeringkas mailbox yang juga bisa mengirim email, atau pembaca ERP yang menyediakan tool query generikSediakan hanya tool yang bernama jelas; filter remote MCP server dengan allowed_tools agar tool yang tidak dipakai tidak pernah diimpor
Permission berlebihSatu API key admin dipakai bersama oleh semua tool yang dipanggil agentSatu service account atau OAuth scope per tool, read-only secara default, scope write diminta hanya lewat step-up
Otonomi berlebihPembayaran di-posting atau record dihapus tanpa ada yang memeriksaApproval gate di setiap tool write, diputuskan oleh kode dan manusia, bukan oleh model

Contoh dari OWASP layak diingat karena sangat biasa: asisten yang dibuat untuk meringkas email ternyata juga punya izin mengirim email, lalu sebuah email berisi injection menyuruhnya meneruskan pesan sensitif ke penyerang. Perbaikan yang dicantumkan semuanya berupa perubahan permission, bukan perubahan prompt. Hapus kemampuan mengirim, autentikasi dengan OAuth scope read-only, atau minta user menyetujui setiap pengiriman.

Aturan pertama: tidak ada secret yang masuk ke context window

Panduan remote MCP OpenAI menyatakan masalahnya dengan lugas: server yang berbahaya bisa mengeksfiltrasi data sensitif dari apa pun yang masuk ke context model. Hal yang sama berlaku untuk halaman web beracun, PDF berbahaya, atau model yang sedang bingung. Jadi aturan pertamanya bersifat struktural. API key yang ditulis di system prompt, deskripsi tool, atau argumen tool adalah key yang bisa diulang oleh model. Model cukup menyebut aksi; executor Anda, yang berjalan di luar model, yang memutuskan credential mana yang dipakai aksi tersebut.

// Wrong: the API key is part of the prompt. Anything in context can be
// repeated, summarised or sent somewhere by an injected instruction.
const tools = [{
  type: "function",
  name: "query_erp",
  description: `Call https://erp.example.co.id/api with header X-Api-Key: ${process.env.ERP_KEY}`,
  parameters: { type: "object", properties: { path: { type: "string" } } },
}];

// Right: the model only names an action and its arguments.
// The executor - your code, outside the model - chooses the credential.
const CREDENTIAL_FOR_TOOL = {
  read_invoice:  "erp/svc-agent-invoice-reader",  // read-only account
  draft_payment: "erp/svc-agent-ap-drafter",      // may create drafts, never post
} as const;

type ToolName = keyof typeof CREDENTIAL_FOR_TOOL;

export async function executeTool(name: ToolName, args: unknown) {
  const secretRef = CREDENTIAL_FOR_TOOL[name];
  if (!secretRef) throw new Error(`no credential mapped for tool ${name}`);

  // getSecret is your own seam over Vault / a cloud secret manager.
  const token = await getSecret(secretRef);
  const res = await callErp(name, args, token);

  // The tool result goes back into context, so it is a leak path too:
  // drop echoed headers, signed URLs and anything shaped like a token.
  return redactSecrets(res);
}

Pemetaan dari nama tool ke referensi secret adalah baris paling berguna di seluruh desain ini. Credential menjadi properti tool, bukan properti percakapan, sehingga input secerdik apa pun tidak bisa membuat read_invoice berjalan dengan akun drafting. Pemetaan ini juga memberi Anda satu file untuk direview ketika ada yang bertanya apa saja yang bisa disentuh agent.

Hasil tool juga merupakan jalur kebocoran. Error ERP yang menampilkan ulang request header, response debug yang memuat nilai Authorization, atau URL download pre-signed semuanya mengalir kembali ke context, tempat instruksi injection berikutnya bisa menemukannya. Redaksi output tool sebelum dikembalikan, dan jangan pernah biarkan tool shell mencetak environment variable miliknya sendiri.

Sandbox: placeholder lewat domain_secrets, bukan key mentah

Agent yang mengeksekusi kode membuat aturan pertama lebih sulit, karena kode yang ditulis model tetap perlu autentikasi ke suatu tempat. Hosted shell OpenAI menjawabnya dengan domain_secrets. Setiap entri berisi domain tujuan, nama yang mudah dikenali, dan nilai secret. Model dan runtime hanya melihat placeholder seperti $ERP_TOKEN; sidecar auth-translation memasang nilai aslinya hanya untuk tujuan yang disetujui, dan dokumentasinya menyatakan nilai mentah tidak disimpan di server API dan tidak muncul di context yang terlihat oleh model.

import OpenAI from "openai";
const client = new OpenAI();

const response = await client.responses.create({
  model: process.env.AGENT_MODEL!,
  input: "Fetch today's open purchase orders and summarise them by vendor.",
  tools: [
    {
      type: "shell",
      environment: {
        type: "container_auto",
        // No network_policy = no outbound network at all (the default).
        network_policy: {
          type: "allowlist",
          // Must be a subset of the org allow list set in the dashboard,
          // or the request fails instead of silently widening access.
          allowed_domains: ["erp-api.example.co.id"],
          domain_secrets: [
            {
              domain: "erp-api.example.co.id",
              name: "ERP_TOKEN",
              // The model and the shell only ever see $ERP_TOKEN.
              // The sidecar swaps in the real value for this domain only.
              value: process.env.ERP_READONLY_TOKEN!,
            },
          ],
        },
      },
    },
  ],
});

Sisi jaringan sama pentingnya dengan sisi secret. Container hosted tidak punya akses jaringan keluar secara default. Untuk mengaktifkannya, admin harus lebih dulu mengatur allow list organisasi di dashboard, dan request juga harus menetapkan network_policy secara eksplisit. Daftar org menentukan seluruh domain yang bisa dijangkau, daftar di level request hanya bisa mempersempitnya, dan request yang menyebut domain di luar daftar org akan gagal alih-alih diam-diam memperluas akses.

OpenAI juga mengingatkan bahwa allowlist itu sendiri membuka jalur eksfiltrasi: izinkan hanya domain yang Anda percaya dan tidak bisa dipakai penyerang untuk menerima data. Situs paste umum atau object store yang menerima upload anonim akan mematahkan semua kontrol lain di halaman ini. Panduan yang sama menyarankan pencatatan host yang diminta setiap session dan tujuan yang benar-benar dijangkau, log yang akan Anda butuhkan setelah insiden.

Token MCP: per request, per user, terikat ke satu server

Remote MCP memindahkan credential melintasi batas kepercayaan, dan kedua sisinya punya aturan. Di sisi OpenAI, Responses API tidak menyimpan nilai yang Anda kirim di field authorization dan tidak menampilkannya di objek Response, sehingga Anda wajib mengirimnya di setiap request. Ini justru fitur: token tinggal di token store Anda, dengan scope milik user yang benar-benar sedang aktif, dan tidak ada salinan berumur panjang di sisi platform.

// Client side: the token belongs to the human user, is bound to ONE
// MCP server (RFC 8707 resource = its canonical URI), and is resent on
// every call because the Responses API does not store it.
const MCP_URL = "https://mcp.erp.example.co.id/mcp";
const userToken = await tokens.getAccessToken({ userId, resource: MCP_URL });

await client.responses.create({
  model: process.env.AGENT_MODEL!,
  input,
  tools: [{
    type: "mcp",
    server_label: "erp",
    server_url: MCP_URL,
    authorization: userToken,
    allowed_tools: ["read_invoice", "draft_payment"],
    require_approval: { never: { tool_names: ["read_invoice"] } },
  }],
});

// Server side: accept only tokens minted FOR this server.
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(new URL("https://id.example.co.id/.well-known/jwks.json"));

export async function authenticate(req: Request) {
  const bearer = req.headers.get("authorization")?.replace(/^Bearer /, "");
  if (!bearer) return challenge(401, "invoices:read");
  const { payload } = await jwtVerify(bearer, JWKS, {
    issuer: "https://id.example.co.id",
    audience: MCP_URL, // a token for any other API is rejected here
  });
  // Never forward 'bearer' to the ERP. Exchange or use this server's own
  // credential downstream - token passthrough is forbidden by the spec.
  return payload;
}

Spesifikasi authorization MCP 2026-07-28 menambahkan aturan audience. Client wajib mengirim parameter resource dari RFC 8707, berisi canonical URI server, di authorization request maupun token request. Server wajib memvalidasi bahwa token memang diterbitkan khusus untuknya, hanya menerima token yang valid untuk resource miliknya sendiri, dan tidak boleh menerima atau meneruskan token lain. Halaman security best practices menyebut pola terlarang ini token passthrough: meneruskan token client apa adanya ke API downstream merusak audit trail dan menjadikan server sebagai confused deputy.

Rilis yang sama memperkuat alur terhadap mix-up attack. Client mencatat issuer yang diharapkan sebelum redirect dan memvalidasi parameter iss dari RFC 9207 sebelum mengirim authorization code ke token endpoint mana pun, sehingga code dari server yang jujur tidak bisa ditukar di server yang jahat. PKCE saja tidak cukup, karena client akan menyerahkan code_verifier ke token endpoint milik penyerang. Client ID Metadata Documents menggantikan Dynamic Client Registration sebagai jalur registrasi yang direkomendasikan, dan DCR dipertahankan hanya demi kompatibilitas mundur.

Satu identitas per tool, dengan scope sesuai kata kerjanya

Satu agent tidak seharusnya menjadi satu principal. Beri setiap tool service account atau OAuth scope sendiri, seukuran satu kata kerja yang dijalankan tool itu. Pada contoh accounts payable, pembagian identitasnya seperti ini:

ToolIdentitasScopeSkenario terburuk jika terkena injection
read_invoiceToken delegasi milik userinvoices:readMembaca invoice yang memang sudah bisa dilihat user
draft_paymentToken user setelah step-uppayments:draftMembuat draft yang tetap harus dirilis oleh manusia
lookup_vendorService account read-onlyvendors:read hanya untuk field nama dan statusMengetahui nama vendor, bukan data rekening bank
post_paymentTidak diberikan ke agentTidak adaTidak ada; posting tetap menjadi aksi manusia di ERP

MCP menyediakan mekanisme untuk menjaga scope awal tetap kecil. Spesifikasinya meminta server mencantumkan scope yang dibutuhkan di challenge WWW-Authenticate dan meminta client memperlakukannya sebagai acuan resmi. Ketika token kekurangan scope saat runtime, server menjawab 403 dengan error insufficient_scope, lalu client melakukan otorisasi ulang dengan gabungan scope yang sudah dimiliki dan scope yang diminta challenge.

# The agent's token carries invoices:read only. It tries to draft a payment:
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
                  scope="payments:draft",
                  resource_metadata="https://mcp.erp.example.co.id/.well-known/oauth-protected-resource",
                  error_description="Drafting a payment needs payments:draft"

# The client re-authorises with the UNION (invoices:read payments:draft),
# the user sees exactly one new permission on the consent screen,
# and the elevation is logged with a correlation id.
# payments:post is never requested by the agent's client at all.

Halaman best practices mencantumkan kesalahan yang membatalkan semua ini: memublikasikan semua scope di scopes_supported, scope wildcard atau serba-bisa seperti all atau full-access, menggabungkan privilege yang tidak berhubungan demi menghindari prompt berikutnya, dan menganggap scope yang tertera di token sudah cukup tanpa logika otorisasi di sisi server. Poin terakhir paling penting untuk pekerjaan ERP, karena aturan sebenarnya biasanya di level baris: user ini hanya boleh melihat invoice untuk company code miliknya sendiri.

Approval gate di setiap tool write

Scope membatasi apa yang mungkin terjadi; approval memutuskan apa yang benar-benar terjadi. Responses API menyediakan require_approval pada tool MCP dengan nilai never, always, atau objek berisi daftar nama tool yang boleh melewati approval. Pasangkan dengan allowed_tools agar tool destruktif dari server tidak pernah diimpor sejak awal. Setiap panggilan yang tertunda lalu datang sebagai item mcp_approval_request, dan kode Anda menjawab dengan mcp_approval_response berisi id request dan nilai boolean.

// The approval decision is made by code and a human, never by the model.
const AUTO_APPROVE = new Set(["read_invoice"]);
const MAX_UNREVIEWED_IDR = 0; // every payment draft goes to a person

for (const item of response.output) {
  if (item.type !== "mcp_approval_request") continue;
  const args = JSON.parse(item.arguments);

  const needsHuman =
    !AUTO_APPROVE.has(item.name) ||
    (item.name === "draft_payment" && args.amountIdr > MAX_UNREVIEWED_IDR);

  const approve = needsHuman
    ? await approvals.ask({ user: userId, tool: item.name, args }) // UI, chat, email
    : true;

  await audit.log({ requestId: item.id, tool: item.name, args, approve });

  pending.push({
    type: "mcp_approval_response",
    approval_request_id: item.id,
    approve,
  });
}

Simpan keputusannya di luar model. Aturan seperti setiap draft pembayaran butuh persetujuan orang, atau nominal di atas batas rupiah tertentu butuh approver kedua, adalah kode biasa dengan baris audit di belakangnya. Bertanya kepada model apakah sebuah panggilan terlihat aman sama saja dengan mengembalikan keputusan ke komponen yang baru saja dikuasai penyerang.

Gunakan allowlist tool read yang boleh melewati approval sebagai default, bukan blocklist tool write yang membutuhkannya. Tool baru yang ditambahkan ke MCP server bulan depan akan langsung tertahan approval, bukan diam-diam disetujui otomatis.

Checklist review credential untuk agent apa pun

Jalankan ini sebelum agent mendapat credential production, dan ulangi setiap kali agent mendapat tool baru:

  1. Cari di system prompt, deskripsi tool, dan contoh few-shot apa pun yang berbentuk key, token, password, atau connection string. Hasilnya harus kosong.
  2. Pastikan setiap tool dipetakan ke tepat satu referensi credential di kode, dan tidak ada tool yang bisa memilih credential sendiri dari argumennya.
  3. Periksa bahwa hasil tool sudah diredaksi dari header, URL bertanda tangan, dan string berbentuk token sebelum kembali ke context.
  4. Untuk eksekusi kode, pastikan secret memakai placeholder domain_secrets dan setiap domain yang diizinkan bukan domain yang bisa dipakai penyerang sebagai tempat penampungan data.
  5. Untuk remote MCP, pastikan token bersifat per user, dikirim per request, terikat ke server lewat parameter resource, dan divalidasi audience-nya di server.
  6. Daftarkan setiap identitas beserta scope-nya, lalu tanyakan apa yang didapat penyerang jika satu token itu bocor. Hapus scope yang tidak pernah dipakai tool.
  7. Pastikan setiap tool write membutuhkan approval, aturan approval tinggal di kode, dan setiap keputusan dicatat beserta argumennya.
  8. Pastikan Anda bisa me-revoke credential satu tool tanpa menghentikan seluruh agent, dan tahu siapa yang harus dihubungi saat melakukannya.

Poin enam dan delapan paling sering dilewati tim, padahal keduanya yang paling berarti di hari terburuk. Jika satu token yang bocor membuka semuanya, atau jika me-revoke token itu membuat seluruh helpdesk mati, desainnya masih berupa satu admin key bersama dengan langkah tambahan.

Pertahanan prompt menentukan seberapa sering agent tertipu; credential menentukan apa yang bisa dilakukan agent yang tertipu. Simpan secret di tempat yang tidak bisa dibaca model, ikat setiap token ke satu user dan satu server, beri setiap tool identitas tersempit yang cukup untuk tugasnya, dan tempatkan manusia di antara agent dan setiap write yang tidak bisa dibatalkan.

FAQ

Pertanyaan Umum

Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.

Apa arti least privilege untuk AI agent?

Artinya setiap tool yang bisa dipanggil agent hanya mendapat akses yang dibutuhkan untuk satu tugasnya, tidak lebih. Entri OWASP LLM06 Excessive Agency menggambarkan kegagalannya sebagai fungsionalitas berlebih, permission berlebih, dan otonomi berlebih. Least privilege membatasi ketiganya, sehingga prompt injection yang berhasil hanya bisa melakukan apa yang diizinkan credential sempit tersebut.

Mengapa API key tidak boleh ditaruh di prompt atau deskripsi tool agent?

Apa pun yang masuk ke context model bisa diulang, diringkas, atau dieksfiltrasi oleh instruksi injection. Panduan remote MCP OpenAI mengingatkan bahwa server berbahaya bisa mengeksfiltrasi data sensitif dari apa pun yang ada di context. Biarkan model menyebut aksinya, lalu kode executor Anda di luar model yang memasang credential.

Bagaimana cara kerja domain_secrets di hosted shell OpenAI?

Anda mencantumkan domain, nama placeholder, dan nilai secret di dalam network_policy milik tool shell. Model dan container hanya melihat placeholder seperti $API_KEY, dan sidecar mengganti nilai aslinya hanya untuk request ke domain yang disetujui. Akses keluar juga memerlukan allow list organisasi di dashboard serta network_policy eksplisit di request.

Apakah OpenAI menyimpan OAuth token yang saya kirim ke remote MCP server?

Tidak. Responses API tidak menyimpan nilai field authorization dan tidak menampilkannya di objek Response, sehingga Anda harus mengirimnya di setiap request. Dengan begitu token tetap tersimpan di store milik Anda sendiri, dengan scope user yang sedang aktif.

Apa itu token passthrough di MCP dan mengapa dilarang?

Token passthrough terjadi ketika MCP server menerima token client tanpa memeriksa bahwa token itu diterbitkan untuk server tersebut, lalu meneruskannya apa adanya ke API downstream. Spesifikasi MCP 2026-07-28 mewajibkan server memvalidasi audience token dan melarang menerima atau meneruskan token lain. Passthrough merusak audit trail dan membuat token curian bisa memakai server sebagai proxy.