Autentikasi dan Otorisasi

Cara API server Kubernetes memverifikasi identitas permintaan dan mengotorisasi aksesnya.

Daftar isi

Setiap operasi pada kluster Kubernetes berjalan melalui satu pintu, yaitu API server, sehingga keamanan kluster bertumpu pada dua pertanyaan yang dijawab berurutan, yaitu siapa pelakunya dan apa yang boleh dilakukannya. Proses menjawab pertanyaan pertama disebut autentikasi, sedangkan proses menjawab pertanyaan kedua disebut otorisasi. Topik ini membahas mekanisme kedua tahap tersebut beserta urutan keputusan API server, mulai dari pemeriksaan identitas sampai keputusan izin, termasuk prinsip default deny yang menjadi dasar semua model otorisasi. Simulasi keputusan pada bagian akhir memperlihatkan alur ini secara konkret dan dapat dijalankan di sandbox.

Autentikasi: memverifikasi identitas permintaan#

Autentikasi adalah proses membuktikan identitas pengirim permintaan (request) sebelum permintaan itu dinilai lebih lanjut. Setiap permintaan membawa kredensial, misalnya sertifikat klien, bearer token, atau pernyataan dari penyedia identitas eksternal. API server menjalankan rantai plugin autentikasi, dan setiap plugin mencoba membaca kredensial tersebut dengan cara masing-masing. Plugin pertama yang berhasil melampirkan identitas berupa nama pengguna, uid, dan daftar grup pada permintaan, sedangkan identitas yang tidak dikenali plugin mana pun membuat API server menjawab 401 Unauthorized.

Tiga mekanisme yang paling sering ditemui pada kluster nyata adalah sebagai berikut:

  • Sertifikat klien, yaitu sertifikat x.509 yang ditandatangani CA kluster, dengan bidang nama umum (CN) menjadi nama pengguna dan bidang organisasi (O) menjadi grupnya.
  • Token ServiceAccount, yaitu JSON Web Token yang ditandatangani API server dan dibawa setiap permintaan yang keluar dari proses di dalam Pod.
  • OIDC (OpenID Connect), yaitu integrasi dengan penyedia identitas eksternal sehingga identitas pengguna aplikasi web yang sudah masuk dapat digunakan untuk API kluster.

Grup yang dilampirkan pada tahap ini menjadi input penting bagi tahap berikutnya, karena aturan RBAC umumnya ditulis terhadap grup dan bukan terhadap pengguna satu per satu. kubectl menyimpan kredensial pada berkas kubeconfig, sehingga setiap perintah kubectl sudah membawa kredensial tersebut tanpa langkah tambahan.

Otorisasi: keputusan izin yang menolak secara default#

Otorisasi adalah proses memutuskan apakah identitas yang sudah terverifikasi boleh melakukan tindakan yang diminta. API server tidak menilai permintaan sebagai teks bebas, tetapi menyusunnya menjadi atribut yang dibandingkan satu per satu, yaitu verb seperti get, list, create, atau delete, jenis resource, subresource bila ada, apiGroup, dan namespace tujuan. Set atribut inilah yang dibandingkan dengan aturan pada setiap modul otorisasi.

Beberapa modul otorisasi yang disediakan API server antara lain RBAC, yaitu model izin berbasis aturan yang dikelola sebagai objek API, Node yang khusus melayani permintaan dari kubelet, ABAC yang membaca kebijakan dari berkas statis, dan webhook yang meneruskan keputusan kepada layanan eksternal. Modul-modul ini bekerja bersama, sehingga permintaan diizinkan apabila salah satu modul mengizinkannya, dan ditolak apabila semua modul menolak atau tidak memiliki aturan yang cocok.

Prinsip dasarnya adalah default deny. Tanpa satu pun aturan yang cocok, permintaan langsung ditolak dengan jawaban 403 Forbidden, dan tidak ada mekanisme pewarisan yang diam-diam memberikan izin tambahan. Perintah berikut adalah cara tercepat menanyakan keputusan izin tanpa membaca semua aturan yang berlaku:

bash
# tidak-dijalankan
# uji izin ali untuk membuat pod di namespace aplikasi-catatan
kubectl auth can-i create pods -n aplikasi-catatan --as ali

# uji izin yang tidak dimiliki oleh citra
kubectl auth can-i delete pods -n aplikasi-catatan --as citra
text Contoh keluaran
yes
no

Perintah kubectl auth can-i menjawab dengan yes atau no sesuai keputusan modul otorisasi yang aktif, dan opsi --as menyimulasikan identitas lain, sehingga pemeriksaan aturan dapat dilakukan tanpa masuk sebagai pengguna tersebut.

