Sebagian pekerjaan saya di server produksi sebuah klien dijalankan lewat shell Bash yang tidak punya tty. Bukan terminal yang saya ketik sendiri, melainkan shell yang dipanggil skrip: perintah masuk, keluaran keluar, dan tidak ada siapa pun yang bisa menyela di tengah jalan. Di lingkungan seperti itu ssh biasa berhenti di prompt password lalu diam, karena prompt itu sedang menunggu ketikan yang tidak akan pernah datang.
Jalan pintas yang biasa dipakai orang untuk kasus ini semuanya absen di Git Bash yang saya pakai. sshpass tidak ada, plink tidak ada, expect tidak ada, dan setsid juga tidak ada. Yang tersisa cuma mekanisme bawaan OpenSSH sendiri, yaitu SSH_ASKPASS, sebuah skrip yang dipanggil ssh untuk mengambil password ketika ia merasa tidak bisa bertanya langsung.
Saya pasang SSH_ASKPASS, dan ssh tetap menggantung.
Tiga hal berbeda menahan satu perintah
Yang membuat ini lambat dibaca adalah kegagalannya terlihat tunggal. Satu prompt yang tidak bergerak, satu proses yang tidak pernah keluar. Padahal yang menahannya ada tiga, dan ketiganya harus dilepas dalam perintah yang sama.
Yang pertama, DISPLAY yang masih ter-set membuat ssh mencoba askpass versi GUI, dan di situlah ia menggantung. Askpass-nya benar-benar dipanggil, hanya saja ssh memanggilnya sebagai program yang diasumsikan menggambar dialog di layar, di sebuah shell yang tidak punya layar.
Yang kedua, tanpa SSH_ASKPASS_REQUIRE=force, OpenSSH mengabaikan askpass ketika ia mengira ada tty. Jadi ada dua kemungkinan gagal yang berlawanan arah dan sama-sama berakhir diam: dipanggil sebagai GUI lalu menggantung, atau tidak dipanggil sama sekali karena ssh merasa masih bisa bertanya sendiri.
Yang ketiga tidak ada hubungannya dengan askpass. Ada key on-disk yang sudah ditolak server, dan key itu harus dilewati secara eksplisit supaya ssh benar-benar sampai ke jalur password. Jalur passwordnya sendiri memang tersedia. Server itu menawarkan dua metode, publickey dan password, dan autentikasi password aktif. Passwordnya diterbitkan oleh admin di sisi klien, dan sejak awal saya putuskan password itu tidak disimpan di catatan mana pun.
Bentuk perintah yang akhirnya jalan
Perbaikan yang saya pakai adalah gabungan empat hal sekaligus: buang DISPLAY, paksa askpass, paksa autentikasi password, dan matikan pubkey. Blok di bawah ini adalah versi yang akhirnya masuk runbook, yang menambahkan satu opsi yang belum ada di catatan pertama saya, NumberOfPasswordPrompts=1.
printf '#!/bin/sh\nprintf "%s\n" "<PASSWORD>"\n' > /tmp/ap.sh; chmod +x /tmp/ap.sh
env -u DISPLAY SSH_ASKPASS=/tmp/ap.sh SSH_ASKPASS_REQUIRE=force \
ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no \
-o NumberOfPasswordPrompts=1 deploy@host.example 'cmd'
rm -f /tmp/ap.sh # hapus segera, isinya password polosenv -u DISPLAY melepas yang pertama, SSH_ASKPASS_REQUIRE=force melepas yang kedua, dan pasangan PreferredAuthentications=password dengan PubkeyAuthentication=no melepas yang ketiga.
Baris rm -f di akhir bukan soal kerapian. Skrip askpass itu memuat password dalam bentuk teks polos di disk, jadi ia harus hilang begitu perintahnya selesai, bukan nanti waktu ingat.
Catatan lama saya justru menyetel DISPLAY
Ada satu hal yang tidak rapi di catatan saya sendiri, dan saya lebih suka menuliskannya apa adanya daripada memolesnya.
Catatan sesi bertanggal 3 Juli 2026 mencatat metode SSH yang menurut catatan itu bekerja headless dan dipakai sepanjang sesi, dengan OpenSSH 10.2. Bentuknya bukan membuang DISPLAY, tapi justru memasangnya, dan mengarahkan stdin ke /dev/null.
SSH_ASKPASS=$PWD/askpass.sh SSH_ASKPASS_REQUIRE=force DISPLAY=:0 ssh ... < /dev/nullLima hari kemudian, pada 8 Juli 2026, runbook yang saya konfirmasi menyimpan bentuk yang kebalikannya di bagian DISPLAY, dan alasan yang ditulis di sana adalah DISPLAY yang ter-set membuat ssh mencoba askpass GUI lalu menggantung.
Saya tidak punya percobaan terkontrol yang memisahkan satu variabel itu dari sisanya, jadi saya tidak akan mengklaim tahu mana yang menentukan. Yang bisa saya lihat, satu-satunya hal yang sama persis di kedua catatan adalah SSH_ASKPASS_REQUIRE=force. Yang saya pakai sekarang adalah versi yang belakangan, karena itu yang tercatat sebagai terkonfirmasi.
Gerbang lain yang gejalanya terlihat seperti server mati
Akses ke kotak itu punya dua gerbang yang terpisah, dan gerbang pertamanya berdiri jauh sebelum urusan password. Port 22 ada di balik allowlist IP di security group AWS, sementara IP mesin saya residensial dan berputar tiap kali koneksinya tersambung ulang.
Akibatnya ada satu mode gagal yang sangat mudah salah baca: port 22 timeout sementara situsnya melayani pengunjung dengan normal. Itu bukan server bermasalah, itu IP saya yang berputar keluar dari allowlist. Kekeliruan membaca gejala ini pernah menghabiskan satu jam.
Cara membedakannya murah, yaitu memukul dua port langsung dari Bash dan membandingkan hasilnya.
timeout 8 bash -c 'echo > /dev/tcp/host.example/443' # terbuka
timeout 8 bash -c 'echo > /dev/tcp/host.example/22' # timeoutKalau 443 terbuka dan 22 mati, mesinnya hidup dan yang menutup adalah firewall. Kalau port 22 bisa dijangkau, artinya IP saya sedang diizinkan dan masalahnya ada di tempat lain. Pada 6 Juli 2026 saya menyelesaikannya dengan meminta admin klien memasukkan satu alamat IP ke allowlist, dan sudah jelas sejak saat itu bahwa ini akan berulang setiap kali IP-nya berputar.
Memperbarui allowlist sendiri, dan jebakannya
Sejak 6 Agustus 2026 admin klien menerbitkan kunci AWS untuk saya, jadi allowlist bisa saya perbarui sendiri tanpa bolak-balik Slack. Skripnya dijalankan dari Git Bash, bukan PowerShell.
~/bin/allow-my-ip.sh "$(curl -s https://checkip.amazonaws.com)" <user> <profil-aws>Yang dikirim harus IP publik. Skrip contoh dari sisi klien memakai alamat 192.168.x.x, yang merupakan alamat LAN, dan mengirim IP privat menghasilkan aturan sia-sia sementara SSH tetap gagal. Kombinasi itu terbaca persis seperti server rusak padahal yang salah adalah angka yang kita kirim sendiri.
Skrip aslinya juga punya dua bug yang harus saya perbaiki dulu. Yang pertama adalah pasangan mktemp dengan file://, yang tidak mungkin jalan di Git Bash karena aws.exe adalah biner Windows native dan tidak bisa me-resolve path /tmp/..., sehingga gagal dengan pesan Unable to load paramfile. Ini sebetulnya penyakit yang sama dengan DISPLAY di atas, yaitu satu program yang berdiri di dunia lain dari shell yang memanggilnya. Yang kedua adalah pemeriksaan duplikat yang tidak pernah aktif di platform mana pun, karena JMESPath-nya menyaring daftar yang sudah diproyeksikan, jadi alurnya selalu jatuh ke penambahan dan keluar dengan kode 1 pada InvalidPermission.Duplicate.
Satu hal lagi yang perlu diketahui sebelum panik melihat isi security group: kuncinya add-only. Dia punya AuthorizeSecurityGroupIngress tapi tidak punya Revoke, jadi IP lama menumpuk permanen dan yang membersihkan adalah admin klien secara manual tiap satu sampai dua minggu, atas pilihannya sendiri. IP lama yang masih terdaftar itu wajar, bukan bug.
Sesudah key auth pulih
Jalur password di atas sebetulnya cuma perlu jalan satu kali, karena yang paling berguna untuk dikerjakan di dalamnya adalah memasang key. Caranya, di dalam sesi password yang sama, salurkan pubkey ke cat >> ~/.ssh/authorized_keys.
cat ~/.ssh/id_ed25519.pub | env -u DISPLAY SSH_ASKPASS=/tmp/ap.sh SSH_ASKPASS_REQUIRE=force \
ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no deploy@host.example \
'cat >> ~/.ssh/authorized_keys'Pubkey ed25519 saya masuk ke authorized_keys di server itu pada 8 Juli 2026, dan sejak itu key auth bekerja dari lingkungan Bash yang sama dengan -i. Perintahnya jadi jauh lebih tenang, dan untuk skrip yang panjang saya kirim lewat stdin memakai heredoc.
ssh -o BatchMode=yes -i ~/.ssh/id_ed25519 deploy@host.example 'php -d display_errors=1 /dev/stdin' <<'PHP'
<?php require "/var/www/html/staging.example.com/wp-load.php"; /* ... */
PHPBentuk ini muncul karena kotak itu punya PHP 8.3 CLI yang jalan tapi tidak punya WP-CLI sama sekali, jadi WordPress harus di-boot sendiri lewat require ke wp-load.php, dan pekerjaan kecil dijalankan dengan php -r. Salah satu pemakaian nyatanya adalah membersihkan 125 transient throttle login yang menahan orang keluar dari halaman login.
Satu aturan yang saya pegang keras di jalur ini: verifikasi ulang docroot dengan meng-grep WP_HOME di wp-config.php sebelum wp-load apa pun, dan jangan pernah menebak dari nama folder.
grep -m1 WP_HOME /var/www/html/staging.example.com/wp-config.phpAlasannya bukan kerapian. Di kotak yang sama ada beberapa situs klien lain, dan setiap wp-load menghidupkan satu situs saja, yaitu situs yang docroot-nya kita tunjuk. Salah menunjuk docroot berarti memberi efek samping ke klien yang salah, dari perintah yang kelihatannya cuma membaca.
Yang saya bawa pulang
Satu prompt yang menggantung tidak berarti satu penyebab. Di kasus ini ada tiga penyebab yang bertumpuk.
SSH_ASKPASS bukan tombol yang tinggal dinyalakan. OpenSSH masih berhak mengabaikannya kalau ia mengira ada tty, dan SSH_ASKPASS_REQUIRE=force adalah bagian yang membuat ia berhenti berhak. Ini satu-satunya potongan yang muncul di semua catatan saya soal server ini.
Kalau port 22 timeout sementara situsnya melayani normal, curigai firewall dan alamat IP kamu sendiri sebelum curiga servernya. Dua probe tcp ke 443 dan 22 memisahkan keduanya dalam hitungan detik.
Dan kalau sebuah jalur akses hanya perlu berhasil satu kali, pakai kesempatan itu untuk memasang key, bukan untuk mengerjakan pekerjaan hari itu. Skrip askpass yang memuat password polos di disk adalah sesuatu yang saya inginkan hidup sesingkat mungkin, dan cara terbaik memendekkan umurnya adalah membuat ia tidak perlu dipanggil lagi besok.