Jawaban langsung: GPUStack di bawah versi 2.2.0 punya celah CVE-2026-102895 dengan skor CVSS 3.1 9.8. Server memberi hak admin tanpa login kepada permintaan yang server anggap datang dari localhost. Hari ini, cek versi, update ke 2.3.0, lalu cari tanda dan ganti semua kunci.

Celah ini sudah dipakai menyerang. CISA memasukkannya ke KEV pada 9 November 2026, dengan batas patch instansi federal AS 12 November 2026. Perbaikan sudah ada sejak versi 2.2.0 pada 18 Juni 2026.

Kondisi utama: Risiko tertinggi ada pada server dengan reverse proxy di host yang sama, misalnya nginx, Caddy, atau Traefik. Proxy itu meneruskan permintaan dari 127.0.0.1, jadi pengunjung bisa mendapat hak admin. Batas: Artikel ini tidak memuat isi header palsu, payload, atau urutan serangan.

Kami membaca catatan CVE-2026-102895, advisory GHSA-gj9m-f8ph-pmhr, catatan rilis GPUStack 2.2.0, catatan rilis GPUStack 2.3.0, dan API catatan CVE pada 11 November 2026. Kami menguji 3 skrip cek, mitigasi sementara, dan update 2.1.1 ke 2.3.0 di podman dengan image resmi GPUStack dan nginx. Jaringan uji tidak terhubung ke internet. Kami tidak mengirim header palsu dan tidak menguji serangan.

Masalah: hak admin tanpa login lewat localhost

GPUStack adalah software open source untuk mengelola cluster GPU dan menjalankan model AI, misalnya LLM, embedding, dan model gambar. Tim memakainya untuk chatbot internal, RAG, dan APIAPIPintu resmi yang dipakai 2 sistem untuk saling mengirim data, tanpa orang menyalin data secara manual.Buka glosarium model sendiri. Repositori resminya punya lebih dari 6.000 bintang di GitHub.

Menurut catatan CVE-2026-102895, CVE-2026-102895 masuk kelas CWE-287, CWE-290, dan CWE-346. Ketiganya adalah kelas autentikasi yang salah dan pemeriksaan asal permintaan yang lemah. Skor 9.8 memakai vektor AV:N, AC:L, PR:N, dan UI:N, artinya serangan lewat jaringan, mudah, dan tanpa login.

PR #5346 masuk 19 Mei 2026, dan rilis 2.2.0 terbit 18 Juni 2026. advisory GHSA-gj9m-f8ph-pmhr terbit 31 Juli, tetapi catatan CVE baru terbit 4 November 2026, 96 hari kemudian. catatan rilis GPUStack 2.2.0 juga tidak menulis perbaikan ini sebagai item keamanan.

Analisa Rama Digital: jeda 96 hari dan catatan rilis tanpa label keamanan membuat pemasang mudah melewatkan update ini. Batas: kami tidak punya data jumlah server yang masih memakai versi lama.

Linimasa: 19 Mei PR perbaikan, 18 Juni rilis 2.2.0, 31 Juli advisory, 23 Oktober rilis 2.3.0, 4 November CVE, 5 November SSVC, 9 November KEV, 12 November batas.
Titik teal: perbaikan dan rilis aman. Titik merah: advisory, catatan CVE, dan serangan. Titik amber: batas waktu. Jeda advisory ke catatan CVE: 96 hari. Sumber: GitHub GPUStack, CVE.org, dan CISA.

Per 11 November 2026, celah ini masuk CISA KEV (katalog 2026.11.09). Penilaian SSVC dari CISA pada 5 November menulis Exploitation active, Automatable yes, dan Technical Impact total. Artinya serangan sudah terjadi, bisa berjalan otomatis, dan dampaknya penuh.

Cara kerja: permintaan dari localhost dianggap admin

Bagian ini memakai ringkasan resmi dan isi patch. Tujuannya membantu Anda memilih langkah, bukan menjelaskan cara menyerang.

Ringkasan dari advisory resmi

Menurut advisory, server GPUStack di bawah 2.2.0 menerima permintaan tanpa login bila alamat klien adalah 127.0.0.1. Server lalu memakai akun admin pertama. Opsi force_auth_localhost mengatur perilaku ini, dan nilai bawaannya false.

Dampak menurut advisory: kendali admin penuh. Pihak luar bisa membuat user dan API key admin, membaca rahasia worker dan kredensial cloud, lalu menjalankan perintah di host worker lewat backend inferensi kustom.

Advisory juga menulis 1 serangan nyata pada server 2.1.1. Penyerang membuat backend inferensi kustom yang menjalankan shell di worker. Javier Sixto melaporkan serangan itu.

Setelan yang membuka jalur

  • Reverse proxy di host yang sama. nginx, Caddy, atau Traefik meneruskan permintaan dari 127.0.0.1, jadi server melihat semua pengunjung sebagai localhost.
  • Mode gateway embedded, bawaan image Docker. Di mode ini server membaca alamat klien dari header X-GPUStack-Real-IP. Menurut advisory, header itu juga diterima dari klien.
  • Opsi force_auth_localhost tidak ditulis, jadi nilainya false.
  • Port API terbuka ke internet atau ke jaringan kantor yang luas.

Apa yang diubah rilis 2.2.0

Menurut PR #5346, rilis 2.2.0 menghapus jalur admin otomatis untuk localhost. Opsi --force-auth-localhost tetap diterima, tetapi tidak berpengaruh lagi. Rilis yang sama membuat perubahan konfigurasi server khusus untuk admin.

Analisa Rama Digital: di uji kami, image 2.1.1 di belakang nginx di host yang sama memberi akun admin tanpa login lewat proxy. Port GPUStack langsung menjawab 401. Jadi alamat yang pengguna buka setiap hari adalah alamat yang terbuka. Batas: uji kami tidak mengirim header palsu.