Alur keputusan API server#

Ketiga tahap pengendalian permintaan berjalan dalam urutan yang tetap, dan masing-masing dapat mengakhiri permintaan dengan penolakan. Diagram berikut merangkum alurnya beserta dua titik penolakan yang menghasilkan kode status berbeda:

Klienkubectl atau pod1. Autentikasisertifikat,token, OIDC2. OtorisasiRBAC, Node,ABAC, Webhook3. Admissionmutasi danvalidasi objeketcdkeadaanidentitas tak dikenal: 401 Unauthorizedtanpa aturan cocok: 403 ForbiddenPrinsip default denytanpa aturan yang cocok, permintaan ditolak sebelum sampai ke etcd
Alur permintaan melalui autentikasi, otorisasi, dan admission

Autentikasi menjadi gerbang pertama, dan otorisasi menjadi gerbang kedua yang menilai atribut permintaan terhadap aturan RBAC. Tahap ketiga adalah admission, yaitu pemeriksaan kebijakan yang berjalan setelah izin diberikan dan sebelum objek disimpan. Admission berbeda dari otorisasi karena tidak menjawab pertanyaan siapa yang boleh, melainkan menjawab pertanyaan apakah objek yang akan disimpan memenuhi kebijakan kluster, dengan kemampuan mengubah objek pada tahap mutasi dan menolaknya pada tahap validasi. Objek yang lolos semua tahap baru disimpan di etcd, sehingga keadaan kluster selalu berupa objek yang sudah melalui pemeriksaan lengkap. Alur ini berlaku untuk permintaan dari kubectl, controller, maupun kubelet, sehingga satu penjelasan mencakup seluruh komponen kluster.

Simulasi keputusan API server#

Skrip berikut menyimulasikan urutan keputusan di atas untuk lima permintaan. Katalog identitas berperan sebagai hasil tahap autentikasi, sedangkan daftar aturan per grup berperan sebagai aturan RBAC yang aktif pada kluster. Setiap permintaan diuji dengan urutan yang sama seperti API server, yaitu mencari identitas terlebih dahulu, lalu mencocokkan verb, resource, dan namespace terhadap setiap aturan:

python simulasi-keputusan.py
"""Simulasi urutan keputusan API server: autentikasi, lalu otorisasi."""

# katalog identitas yang dikenali plugin autentikasi
IDENTITAS = {
    "ali": {"groups": ["ops-dev"]},
    "citra": {"groups": ["pembaca"]},
    "system:serviceaccount:aplikasi-catatan:pembaca-catatan": {
        "groups": ["system:serviceaccounts:aplikasi-catatan"],
    },
}

# aturan RBAC aktif per grup, berupa pasangan (verb, resource, namespace)
ATURAN_RBAC = {
    "ops-dev": [
        ("create", "pods", "aplikasi-catatan"),
        ("delete", "pods", "operasi"),
    ],
    "pembaca": [
        ("get", "pods", "*"),
        ("list", "pods", "*"),
    ],
    "system:serviceaccounts:aplikasi-catatan": [
        ("get", "configmaps", "aplikasi-catatan"),
    ],
}

# daftar permintaan yang diuji: (klien, verb, resource, namespace)
PERMINTAAN = [
    ("ali", "create", "pods", "aplikasi-catatan"),
    ("citra", "list", "pods", "operasi"),
    ("citra", "delete", "pods", "operasi"),
    ("system:serviceaccount:aplikasi-catatan:pembaca-catatan", "get", "configmaps", "aplikasi-catatan"),
    ("dodi", "list", "pods", "operasi"),
]

for nomor, (klien, verb, resource, namespace) in enumerate(PERMINTAAN, start=1):
    print(f"permintaan {nomor}: {klien} -> {verb} {resource} -n {namespace}")
    if klien not in IDENTITAS:
        print("  autentikasi : GAGAL -> 401 Unauthorized")
        continue
    identitas = IDENTITAS[klien]
    print(f"  autentikasi : BERHASIL (groups: {', '.join(identitas['groups'])})")
    keputusan = None
    for grup in identitas["groups"]:
        for aturan in ATURAN_RBAC.get(grup, []):
            cocok = (
                aturan[0] in (verb, "*")
                and aturan[1] in (resource, "*")
                and aturan[2] in (namespace, "*")
            )
            if cocok:
                keputusan = f"DIIZINKAN (aturan {aturan[0]} {aturan[1]} {aturan[2]} dari grup {grup})"
                break
        if keputusan:
            break
    if keputusan is None:
        keputusan = "DITOLAK -> 403 Forbidden (default deny)"
    print(f"  otorisasi   : {keputusan}")
    print()
