Documentation
¶
Overview ¶
Package upstream implements the outbound (client-side) half of the bastion: dialing targets and opening channels on their behalf.
Index ¶
- Variables
- func ScanHostKey(ctx context.Context, host string, port int) (ssh.PublicKey, error)
- type CommandError
- type Conn
- func (c *Conn) Capabilities(ctx context.Context) (ManageCapabilities, error)
- func (c *Conn) Client() *ssh.Client
- func (c *Conn) Close() error
- func (c *Conn) Exec(ctx context.Context, command, stdin string) (string, error)
- func (c *Conn) Facts() model.TargetFacts
- func (c *Conn) OpenSession() (ssh.Channel, <-chan *ssh.Request, error)
- func (c *Conn) Probe(ctx context.Context) (model.TargetProbe, error)
- type Identity
- type ManageCapabilities
Constants ¶
This section is empty.
Variables ¶
var ( // ErrUnreachable: TCP bağlantısı hiç kurulamadı. ErrUnreachable = errors.New("upstream: target unreachable") // ErrHostKeyMismatch: hedef, sabitlenmişten BAŞKA bir anahtar sundu. // Bu bir yapılandırma sorunu olabilir de olmayabilir de — postern // bu ikisini ayırt edemez ve etmemeli. ErrHostKeyMismatch = errors.New("upstream: host key mismatch") // ErrRefused: taşıma kuruldu, hedef sertifikamızı kabul etmedi. // Neredeyse her zaman tek bir şey demek: hedef bu bastion'ın CA'sına // güvenmiyor (TrustedUserCAKeys) ya da kullanıcı hedefte yok. ErrRefused = errors.New("upstream: target refused our certificate") /* * ErrHandshake: TCP kuruldu ama el sıkışma KİMLİK DOĞRULAMAYA * GELMEDEN bitti. * * ⚠️ REDDEDİLMİŞ DEĞİLİZ — hedef sertifikamızı görmedi bile. * Algoritma uyuşmazlığı, susan bir hedef, SSH konuşmayan bir port: * üçü de burada. Operatörün yapması gereken şey ErrRefused'dakiyle * hiç ilgisi olmayan bir şey, o yüzden ayrı bir sınıf. */ ErrHandshake = errors.New("upstream: ssh handshake did not complete") )
* Bağlantının NEDEN kurulamadığı. * * ⚠️ NEDEN AYRIŞTIRILIYOR: üçü de "bağlanamadım" ama operatörün yapması * gereken şey üçünde de farklı — biri ağ/firewall, biri hedefin * kimliğinin değişmesi (ya da araya girme), biri hedefin bu bastion'ın * CA'sına güvenmemesi. Hepsini tek bir "session unavailable" altında * toplamak, kullanıcıya hiçbir şey söylemeyen bir ekran bırakıyordu: * ÖLÇÜLDÜ — panelde kabuk düğmesine basınca tek görünen şey * "[disconnected]" idi, sebep yalnızca sunucu günlüğünde duruyordu.
var CapabilityCommands = []string{
"sudo -n -l",
"for n in useradd adduser groupadd addgroup usermod userdel groupdel visudo bash; do command -v $n; done",
"sudo -n sshd -T 2>/dev/null | awk 'tolower($1)==\"authorizedprincipalsfile\"{print $2}'",
}
* CapabilityCommands, yönetilebilirliği ölçen SABİT komutlar. * * Probe'daki kuralın aynısı: derleme zamanında sabit, yapılandırılamaz, * salt okuma ve kabuk yorumuna açık hiçbir şey içermiyor. Operatörün * komut yazabildiği bir alan, config'e erişen herkese bütün filoda * uzaktan komut çalıştırma yetkisi verirdi. * * ⚠️ SON SÖZÜ ARACIN KENDİSİ SÖYLÜYOR. "sudoers dosyasını yazdık" * demek, sudo'nun onu OKUDUĞU anlamına gelmiyor: sudoers.d ancak ana * dosyada bir includedir satırı varsa okunuyor ve sıkılaştırılmış * kurulumlarda o satır kaldırılmış olabiliyor. `sudo -n -l` postern'in * KENDİ hakkını listeliyor; çıktı doluysa hem sudo çalışıyor hem de * kuralımızın okunduğu kanıtlanmış oluyor.
var ErrNoAnswer = errors.New("upstream: the target did not report whether the command ran")
* ErrNoAnswer: hedef, komutun çalışıp çalışmadığını BİLDİRMEDİ. * * Üç ayrı durum aynı sınıfa düşüyor ve düşmeli: kanal hiç açılamadı, * hedef exec isteğini reddetti, ya da kanal çıkış kodu gelmeden kapandı. * Üçünde de elimizdeki tek doğru cümle "bilmiyoruz". * * ⚠️ İLK ADI "ErrLost" ("bağlantı koptu") İDİ VE YANLIŞTI: exec isteğini * reddeden hedef bağlantıyı koparmıyor, açıkça cevap veriyor. Operatöre * "makine gitti" demek onu ağa baktırırdı. Sebebi ayırt etmek için * x/crypto'nun hata metnini ayrıştırmıyoruz (bkz. hostkey.go: bir * bağımlılığın dizgisi değiştiği gün sessizce yanlış sınıfa düşülür); * kütüphanenin mesajı %w zincirinde operatöre ulaşıyor. * * ⚠️ ErrUnreachable DEĞİL: o, TCP'nin hiç kurulmadığını söylüyor.
var ProbeCommands = []string{
"uname -srm",
"cat /etc/os-release",
}
* ProbeCommands, tanımada çalıştırılan SABİT komutlar. * * ⚠️ DEĞİŞKEN DEĞİL, SABİT — ve bu listenin yapılandırılabilir olmaması * özelliğin en önemli parçası. Operatörün komut yazabildiği bir alan, * config dosyasına erişebilen herkese denetim altındaki HER MAKİNEDE * uzaktan komut çalıştırma yetkisi verirdi; bastion'ı tam da engellemek * için var olduğu şeye çevirirdi. Bu yüzden liste derleme zamanında * sabit ve gözden geçirilebilir. * * İkisi de SALT OKUMA ve yan etkisiz. Kabuk yorumuna açık hiçbir şey * içermiyorlar (joker, boru, yönlendirme, değişken yok) — SSH exec * isteği dizeyi hedefin kabuğuna verdiği için bu önemli.
Functions ¶
func ScanHostKey ¶
* ScanHostKey, hedefin SUNDUĞU host key'i getirir. * * ⚠️ BU BİR DOĞRULAMA DEĞİL, BİR SORU. Dönen anahtar "doğru" anahtar * değil; o adreste o anda cevap veren makinenin sunduğu anahtar. Ağ * yolunda duran biri kendi anahtarını sunabilir ve buradan onu alırız. * * Yine de yapıştırmalı akıştan daha kötü DEĞİL: operatör de * `ssh-keyscan`i kendi makinesinden, çoğu zaman aynı ağ üzerinden * çalıştırıp yapıştırıyordu. Kazanılan şey yazım hatasının ve "şimdilik * boş bırakayım" cazibesinin ortadan kalkması; kaybedilen bir şey yok. * Güvenlik, çağıranın parmak izini operatöre AÇIKÇA gösterip onay * istemesinden geliyor — bkz. httpapi.adminScanTarget.
Types ¶
type CommandError ¶ added in v1.2.0
* CommandError: hedef komutu ÇALIŞTIRDI ve sıfırdan farklı bir kodla * bitirdi. * * ⚠️ BU BİR ARIZA DEĞİL, BİR CEVAP. `command -v` bulamadığı ad için, * `sudo -n -l` yetkisiz hesap için sıfırdan farklı dönüyor; çağıran * hangisinin cevap hangisinin arıza olduğuna errors.As ile karar * veriyor. stderr burada taşınıyor çünkü operatörün okuyacağı sebep * ("useradd: group 'x' does not exist") yalnızca orada yazıyor.
func (*CommandError) Error ¶ added in v1.2.0
func (e *CommandError) Error() string
type Conn ¶
type Conn struct {
// contains filtered or unexported fields
}
Conn is an established SSH connection to a target.
func DialManagement ¶ added in v1.2.0
func DialManagement(ctx context.Context, t model.Target, authority *ca.CA, actor, reason string) (*Conn, error)
* DialManagement, hedefe yönetim hesabıyla bağlanır. * * ⚠️ actor VE reason ZORUNLU ve sertifikanın KeyID'sine giriyor. KeyID * hedefin KENDİ sshd günlüğüne düşüyor ("Accepted publickey for postern * ... ID postern-manage: admin: check"). Makinenin sahibi postern'in * denetim kaydını göremiyor olabilir; o satırda düğmeye kimin bastığı * yazmıyorsa, filo çapında bir root girişi hiçbir kişiye * bağlanamazdı.
func DialWithCert ¶
func DialWithCert(ctx context.Context, t model.Target, identity Identity, authority *ca.CA) (*Conn, error)
DialWithCert connects to t using a freshly minted, short-lived certificate instead of a static key.
func (*Conn) Capabilities ¶ added in v1.2.0
func (c *Conn) Capabilities(ctx context.Context) (ManageCapabilities, error)
* Capabilities, hedefte sabit komutları çalıştırıp yeteneği ölçer. * * ⚠️ HATA "YÖNETİLEBİLİR DEĞİL" DEMEK DEĞİL. Bağlantı koptuğunda * bilmediğimiz şeyi bilmiyoruz; onu "yönetilemez" diye kaydetmek, * ölçmediğimiz bir sonucu ölçülmüş gibi göstermek olurdu. * * ⚠️ BU KURAL YAZILIYDI AMA UYGULANMIYORDU. İlk hâli her hatayı yutup * boş çıktıyı ayrıştırıyordu: kopan bir bağlantı "sudo yok, useradd yok, * visudo yok" diye, hatasız dönüyordu. Sıfırdan farklı çıkış bir cevap * (CommandError), cevapsızlık ise hata.
func (*Conn) Exec ¶ added in v1.2.0
* Exec, komutu hedefte çalıştırır ve STDOUT'u döner. * * ⚠️ STDOUT VE STDERR AYRI TUTULUYOR — BİRLEŞTİRMEK VERİYİ BOZUYOR. * Dönen dize yalnızca bir kayıt değil, VERİ olarak da okunuyor: plan, * hedefteki sudoers dosyasını `sudo -n cat` ile okuyup yazacağıyla * bayt bayt karşılaştırıyor. Bu makinelerde çok sık görülen * "sudo: unable to resolve host" uyarısı stdout'a karışsaydı, dosyalar * hiç eşleşmez ve plan her koşuda aynı dosyayı yeniden yazardı. Hata * sebebi kaybolmuyor: CommandError'da taşınıyor. * * Komut bir KABUĞA gidiyor (SSH exec): planın adımları `>/dev/null` ve * `&&` içeriyor ve bunlar argv'ye bölünseydi, tee adımı ">/dev/null" * adında bir dosya yaratıp sudoers dosyasını hiç yazmazdı.
func (*Conn) Facts ¶
func (c *Conn) Facts() model.TargetFacts
Facts, EL SIKIŞMADAN öğrenilenleri döner.
⚠️ KAPSAM KASITLI OLARAK DAR. Buradan çıkan her şey sunucunun bağlantı kurarken kendiliğinden söylediği: afişi, anlaştığımız host key türü, bir de ne kadar sürdüğü. Hedefte `uname` ya da `/etc/os-release` okumak çok daha fazlasını verirdi — ve bunu yapan iki yol da (target_probe, yönetim bağlantısı) varsayılan olarak kapalı ve buraya YAZMIYOR: bu yapı yalnızca el sıkışmada öğrenileni tutuyor. Bir bastion'ın kendiliğinden envanter aracına dönüşmesi, denetim altındaki her makinede sessizce komut çalıştırması demek.
func (*Conn) OpenSession ¶
OpenSession opens a "session" channel on the target and returns it along with the target's request stream.
func (*Conn) Probe ¶
* Probe, hedefte sabit okuma komutlarını çalıştırıp makineyi tanır. * * ⚠️ BU FONKSİYON YALNIZCA target_probe.enabled İLE ÇAĞRILIR. Kapalıyken * postern hedefte kullanıcının oturumu dışında hiçbir şey çalıştırmaz. * * ⚠️ KOMUTLAR KULLANICININ BAĞLANTISINDA ÇALIŞIR. Bu yorum önceden * "postern'in kullanıcılardan bağımsız bir erişimi yok" diyordu; yönetim * hesabıyla (manage.go) artık var — ama yalnızca rolü * postern_manage_host ile koşmuş hedeflerde, ve bilinçli olarak burada * KULLANILMIYOR. Tanımayı ona taşımak iki şeyi bozardı: yönetim hesabı * olmayan hedeflerde tanıma tamamen kapanırdı, ve `uname` okumak için * parolasız root tutan bir kimlik kullanmak gereğinden geniş bir yetkiyle * salt okuma yapmak olurdu. Bedeli şu: komutlar hedefin günlüklerinde * bağlanan kullanıcının adına görünür. Kabul edilebilir olmasının tek * sebebi özelliğin varsayılan kapalı ve her koşusunun denetime yazılıyor * olması.
type ManageCapabilities ¶ added in v1.2.0
type ManageCapabilities struct {
// Sudo, postern'in parolasız sudo hakkının GÖRÜLDÜĞÜ.
Sudo bool
// AddUser / AddGroup, bulunan araçların tam yolu ("" ise yok).
AddUser string
AddGroup string
ModUser string
DelUser string
DelGroup string
Visudo string
// Family, os-release'den çıkan aile ("debian", "rhel", "alpine"…).
Family string
/*
* Shell, açılan hesaba yazılacak kabuk: bash bulunduysa onun yolu,
* yoksa /bin/sh. ⚠️ ÖLÇÜLDÜ: /bin/bash sabit yazılıydı ve Alpine'de
* hesap açılıyor, sertifika kabul ediliyor ve sshd "shell /bin/bash
* does not exist" diye kapıyı kapatıyordu.
*/
Shell string
// Missing, eksik olanların adı — panelin yazacağı cümle bu.
Missing []string
/*
* PrincipalsFile, sshd'nin etkin AuthorizedPrincipalsFile deseni
* ("/etc/ssh/auth_principals/%u"). Boşsa ya sshd "none" diyor (giriş
* adı sertifikanın principal'ında aranır, dosya gerekmez) ya da
* sorulamadı. Geçici hesabın sertifikayla açılabilmesi için plan bu
* desene göre hesabın dosyasını yazıyor.
*/
PrincipalsFile string
}
* ManageCapabilities, hedefte neyin bulunduğu. * * ⚠️ ARAÇ ADLARI SAKLANIYOR, "var/yok" DEĞİL. useradd ile busybox * adduser'ın bayrakları farklı; hangisinin bulunduğunu bilmeden doğru * komutu kuramayız ve "adduser vardır" varsayımı Alpine'de sessizce * yanlış komut üretirdi.
func ParseCapabilities ¶ added in v1.2.0
func ParseCapabilities(sudoOut, whichOut, osRelease, sshdPrincipals string) ManageCapabilities
* ParseCapabilities, sabit komutların çıktısını okur. * * Ayrı bir fonksiyon çünkü arıza burada yaşıyor: bağlantı kurmadan, * gerçek dağıtım çıktılarına karşı ölçülebiliyor.
func (ManageCapabilities) Manageable ¶ added in v1.2.0
func (c ManageCapabilities) Manageable() bool
* Manageable, postern'in bu hedefi yönetip yönetemeyeceği. * * ⚠️ POZİTİF KANIT İSTİYOR. "Eksik bir şey görmedim" yetmiyor; her * parçanın BULUNDUĞU görülmüş olmalı. Yoklama yarıda kesildiyse * (bağlantı koptu, komut boş döndü) sonuç "yönetilebilir" olmamalı.
func (ManageCapabilities) Summary ¶ added in v1.2.0
func (c ManageCapabilities) Summary() string
Summary, panelde ve denetim satırında görünecek tek cümle.