Penemu celah menurut advisory: Nikko Enggaliano, peneliti Indonesia, serta Wei Yunzhou, Rui Yang, dan Haoyu Wang. Rui Yang dan Haoyu Wang berasal dari Johns Hopkins.

Status eksploitasi per 11 November 2026

SumberStatus
CISA KEVMasuk 9 November 2026, batas patch instansi federal AS 12 November 2026
Catatan CVE-2026-102895 (CVSS 3.1 dari CNA GitHub)Skor 9.8, kelas Critical, CWE-287, CWE-290, dan CWE-346
CISA SSVC (5 November 2026)Exploitation active, Automatable yes, Technical Impact total
Advisory GPUStackSerangan nyata teramati pada server 2.1.1
VendorCelah ditutup di 2.2.0. Versi stabil terbaru 2.3.0 per 11 November 2026
4 kartu: CVE-2026-102895 skor 9.8, masuk CISA KEV 9 November, dampak kendali admin penuh atas cluster GPU, dan login localhost tidak wajib, lalu tabel fakta.
Kartu merah: celah, skor, dan status serangan. Kartu amber: dampak dan setelan bawaan. Tabel: fakta celah per 11 November 2026. Kotak teal: versi aman. Sumber: catatan CVE-2026-102895, advisory GHSA-gj9m-f8ph-pmhr, CISA KEV, dan analisa Rama Digital.

Skor 9.8, status KEV, dan serangan aktif membuat celah ini masuk kelas darurat. Server GPUStack menyimpan model, API key, token worker, dan kredensial cloud. Dengan hak admin, pihak luar juga bisa menjalankan beban kerja di GPU Anda, dan tagihannya jatuh ke Anda.

Siapa yang paling berisiko

Risiko bergantung pada versi, cara server terbuka, dan isi worker. Tabel ini membantu Anda mengurutkan server bila Anda mengelola banyak server GPU.

Kondisi serverRisikoAlasan
Versi di bawah 2.2.0 di belakang reverse proxy di host yang samaTertinggiSetiap pengunjung proxy mendapat hak admin
Versi di bawah 2.2.0, port terbuka ke internetTertinggiServer juga menerima header alamat klien. Jawaban 401 tidak membuktikan aman
Worker dengan GPU mahal dan kredensial cloudTinggiAdmin bisa menjalankan perintah di worker dan membaca kredensial
Hanya terbuka di jaringan kantor atau VPNTinggi1 perangkat yang dibobol di jaringan itu bisa memakai jalur ini
Image Docker tag latest yang ditarik sebelum 18 Juni 2026TinggiImage itu masih di bawah 2.2.0
Sudah 2.2.0 atau lebih baruRendahTetap cari tanda bila server terbuka sebelum update
6 kartu risiko: proxy di host yang sama, port terbuka ke internet, dan worker dengan GPU merah; hanya kantor atau VPN dan image latest lama amber; sudah 2.2.0 abu-abu.
Kartu merah: risiko tertinggi. Kartu amber: periksa. Kartu abu-abu: risiko rendah. Kotak bawah: status CISA KEV. Sumber: catatan CVE-2026-102895, advisory GHSA-gj9m-f8ph-pmhr, CISA KEV, dan analisa Rama Digital.

Analisa Rama Digital: nginx di server yang sama untuk HTTPS dan domain adalah bentuk paling berisiko. Bentuk itu cepat dipasang, jadi periksa server GPU dengan bentuk itu lebih dulu. Tutup alamat itu dari internet sampai update selesai.

Panel admin cluster lain punya risiko yang sama bila terbuka ke internet. Cek juga celah Rancher Manager CVE-2026-94290, celah sertifikat LXD dan Incus CVE-2026-112816, dan celah pgAdmin 4 CVE-2026-116241. Patch juga kernel Linux di server GPU yang sama, misalnya celah TCP kernel Linux CVE-2026-98432. Untuk server telepon, baca celah SIP FreeSWITCH CVE-2026-116779.

Versi rentan dan versi aman

Semua versi GPUStack di bawah 2.2.0 rentan terhadap CVE-2026-102895. Rentang ini mencakup seluruh jalur 0.x dan 2.0.0 sampai 2.1.2. Versi 2.2.0 menutup celah, dan 2.3.0 adalah versi stabil terbaru per 11 November 2026.

Versi GPUStackStatus per 11 November 2026Update ke
0.x (semua)Rentan CVE-2026-102895 (9.8)2.3.0 hari ini
2.0.0 sampai 2.1.2Rentan CVE-2026-102895 (9.8)2.3.0 hari ini
2.2.0Aman, rilis 18 Juni 20262.3.0 saat jadwal patch berikutnya
2.3.0Aman, versi stabil terbaru per 11 November 2026Pakai versi ini
Tabel versi GPUStack: 0.x sampai 2.1.2 rentan, 2.2.0 aman minimum, 2.3.0 aman dan dipakai, dan image Docker tag latest lama perlu ditarik ulang.
Merah: rentan. Teal: aman. Amber: image Docker lama. Kotak bawah: cara cek versi yang berjalan dan catatan rilis 2.2.0. Sumber: catatan rilis GPUStack 2.2.0 dan 2.3.0, advisory GHSA-gj9m-f8ph-pmhr, Docker Hub, dan analisa Rama Digital.

Data versi berasal dari advisory GHSA-gj9m-f8ph-pmhr, catatan rilis GPUStack 2.2.0, dan catatan rilis GPUStack 2.3.0. Alamat /version menjawab tanpa login, jadi Anda bisa cek versi dari laptop. Image dengan tag latest yang ditarik sebelum 18 Juni 2026 masih di bawah 2.2.0, jadi tarik ulang dengan tag v2.3.0.