text Keluaran
permintaan 1: ali -> create pods -n aplikasi-catatan
  autentikasi : BERHASIL (groups: ops-dev)
  otorisasi   : DIIZINKAN (aturan create pods aplikasi-catatan dari grup ops-dev)

permintaan 2: citra -> list pods -n operasi
  autentikasi : BERHASIL (groups: pembaca)
  otorisasi   : DIIZINKAN (aturan list pods * dari grup pembaca)

permintaan 3: citra -> delete pods -n operasi
  autentikasi : BERHASIL (groups: pembaca)
  otorisasi   : DITOLAK -> 403 Forbidden (default deny)

permintaan 4: system:serviceaccount:aplikasi-catatan:pembaca-catatan -> get configmaps -n aplikasi-catatan
  autentikasi : BERHASIL (groups: system:serviceaccounts:aplikasi-catatan)
  otorisasi   : DIIZINKAN (aturan get configmaps aplikasi-catatan dari grup system:serviceaccounts:aplikasi-catatan)

permintaan 5: dodi -> list pods -n operasi
  autentikasi : GAGAL -> 401 Unauthorized

Pada keluaran tersebut, permintaan pertama diizinkan karena grup ops-dev memang memiliki aturan create untuk pods di namespace aplikasi-catatan. Permintaan kedua diizinkan melalui wildcard namespace pada aturan grup pembaca. Permintaan ketiga memperlihatkan keputusan default deny, yaitu citra sudah terautentikasi tetapi tidak memiliki aturan delete, sehingga jawabannya 403. Permintaan keempat diizinkan melalui grup otomatis system:serviceaccounts, yaitu grup yang dilampirkan pada setiap permintaan dari ServiceAccount di namespace tersebut. Permintaan kelima gagal pada tahap autentikasi, sehingga tahap otorisasi tidak pernah dijalankan dan jawabannya 401.

Skrip ini tidak berkomunikasi dengan API server mana pun, tetapi urutan keputusannya identik, sehingga perpindahan dari simulasi ke kluster nyata hanya menambahkan dua langkah, yaitu menerbitkan identitas dan menuliskan aturan RBAC-nya sebagai objek API. Aturan pada kluster nyata juga sering ditulis terhadap grup, dan keluaran di atas memperlihatkan alasannya, yaitu satu aturan per grup mengurangi jumlah aturan yang harus ditinjau dibandingkan dengan satu aturan per pengguna.

Kesalahan umum#

Dua kode status pada alur ini sering ditafsirkan keliru, dan kebiasaan menyalin aturan dari contoh juga menimbulkan izin yang tidak disengaja. Tiga kesalahan berikut paling sering muncul saat model keamanan ini mulai diterapkan, dan ketiganya sulit ditelusuri karena gejalanya muncul jauh dari penyebabnya, yaitu aplikasi gagal berjalan tanpa pesan yang menunjuk aturan mana yang kurang:

  • Menafsirkan 403 Forbidden sebagai masalah identitas. Identitas sudah terverifikasi pada tahap autentikasi, sehingga akar masalahnya adalah aturan yang tidak ada. Perbaikannya adalah memeriksa keputusan izin dengan kubectl auth can-i, lalu melengkapi Role atau RoleBinding-nya.
  • Menyalin aturan dengan verb ["*"] dan resources ["*"] dari contoh di internet. Aturan semacam itu memberikan seluruh tindakan pada jenis objek yang disebut, dan salinannya sering lolos tanpa ditinjau. Perbaikannya adalah menulis verb dan resource minimum yang benar-benar dibutuhkan aplikasi.
  • Menyimpan berkas kubeconfig yang berisi sertifikat atau token pada lokasi yang mudah diakses pihak lain. Kredensial yang bocor berarti identitas yang bocor sampai sertifikatnya dicabut. Perbaikannya adalah membatasi izin berkasnya dan mengganti kredensial lama saat berganti perangkat.

Ringkasan#

  • Autentikasi membuktikan identitas pengirim melalui rantai plugin, melampirkan nama pengguna dan grup pada permintaan, dan menjawab 401 apabila identitas tidak dikenali.
  • Otorisasi menilai verb, resource, apiGroup, dan namespace permintaan terhadap modul seperti RBAC dan Node, serta mengizinkan permintaan bila salah satu modul mengizinkannya.
  • Tanpa aturan yang cocok, permintaan ditolak sesuai prinsip default deny dengan jawaban 403 Forbidden.
  • Admission berjalan setelah izin diberikan untuk mutasi dan validasi kebijakan, sedangkan objek yang lolos seluruh tahap disimpan di etcd.

Referensi#