Menurut Docker Hub, image v2.3.0 berukuran sekitar 1,89 GB, sedangkan v2.1.1 sekitar 3,27 GB. Siapkan ruang disk dan waktu unduh sebelum jam update.

Prasyarat sebelum mulai

  • Akses SSH dengan sudo ke server GPUStack, atau akses ke perintah docker di server itu.
  • Akun admin GPUStack untuk membuat 1 API key khusus langkah 5.
  • Laptop atau server lain di luar server GPUStack untuk langkah 2.
  • curl dan python3 di komputer yang menjalankan skrip.
  • Jadwal jam sepi. Restart container memutus inferensi yang sedang berjalan.
  • Daftar semua server dan worker GPUStack, termasuk server uji dan server lama.

Langkah 1: Cek versi GPUStack

Simpan skrip ini sebagai cek-versi-gpustack.sh di folder ~/cek. Skrip membaca versi dari perintah gpustack version, dari container docker atau podman bernama gpustack, atau dari alamat server. Skrip lalu membandingkan versi dengan 2.2.0.

#!/usr/bin/env bash
# Cek versi GPUStack terhadap CVE-2026-102895 (admin tanpa login lewat permintaan loopback, CVSS 3.1 9.8).
# Data: catatan CVE, CISA KEV, dan advisory GHSA-gj9m-f8ph-pmhr. Versi di bawah 2.2.0 rentan.
# Pakai di server:            bash cek-versi-gpustack.sh
# Pakai dengan alamat server: bash cek-versi-gpustack.sh https://gpu.example.com
# Pakai untuk versi salinan:  bash cek-versi-gpustack.sh 0.7.1 2.1.1 2.3.0
# Skrip hanya membaca. Skrip tidak mengubah apa pun dan tidak butuh login.

AMAN=2.2.0

lebih_kecil() { [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" = "$1" ]; }

cek() {
  local v="${1#v}" dasar ekor
  if ! [[ $v =~ ^([0-9]+\.[0-9]+\.[0-9]+)(.*)$ ]]; then
    printf '%-10s TIDAK DIKENAL, cocokkan manual dengan tabel versi\n' "$1"
    return
  fi
  dasar="${BASH_REMATCH[1]}"; ekor="${BASH_REMATCH[2]}"
  if lebih_kecil "$dasar" "$AMAN"; then
    printf '%-10s RENTAN CVE-2026-102895 (9.8) -> update ke 2.3.0 hari ini\n' "$v"
  elif [ "$dasar" = "$AMAN" ] && [ -n "$ekor" ] && ! [[ $ekor =~ ^(\.post|\+) ]]; then
    printf '%-10s RILIS UJI dari 2.2.0, pakai 2.2.0 stabil atau lebih baru\n' "$v"
  else
    printf '%-10s AMAN untuk CVE-2026-102895\n' "$v"
  fi
}

dari_url() {
  # /version terbuka tanpa login di GPUStack. Contoh jawaban: {"version":"2.1.1","git_commit":"..."}
  local u="${1%/}" isi
  isi=$(curl -sS -m 15 "$u/version" 2>/dev/null)
  sed -n 's/.*"version"[ ]*:[ ]*"\([^"]*\)".*/\1/p' <<<"$isi" | head -n1 | LC_ALL=C tr -cd '[:alnum:]._+-' | cut -c1-40
}

if (( $# > 0 )); then
  for a in "$@"; do
    if [[ $a =~ ^https?:// ]]; then
      v=$(dari_url "$a")
      if [ -z "$v" ]; then echo "$a: versi tidak terbaca. Cek alamat atau jalankan skrip di server."; continue; fi
      echo "Sumber: $a/version"
      cek "$v"
    else
      cek "$a"
    fi
  done
  exit 0
fi

ada=0
if command -v gpustack >/dev/null 2>&1; then
  v=$(timeout 30 gpustack version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+[0-9A-Za-z.+]*' | head -n1)
  if [ -n "$v" ]; then echo "Sumber: perintah gpustack di host ini"; cek "$v"; ada=1; fi
fi
for rt in docker podman; do
  command -v $rt >/dev/null 2>&1 || continue
  while read -r nama image; do
    [ -z "$nama" ] && continue
    v=$(timeout 30 $rt exec "$nama" gpustack version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+[0-9A-Za-z.+]*' | head -n1)
    [ -z "$v" ] && continue
    echo "Sumber: container $nama ($image)"
    cek "$v"; ada=1
  done < <($rt ps --format '{{.Names}} {{.Image}}' 2>/dev/null | grep -i gpustack)
done
if (( ada == 0 )); then
  echo "GPUStack tidak ditemukan di host ini."
  echo "Coba: bash cek-versi-gpustack.sh https://alamat-server-anda, atau tulis versi sebagai argumen."
  exit 1
fi
# Di server: baca versi dari perintah gpustack atau dari container gpustack
bash cek-versi-gpustack.sh
# Dari laptop: baca versi lewat alamat server
bash cek-versi-gpustack.sh https://gpu.example.com
# Cek versi yang Anda catat dari server lain
bash cek-versi-gpustack.sh 0.7.1 2.1.2 2.3.0
# Cara manual tanpa skrip
docker exec gpustack gpustack version
Terminal: gpustack version menulis v2.1.1, skrip cek versi menulis RENTAN. Mode manual: 0.7.1 dan 2.1.2 rentan, 2.2.0rc1 rilis uji, 2.3.0 aman.
Container uji memakai image resmi GPUStack 2.1.1. Skrip hanya membaca versi dan tidak butuh login. Sumber: uji Rama Digital di podman dengan image resmi GPUStack 2.1.1 dan 2.3.0 serta nginx 1.29, tanpa internet, 10 November 2026.

Cara manual dari browser: buka https://alamat-server-anda/version. Jawaban JSON memuat nomor versi.

Cek hasil: "RENTAN" berarti lanjut ke langkah 2 dan 3 hari ini. "RILIS UJI" berarti pindah ke rilis stabil. "AMAN" tetap berarti jalankan langkah 5, karena server bisa sudah diserang sebelum update.

Langkah 2: Cek akses tanpa login dari laptop

Skrip ini menjawab 1 pertanyaan: apakah alamat server memberi akun tanpa login. Jalankan skrip dari laptop atau server lain, bukan dari server GPUStack, agar hasilnya sama dengan yang pengunjung lihat.

Simpan skrip ini sebagai cek-paparan-gpustack.sh. Skrip mengirim 2 permintaan GET biasa tanpa login ke setiap alamat. Skrip tidak mengirim header palsu, tidak login, dan tidak mengubah data.

#!/usr/bin/env bash
# Cek apakah alamat GPUStack memberi akun tanpa login (CVE-2026-102895).
# Pakai dari komputer lain, bukan dari server itu sendiri:
#   bash cek-paparan-gpustack.sh https://gpu.example.com http://203.0.113.10:80
# Tulis setiap alamat yang orang lain bisa buka: domain di reverse proxy, IP publik, dan port langsung.
# Skrip mengirim 2 permintaan GET biasa tanpa login ke setiap alamat: /version dan /v2/users/me.
# Skrip tidak mengirim header palsu, tidak login, dan tidak mengubah apa pun.

(( $# > 0 )) || { echo "Tulis minimal 1 alamat. Contoh: bash $0 https://gpu.example.com"; exit 2; }
terbuka=0; total=0

for a in "$@"; do
  u="${a%/}"; total=$((total + 1))
  echo "Alamat: $u"
  host=$(sed -E 's#^https?://([^/:]+).*#\1#' <<<"$u")
  if [[ $host =~ ^(127\.|localhost$|\[?::1\]?$) ]]; then
    echo "   Alamat loopback. Di versi di bawah 2.2.0, jawaban admin dari sini memang perilaku bawaan."
    echo "   Uji dari komputer lain agar hasilnya sama dengan yang pengunjung lihat."
  fi
  v=$(curl -sS -m 15 "$u/version" 2>/dev/null | sed -n 's/.*"version"[ ]*:[ ]*"\([^"]*\)".*/\1/p' | head -n1 | LC_ALL=C tr -cd '[:alnum:]._+-' | cut -c1-40)
  echo "   Versi dari /version: ${v:-tidak terbaca}"
  isi=$(curl -sS -m 15 -o - -w '\n%{http_code}' "$u/v2/users/me" 2>/dev/null)
  kode=$(tail -n1 <<<"$isi"); badan=$(sed '$d' <<<"$isi")
  case "$kode" in
    401|403)
      echo "   /v2/users/me tanpa login: $kode, butuh login. Baik." ;;
    200)
      nama=$(sed -n 's/.*"username"[ ]*:[ ]*"\([^"]*\)".*/\1/p' <<<"$badan" | head -n1 | LC_ALL=C tr -cd '[:alnum:]._@-' | cut -c1-40)
      if grep -qE '"is_admin"[ ]*:[ ]*true' <<<"$badan"; then peran=admin; else peran=user; fi
      echo "   /v2/users/me tanpa login: 200, server memberi akun ${nama:-?} ($peran)."
      echo "   TERBUKA: siapa pun yang membuka alamat ini dapat hak $peran tanpa password."
      terbuka=$((terbuka + 1)) ;;
    000|"")
      echo "   Tidak terjangkau dari komputer ini." ;;
    *)
      echo "   /v2/users/me tanpa login: kode $kode. Periksa manual." ;;
  esac
done
echo
if (( terbuka > 0 )); then
  echo "Hasil: $terbuka dari $total alamat TERBUKA. Tutup alamat itu sekarang, lalu ikuti langkah 3 sampai 5."
else
  echo "Hasil: 0 dari $total alamat memberi akun tanpa login."
  echo "Tetap update ke 2.2.0 atau lebih baru. Hasil ini hanya berlaku untuk alamat yang Anda uji."
fi
# Dari laptop: tulis setiap alamat yang orang lain bisa buka
bash cek-paparan-gpustack.sh https://gpu.example.com http://203.0.113.10:80
# Di server: lihat proses yang mendengar di port terbuka
sudo ss -ltnp | grep -E 'nginx|caddy|traefik|docker-proxy|gpustack'
Terminal dari laptop: alamat lewat nginx di port 8080 memberi akun admin tanpa login, port 80 langsung menjawab 401. Hasil: 1 dari 2 alamat TERBUKA.
nginx berjalan di host yang sama dengan GPUStack 2.1.1. Skrip hanya mengirim GET biasa tanpa login dan tanpa header palsu. Sumber: uji Rama Digital di podman dengan image resmi GPUStack 2.1.1 dan 2.3.0 serta nginx 1.29, tanpa internet, 10 November 2026.

Di uji kami, GPUStack 2.1.1 berjalan di belakang nginx di host yang sama. Alamat lewat nginx di port 8080 memberi akun admin tanpa login. Port 80 langsung ke GPUStack menjawab 401. Hasilnya 1 dari 2 alamat TERBUKA.

Batas: skrip hanya menguji jalur tanpa header. Menurut advisory, server di bawah 2.2.0 juga menerima header alamat klien, jadi jawaban 401 tidak membuktikan aman. Versi dari langkah 1 tetap menjadi penentu.

Cek hasil: "TERBUKA" berarti tutup alamat itu sekarang, lalu update dalam 1 jam atau pakai langkah 4. "0 dari N alamat" tetap berarti update hari ini.

Langkah 3: Update ke 2.3.0 dengan volume data yang sama

Hanya update yang menutup celah. Volume data menyimpan database, user, dan API key, jadi pakai volume yang sama saat container baru berjalan.

# Docker. Jalankan di jam sepi, karena restart memutus inferensi yang berjalan.
# 1. Catat image dan volume dari container lama
docker inspect gpustack --format '{{.Config.Image}}'
docker inspect gpustack --format '{{range .Mounts}}{{.Name}} {{.Source}} -> {{.Destination}}{{println}}{{end}}'
# 2. Tarik image aman lebih dulu (sekitar 1,89 GB)
docker pull gpustack/gpustack:v2.3.0
# 3. Hentikan dan hapus container lama. Volume data tetap ada.
docker stop gpustack && docker rm gpustack
# 4. Jalankan lagi dengan opsi run awal Anda. Hanya tag image yang berubah.
docker run -d --name gpustack --restart unless-stopped \
  <opsi port, GPU, dan env dari perintah awal Anda> \
  -v gpustack-data:/var/lib/gpustack \
  gpustack/gpustack:v2.3.0
# 5. Cek versi lagi di server, lalu cek akses dari laptop
bash ~/cek/cek-versi-gpustack.sh
Terminal: pull image v2.3.0, container lama dihapus, container baru memakai volume yang sama, skrip menulis AMAN, lalu dari laptop 0 dari 2 alamat terbuka.
Update memakai volume data yang sama, jadi user dan API key tetap ada. Perintah podman sama dengan perintah docker. Sumber: uji Rama Digital di podman dengan image resmi GPUStack 2.1.1 dan 2.3.0 serta nginx 1.29, tanpa internet, 10 November 2026.
  1. Catat opsi run container lama: volume, port, variabel lingkungan, dan opsi GPU.
  2. Tarik image gpustack/gpustack:v2.3.0 sebelum container lama berhenti, agar waktu henti pendek.
  3. Hentikan dan hapus container lama. Volume data tetap ada.
  4. Jalankan container baru dengan opsi yang sama. Hanya tag image yang berubah.
  5. Jalankan lagi cek-versi-gpustack.sh. Hasil yang benar: "AMAN untuk CVE-2026-102895".
  6. Jalankan lagi langkah 2 dari laptop, lalu kirim 1 permintaan inferensi uji.

Di uji kami, container 2.3.0 memakai volume data dari 2.1.1. User dan API key tetap ada, skrip versi menulis AMAN, dan alamat lewat nginx menjawab 401 tanpa login.

Bila Anda memakai Docker Compose, ganti tag image di berkas compose ke v2.3.0. Lalu jalankan docker compose pull dan docker compose up -d.

Untuk instalasi pip, update paket di environment Python yang sama, lalu restart layanan GPUStack. Kami tidak menguji jalur ini.

# Instalasi pip (tidak kami uji): cek versi terpasang lebih dulu
pip show gpustack | grep -i '^version'
# Update di environment Python yang sama dengan instalasi awal
pip install --upgrade 'gpustack>=2.2.0'
# Restart layanan GPUStack dengan cara yang sama seperti saat instalasi, misalnya:
sudo systemctl restart gpustack

Rekomendasi Rama Digital: update juga setiap worker ke versi yang sama dengan server. Lalu cek bahwa setiap worker kembali aktif di halaman Workers.

Tangga 5 baris: cek versi, cek akses tanpa login, update ke 2.3.0 dengan volume yang sama, paksa login localhost berlabel sementara, lalu cari tanda dan ganti kunci.
(1) sampai (3) dan (5): urutan perbaikan. (4): lapisan sementara. Kotak amber mengingatkan bahwa hanya update yang menutup celah. Sumber: advisory GHSA-gj9m-f8ph-pmhr, uji container Rama Digital, dan analisa Rama Digital.

Cek hasil: skrip versi menulis "AMAN" di setiap server, skrip paparan menulis 0 alamat TERBUKA, dan 1 permintaan inferensi uji berhasil.

Langkah 4: Paksa login dari localhost bila update tertunda

Langkah ini hanya lapisan sementara. Pakai langkah ini bila Anda belum bisa update hari ini, misalnya karena ruang disk kurang atau Anda butuh uji di server cadangan dulu.

Advisory memberi 3 langkah sementara. Set force_auth_localhost ke true. Buang header X-GPUStack-Real-IP dari klien di proxy. Tutup API dari jaringan yang tidak dipercaya. Pakai ketiganya bersama.

# Sementara, bila update tertunda. Versi tetap rentan sampai langkah 3.
# 1. Jalankan ulang container dengan login wajib dari localhost. Opsi lain tetap sama.
docker stop gpustack && docker rm gpustack
docker run -d --name gpustack --restart unless-stopped \
  <opsi port, GPU, dan env dari perintah awal Anda> \
  -e GPUSTACK_FORCE_AUTH_LOCALHOST=true \
  -v gpustack-data:/var/lib/gpustack \
  gpustack/gpustack:<tag lama Anda>
# Instalasi pip: tambah --force-auth-localhost pada perintah gpustack start.
# 2. nginx: buang header ini di setiap blok location yang meneruskan ke GPUStack:
#    proxy_set_header X-GPUStack-Real-IP "";
sudo grep -rn 'X-GPUStack-Real-IP' /etc/nginx/
sudo nginx -t && sudo systemctl reload nginx
# 3. Dari laptop: cek akses lagi
bash cek-paparan-gpustack.sh https://gpu.example.com
Terminal: container 2.1.1 berjalan lagi dengan GPUSTACK_FORCE_AUTH_LOCALHOST=true, nginx membuang header X-GPUStack-Real-IP, lalu 0 dari 2 alamat terbuka.
Versi tetap 2.1.1, jadi langkah ini bukan perbaikan. Admin tetap masuk dengan password atau API key. Sumber: uji Rama Digital di podman dengan image resmi GPUStack 2.1.1 dan 2.3.0 serta nginx 1.29, tanpa internet, 10 November 2026.

Di uji kami, container 2.1.1 berjalan lagi dengan GPUSTACK_FORCE_AUTH_LOCALHOST=true, dan nginx membuang header itu. Alamat lewat nginx berubah dari TERBUKA ke 401. Admin tetap masuk dengan API key, dan versi tetap 2.1.1.

Untuk Caddy, tulis header_up -X-GPUStack-Real-IP di dalam blok reverse_proxy. Untuk Traefik, pakai middleware headers dengan customRequestHeaders yang memberi header itu nilai kosong. Kami tidak menguji Caddy dan Traefik.

Analisa Rama Digital: langkah ini hanya mengurangi jalan masuk. Celah tetap ada di kode sampai versi 2.2.0. Sesudah update, baris header kosong boleh tetap ada, karena di uji kami 2.3.0 tetap menjawab normal lewat proxy itu.

Cek hasil: grep menemukan baris header di conf nginx, nginx -t lolos, dan skrip paparan dari laptop menulis 0 alamat TERBUKA.

Langkah 5: Cari tanda dan ganti semua kunci

Semua versi sampai 2.1.2 rentan, dan serangan sudah teramati. Cari tanda di setiap server, termasuk server yang sudah update hari ini.

Skrip butuh API key admin. Buat 1 API key baru di UI GPUStack khusus untuk cek ini, lalu simpan ke berkas. Jangan tulis kunci sebagai argumen perintah, karena perintah ps bisa menampilkan argumen ke user lain.

# Buat 1 API key baru di UI GPUStack khusus untuk cek ini.
# Simpan ke berkas dengan izin 600: tempel kunci, tekan Enter, lalu tekan Ctrl+D.
umask 077; cat > ~/.gpustack-admin.key
ls -l ~/.gpustack-admin.key

Simpan skrip ini sebagai cek-tanda-gpustack.sh. Skrip membaca 4 hal lewat GET: admin dan user baru, API key baru, backend inferensi kustom, dan baris log untuk API key baru, user baru, dan backend dari YAML.

Skrip mengganti karakter kontrol di baris log dengan tanda tanya, agar teks dari penyerang tidak mengubah terminal Anda. Skrip tidak menjalankan perintah dari backend kustom.

#!/usr/bin/env bash
# Cari tanda pemakaian CVE-2026-102895 di GPUStack: admin baru, API key baru, backend kustom, dan baris log.
# Pakai: bash cek-tanda-gpustack.sh <alamat-server> [hari] [berkas-log]
#   Contoh: GPUSTACK_KEY_FILE=~/.gpustack-admin.key bash cek-tanda-gpustack.sh https://gpu.example.com 30 /var/log/nginx/access.log
#   Log container: docker logs gpustack > server.log 2>&1, lalu tulis server.log sebagai berkas log.
# Kunci API admin dibaca dari variabel GPUSTACK_API_KEY atau berkas GPUSTACK_KEY_FILE (bawaan ~/.gpustack-admin.key).
# Jangan tulis kunci sebagai argumen. Perintah ps bisa menampilkan argumen ke user lain.
# Skrip hanya membaca lewat GET. Skrip butuh curl dan python3.

U="${1:-}"; HARI="${2:-30}"; LOG="${3:-}"
[ -n "$U" ] || { echo "Tulis alamat server. Contoh: bash $0 https://gpu.example.com 30"; exit 2; }
U="${U%/}"; [[ $HARI =~ ^[0-9]+$ ]] || HARI=30
KUNCI="${GPUSTACK_API_KEY:-}"
if [ -z "$KUNCI" ]; then
  F="${GPUSTACK_KEY_FILE:-$HOME/.gpustack-admin.key}"
  [ -r "$F" ] || { echo "Kunci API admin tidak ditemukan. Simpan di $F (chmod 600) atau isi GPUSTACK_API_KEY."; exit 2; }
  KUNCI=$(tr -d '\r\n' < "$F")
fi
TMP=$(umask 077; mktemp -d); trap 'rm -rf "$TMP"' EXIT
printf 'Authorization: Bearer %s\n' "$KUNCI" > "$TMP/h"; unset KUNCI
ambil() { curl -sS -m 30 -H @"$TMP/h" -o "$TMP/$2" -w '%{http_code}' "$U$1" 2>/dev/null; }

k1=$(ambil '/v2/users/me' me.json)
[ "$k1" = 200 ] || { echo "Login dengan kunci gagal (kode $k1). Pakai kunci API milik admin."; exit 1; }
k2=$(ambil '/v2/users?perPage=100' users.json)
k3=$(ambil '/v2/api-keys?perPage=100' keys.json)
k4=$(ambil '/v2/inference-backends?perPage=100' backends.json)

python3 -I - "$TMP" "$HARI" "$k2" "$k3" "$k4" <<'PY'
import json, sys, datetime as dt
tmp, hari, k2, k3, k4 = sys.argv[1], int(sys.argv[2]), *sys.argv[3:6]
now = dt.datetime.now(dt.timezone.utc); batas = now - dt.timedelta(days=hari)
def bersih(s, n=40): return ''.join(c if c.isprintable() else '?' for c in str(s))[:n]
def waktu(s):
    try:
        t = dt.datetime.fromisoformat(str(s).replace('Z', '+00:00'))
        return t if t.tzinfo else t.replace(tzinfo=dt.timezone.utc)
    except ValueError: return None
def jam(s):
    t = waktu(s)
    return t.astimezone(dt.timezone.utc).strftime('%Y-%m-%d %H:%M UTC') if t else '-'
def items(nama):
    d = json.load(open(f'{tmp}/{nama}'))
    return d.get('items', d if isinstance(d, list) else [])
tanda = 0
print(f'1. User admin dan user baru dalam {hari} hari')
if k2 == '200':
    us = items('users.json')
    for u in us:
        t = waktu(u.get('created_at')); baru = t and t >= batas
        if u.get('is_admin') or baru:
            ket = 'BARU, PERIKSA' if baru else 'lama'
            print(f"   {bersih(u.get('username'),24):24} admin={str(bool(u.get('is_admin'))).lower():5} aktif={str(bool(u.get('is_active'))).lower():5} dibuat {jam(u.get('created_at'))}  {ket}")
            if baru: tanda += 1
    print(f'   Total user: {len(us)}')
else: print(f'   Daftar user tidak terbaca (kode {k2}).')
print(f'2. API key milik admin ini dalam {hari} hari')
if k3 == '200':
    ks = items('keys.json'); n = 0
    for k in ks:
        t = waktu(k.get('created_at'))
        if t and t >= batas:
            n += 1; tanda += 1
            exp = k.get('expires_at') or 'tidak pernah'
            print(f"   PERIKSA: {bersih(k.get('name'),24):24} dibuat {jam(k.get('created_at'))}  kedaluwarsa {jam(exp) if k.get('expires_at') else exp}")
    print(f'   Kunci baru: {n} dari {len(ks)}. Kunci yang Anda buat untuk skrip ini juga terhitung.')
else: print(f'   Daftar API key tidak terbaca (kode {k3}).')
print('3. Backend inferensi kustom (bukan bawaan)')
if k4 == '200':
    bs = items('backends.json'); kus = [b for b in bs if not b.get('is_built_in')]
    for b in kus:
        tanda += 1
        print(f"   PERIKSA: {bersih(b.get('backend_name'),30):30} versi {bersih(b.get('default_version') or '-',12)}")
    if kus: print('   Cocokkan dengan catatan tim. Buka detail di UI. Jangan jalankan perintahnya.')
    print(f'   Backend kustom: {len(kus)} dari {len(bs)}')
else: print(f'   Daftar backend tidak terbaca (kode {k4}).')
open(f'{tmp}/tanda', 'w').write(str(tanda))
PY
tanda=$(cat "$TMP/tanda" 2>/dev/null || echo 0)

echo "4. Baris log: from-yaml, API key baru, user baru"
if [ -z "$LOG" ]; then
  echo "   Berkas log tidak ditulis. Tulis log reverse proxy atau log server sebagai argumen ketiga."
elif [ ! -r "$LOG" ]; then
  echo "   Berkas $LOG tidak bisa dibaca."
else
  isi=$(zcat -f "$LOG" 2>/dev/null | LC_ALL=C tr '\001-\011\013-\037\177-\237' '[?*]' | grep -E 'inference-backends/from-yaml|POST /v2/api-keys|POST /v2/users[ ?"]')
  n=$(printf '%s' "$isi" | grep -c .)
  echo "   Baris cocok: $n"
  if (( n > 0 )); then printf '%s\n' "$isi" | cut -c1-140 | tail -n 8 | sed 's/^/   /'; tanda=$((tanda + 1)); fi
fi
echo
if (( tanda > 0 )); then
  echo "Hasil: $tanda tanda perlu diperiksa. Anggap server bocor sampai terbukti bersih."
  echo "Ganti semua API key, token worker, dan kredensial cloud setelah update."
else
  echo "Hasil: 0 tanda. Tetap ganti API key dan token bila server pernah terbuka sebelum update."
fi
# Alamat server, rentang 30 hari, dan log akses nginx
bash cek-tanda-gpustack.sh https://gpu.example.com 30 /var/log/nginx/access.log
# Rentang 120 hari (sejak advisory terbit 31 Juli 2026) dengan log container GPUStack
(umask 077; docker logs gpustack > server.log 2>&1)
bash cek-tanda-gpustack.sh https://gpu.example.com 120 server.log
# Sesudah cek: hapus berkas kunci dan log, lalu hapus kunci itu di UI
rm ~/.gpustack-admin.key server.log
Terminal: skrip tanda menulis 1 admin baru, 1 API key tanpa masa berlaku, 0 dari 4 backend kustom, 1 baris log pembuatan kunci, dan hasil 3 tanda.
Server uji tidak pernah diserang. 3 tanda itu milik kami: server baru, kunci untuk skrip ini, dan log pembuatan kunci itu. Jam memakai UTC. Sumber: uji Rama Digital di podman dengan image resmi GPUStack 2.1.1 dan 2.3.0 serta nginx 1.29, tanpa internet, 10 November 2026.
2 kolom tanda: di GPUStack ada admin baru, API key tanpa masa berlaku, backend kustom, dan model baru; di log dan worker ada POST asing, proses asing, dan tagihan naik.
Kolom amber: tanda di GPUStack. Kolom merah: tanda di log dan host worker. Kotak bawah: langkah bila Anda menemukan 1 tanda saja. Sumber: advisory GHSA-gj9m-f8ph-pmhr, CISA KEV, uji container Rama Digital, dan analisa Rama Digital.

Di uji kami, server tidak pernah diserang. Skrip tetap menulis 3 tanda: server baru, kunci untuk skrip ini, dan baris log pembuatan kunci itu. Cocokkan setiap tanda dengan catatan tim Anda.

Cek hasil: skrip menulis "0 tanda", atau setiap tanda cocok dengan perubahan tim Anda. Log bisa dihapus penyerang, jadi 0 tanda belum berarti aman.

Ganti kunci sesudah update, juga bila hasilnya 0 tanda, karena server terbuka sebelum update. Menurut advisory, ganti API key admin, token worker, token registrasi, dan kredensial cloud yang server simpan. Ganti juga password admin.

Bila ada 1 tanda saja, simpan log proxy, log container, dan daftar backend sebagai bukti. Hapus user, API key, dan backend yang tidak dikenal. Periksa proses dan koneksi keluar di setiap worker, karena backend kustom berjalan di sana.

Server GPU bisa memproses data pelanggan, misalnya prompt chatbot dan dokumen RAG. Bila data pribadi bocor, UU 27/2022 tentang Pelindungan Data Pribadi mewajibkan pemberitahuan tertulis paling lambat 3 kali 24 jam sesudah kebocoran. Baca juga panduan security, privacy, dan UU PDP untuk perusahaan.

Contoh: 1 pagi respons untuk 3 server GPU

Simulasi ini memakai data dummy. Nama server, waktu, dan hasil bukan data pelanggan.

Waktu (WIB)Server dummyYang dicekHasilTindakan
08.00gpu-lab-a (2.1.1, nginx di host yang sama, domain publik)Langkah 1, 2, dan 5TERBUKA, 1 API key admin asing tanpa masa berlakuRespons insiden, lalu update
08.30gpu-kantor-b (0.7.1, hanya VPN)Langkah 1 dan 2RENTAN, 0 alamat TERBUKA dari VPNLangkah 4 pagi ini, update ke 2.3.0 jam 12.00
09.00gpu-c (2.3.0)Langkah 1 dan 5AMAN, 0 tandaGanti kunci, simpan output sebagai bukti

Pelajaran dari simulasi: server yang terbuka ke internet butuh langkah 5 dulu, bukan hanya update. Update menutup celah, tetapi tidak menghapus user, API key, atau backend yang sudah dibuat penyerang.

Siapa yang memasang patch

Tanggung jawab patch berbeda per jenis layanan, jadi tabel ini membantu Anda tahu siapa yang bergerak hari ini.

Jenis layananPemegang patchYang Anda kerjakan
GPUStack di server GPU atau VPS sendiriTim AndaLangkah 1 sampai 5
GPUStack yang dipasang vendor atau integratorSesuai kontrak perawatanTanya versi hari ini, lalu minta bukti output langkah 1 dan 5
Template GPUStack di penyedia cloud GPUTim Anda, karena Anda menjalankan instance ituCek tag image, lalu update ke v2.3.0
Platform AI dari penyedia yang memakai GPUStackPenyediaTanya apakah layanan memakai GPUStack dan kapan patch terpasang

Rekomendasi Rama Digital: masukkan GPUStack ke jadwal patch bulanan, sama seperti sistem operasi. Patch celah kritis dalam 24 jam, dan celah yang masuk CISA KEV pada hari yang sama.

Checklist hari ini

  1. Buat daftar semua server dan worker GPUStack. Pemilik: tim IT. Bukti: daftar server dan versi.
  2. Cek versi dengan langkah 1. Pemilik: admin server GPU. Bukti: output skrip.
  3. Cek akses tanpa login dari laptop dengan langkah 2. Pemilik: admin server GPU. Bukti: output skrip.
  4. Update ke 2.3.0 dengan volume data yang sama. Pemilik: admin server GPU. Bukti: output skrip versi sesudah update.
  5. Bila update tertunda, pasang langkah 4. Pemilik: admin server GPU. Bukti: output skrip paparan.
  6. Cari tanda dengan langkah 5, lalu ganti semua kunci. Pemilik: admin server GPU dan tim keamanan. Bukti: output skrip dan daftar kunci baru.
  7. Tutup API GPUStack dari internet untuk alamat yang tidak butuh akses.

Tanya jawab

Apakah celah ini sudah dipakai menyerang?

Ya. Advisory GPUStack menulis serangan nyata pada server 2.1.1. CISA memasukkan celah ini ke KEV pada 9 November 2026.

Server saya tidak memakai reverse proxy. Apakah saya aman?

Tidak. Menurut advisory, server di bawah 2.2.0 juga menerima header alamat klien di mode gateway embedded, bawaan Docker. Update ke 2.3.0 tetap wajib.

Skrip paparan menulis 401. Apakah server saya aman?

Belum tentu. Skrip hanya menguji jalur tanpa header. Versi di bawah 2.2.0 tetap rentan, jadi versi dari langkah 1 menjadi penentu.

Apakah force_auth_localhost menutup celah ini?

Hanya sementara. Opsi itu memaksa login dari localhost di versi lama. Pakai bersama header kosong di proxy, lalu update ke 2.3.0 secepatnya.

Apakah update menghapus user dan API key saya?

Tidak, bila container baru memakai volume data yang sama. Di uji kami, user dan API key tetap ada sesudah update 2.1.1 ke 2.3.0.

Bagaimana saya tahu ada yang menyalahgunakan celah ini?

Jalankan langkah 5. Cek admin dan user baru, API key tanpa masa berlaku, backend inferensi kustom, dan baris log pembuatan kunci. Cek juga proses asing dan tagihan GPU di worker.

Langkah berikutnya

Batas yang tetap berlaku: force_auth_localhost bukan perbaikan, dan update tidak menghapus user, API key, atau backend yang sudah dibuat. Bila server Anda menunjukkan tanda di langkah 5, atau Anda butuh bantuan audit dan pemulihan, pakai Vibe Code Rescue. Untuk 1 masalah mendesak hari ini, jadwalkan sesi 30 menit.

Sumber