Documentation
¶
Overview ¶
* Package provision, hedeflerde grup/kullanıcı/sudo durumunu kurar. * * ⚠️ PLAN İLE UYGULAMA AYRI — VE AYRILMALARI BU PAKETİN TEMEL KARARI. * Plan saf: gözlenen durum ile istenen durumu alıp adımları üretiyor, * hiçbir şeye bağlanmıyor ve tek başına ölçülebiliyor. Hatalar burada * yaşıyor: "zaten var mı", "hangi araç", "ne sırayla". Uygulama ince * bir katman ve gerçek makinede doğrulanıyor. * * ⚠️ BU PAKET HİÇBİR ŞEY SİLMİYOR. Silme ayrı ve yıkıcı bir yol; * yayılımın içine karıştırılsaydı, bir yapılandırma hatası "eksik" * diye okunup üretimde hesap kapatırdı.
Index ¶
- Constants
- Variables
- func PrincipalsPath(pattern, user string) (string, error)
- func PrincipalsPatternFor(ctx context.Context, r Runner, user, fallback string) string
- func SudoPath(group string) string
- func UserSudoPath(user string) string
- type AccountFacts
- type Desired
- type Group
- type GroupFacts
- type GroupInfo
- type Observed
- type Outcome
- type Report
- type Result
- type Revoke
- type RevokeMode
- type Runner
- type SSHRunner
- type Step
- func GroupDeleteSteps(caps upstream.ManageCapabilities, groups []string) ([]Step, error)
- func Plan(caps upstream.ManageCapabilities, d Desired, o Observed) ([]Step, error)
- func PrincipalRemoveStep(path, user string) (Step, error)
- func RemoveSudoFilesPlan(files []string) ([]Step, error)
- func RevokePlan(caps upstream.ManageCapabilities, r Revoke) ([]Step, error)
- type StepKind
- type User
Constants ¶
const JITGroup = "postern-jit"
* JITGroup, postern'in açtığı geçici hesapların üye olduğu grup. * * ⚠️ SİLMENİN ÖN KOŞULU BU ÜYELİK. Ada bakarak silmek, postern'den * önce var olan bir hesabı yok edebilirdi; göç 022'nin dersi * ("satır kararlı kimlikle anahtarlanır, adla değil") burada da * geçerli. Grup, "bu hesabı ben açtım" demenin makinedeki hâli.
const MinJITGID = 1000
* MinJITGID, bir geçici hesabın alınabileceği en küçük grup numarası. * * ⚠️ ALTINDAKİLER KORUNUYOR. Linux'ta 1000'in altı sistem gruplarıdır * (login.defs: SYS_GID_MAX 999, GID_MIN 1000): root, wheel/sudo, adm, * shadow, docker… Bunlardan birine üyelik, hiçbir sudo kuralı yazmadan * root'a giden bir yol — docker grubu root eşdeğeridir, shadow parola * özetlerini okutur. Geçici hesap yalnızca kullanıcı gruplarına * alınabilir; sınır kullanıcının kararı (2026-09-13).
Variables ¶
var ErrUnconfirmed = errors.New("the target did not confirm whether the step ran")
* ErrUnconfirmed, hedefin adımın ÇALIŞIP ÇALIŞMADIĞINI bildirmediğini * söyler: kanal açılamadı, exec isteği reddedildi, kanal çıkış kodu * gelmeden kapandı, ya da bağlam komut koşarken doldu. * * ⚠️ NE "BAŞARISIZ" NE "ULAŞILAMADI" — VE ÜÇÜNCÜ SINIF OLMADAN İKİSİ DE * YALAN SÖYLÜYORDU. Bağlam sudoers kurulumunun ortasında dolarsa * postern kanalı kapatıyor ama OpenSSH pty'siz çocuğa sinyal * göndermiyor: `install` root olarak BİTİYOR. Bunu "ulaşılamadı, hiçbir * şey olmadı" diye raporlamak, kurulmuş bir sudo kuralını yok saymak; * "başarısız" diye raporlamak operatörü hedefin günlüklerine yollamak * olurdu. Doğru cümle "bilmiyoruz" ve rapor tam olarak onu söylüyor.
var ErrUnreachable = errors.New("target could not be reached")
* ErrUnreachable, makineye HİÇ ULAŞILAMADIĞINI söyler. * * ⚠️ "KOMUT DÜŞTÜ" İLE "MAKİNEYE ULAŞAMADIM" AYRI ŞEYLER — VE KARIŞTIKLARI * HÂLİ ÖLÇTÜK. Yayılım raporu "group.add failed" diyordu; gerçek ise * bağlantının hiç kurulamamasıydı. İkisi operatöre bambaşka şeyler * söylüyor: ilki "hedef reddetti, bak", ikincisi "makine kapalı, tekrar * dene". Hedefi, taşıma katmanının hatasıyla suçlamak bu projenin * reddettiği türden bir rapor.
Functions ¶
func PrincipalsPath ¶
* PrincipalsPath, sshd'nin AuthorizedPrincipalsFile desenini bir hesap * için yola çevirir. Boş desen → "" (dosya gerekmiyor). * * ⚠️ DESENDE %u ŞART. %u'suz bir desen tek bir PAYLAŞILAN dosya demek * ve ona yazmak makinedeki herkesin principal listesini ezmek olurdu. * %h (ev dizini) gibi öbür belirteçler desteklenmiyor: ev dizinini * varsaymak yerine reddetmek, yanlış yere dosya yazmaktan iyi. Sonuç * kabuk komutuna giriyor; yol karakter kontrolünden geçmek zorunda.
func PrincipalsPatternFor ¶
* PrincipalsPatternFor, AuthorizedPrincipalsFile deseninin HESABIN * BAĞLAMINDA etkin değerini okur. * * ⚠️ BAĞLAMSIZ OKUMA MATCH BLOKLARINI GÖRMÜYOR ve bu ölçüldü. Yetenek * sondası `sudo -n sshd -T` çalıştırıyor; o çıktı yalnızca genel * yapılandırmanın birleşimi. Demo hedefine * * Match User jitayse * AuthorizedPrincipalsFile /etc/ssh/jit_principals/%u * * eklenip iki okuma karşılaştırıldı: bağlamsız okuma * `/etc/ssh/auth_principals/%u`, `-C user=jitayse` ile okuma * `/etc/ssh/jit_principals/%u` dedi. Yani Match bloklu bir kurulumda * postern dosyayı sshd'nin BAKMADIĞI yere yazıyor; hak "uygulandı" * görünüyor ve kişi giremiyor. * * ⚠️ KOMUTA GİREN TEK DEĞİŞKEN, ŞEKLİ DOĞRULANMIŞ HESAP ADI. * CapabilityCommands bilerek sabit; oraya değişken koymak config'e * erişen herkese filoda komut çalıştırma yetkisi verirdi. Burası plan * katmanı: ad zaten `^[a-z_][a-z0-9_.-]{0,31}$` kümesinde (kabuk * metakarakteri yok) ve uymayan ad için komut hiç kurulmuyor, genel * değere düşülüyor. * * ⚠️ OKUNAMAZSA GENEL DEĞER. sshd'si olmayan, sudo'suz ya da yaşlı bir * hedefte bu okuma başarısız olabiliyor; o durumda bağlamsız değer * bugünkü davranışın aynısı. Sessiz değil: çağıran farkı raporluyor.
func SudoPath ¶
SudoPath, bir grup için postern'in yazdığı dosyanın yolu. Dışa açık: hedefin durumunu okuyan taraf (Observed.PosternSudoers) aynı yolu kullanmazsa plan dosyayı hiç "var" görmez ve her koşuda yeniden yazar.
func UserSudoPath ¶
UserSudoPath, bir hesap için postern'in yazdığı dosyanın yolu. Grup dosyalarından ayrı bir önek: "dba" adlı grup ile "dba" adlı hesabın kuralları aynı dosyaya düşmesin.
Types ¶
type AccountFacts ¶
AccountFacts, sökme planının bir hesap hakkında bilmesi gerekenler.
func Account ¶
* Account, bir hesabın numarasını, evini ve gruplarını hedeften okur — * sökme planının varsayım değil ölçüm istediği üç şey. * * ⚠️ SİLMEDEN ÖNCE OKUNUYOR. Ad-UID eşlemesi userdel ile kayboluyor ve * kalan dosyaların raporu o numarayla aranıyor; sonradan sorulsa cevap * yok. Ev dizini de getent'ten: "/home/<ad>" varsaymak, farklı evi olan * bir hesapta karalama yolu kontrolünü yanlış yere baktırırdı.
func (AccountFacts) InJITGroup ¶
func (a AccountFacts) InJITGroup() bool
InJITGroup, hesabın postern tarafından açıldığının kanıtı.
type Desired ¶
type Desired struct {
Groups []Group
Users []User
/*
* PrincipalsFile, hedefin sshd'sinin AuthorizedPrincipalsFile deseni
* (upstream.ManageCapabilities.PrincipalsFile). Boş değilse geçici
* hesabın dosyası yazılıyor; boşsa sshd giriş adını sertifikanın
* principal'ında arıyor ve dosya gerekmiyor.
*/
PrincipalsFile string
}
Desired, bir hedefte olması istenen durum.
type Group ¶
type Group struct {
Name string
// Sudo, gruba yazılacak kural. Komutu yoksa sudo dosyası hiç
// yazılmıyor — "sudosuz grup" geçerli bir istek.
Sudo sudoers.Rule
}
Group, hedefte bulunması istenen grup ve onun sudo kuralı.
type GroupFacts ¶
* GroupFacts, bir grubun hedefteki kullanımı: üyeleri ve birincil grubu o * olan hesaplar. Geri alma "boş mu" sorusunu bununla cevaplıyor.
func GroupUsage ¶
* GroupUsage, bir grubun üyelerini ve birincil grubu o olan hesapları okur. * * ⚠️ İKİSİ DE OKUNUYOR. /etc/group'taki üye listesi birincil grubu * göstermiyor: `useradd -g dba veli` ile açılan hesap dba'nın üyesi * görünmez ama grubu siler silmez o hesap sahipsiz bir GID'e düşer.
func (GroupFacts) UsedByOthers ¶
func (f GroupFacts) UsedByOthers(except string) bool
UsedByOthers, except dışında birinin grubu kullanıp kullanmadığı.
type GroupInfo ¶
type GroupInfo struct {
Name string `json:"name"`
GID int `json:"gid"`
Members []string `json:"members"`
}
GroupInfo, hedefteki bir grubun adı, numarası ve üyeleri.
type Observed ¶
type Observed struct {
Groups map[string]bool
// GIDs, var olan grupların numaraları — sistem grubu ayrımı için.
GIDs map[string]int
// Users, hesabın üye olduğu gruplar.
Users map[string][]string
// PosternSudoers, postern'in yazdığı sudo dosyalarının içeriği.
PosternSudoers map[string]string
// Principals, geçici hesapların principals dosyalarının içeriği (yol → içerik).
Principals map[string]string
}
Observed, hedefte ŞU AN olan durum.
func Observe ¶
* Observe, istenen durumun ilgilendirdiği her şeyi hedeften okur: gruplar * var mı, hesaplar hangi gruplarda, postern'in sudo dosyalarında ne var. * * ⚠️ YALNIZCA SORULANI OKUYOR. Makinenin bütün grup ve hesap listesini * çekmek daha kolay olurdu ama hedefin envanterini bastion'a taşımak * demekti; plan neyi değiştirecekse yalnızca onu soruyor.
type Outcome ¶
type Outcome string
Outcome, tek bir adımın sonucu.
const ( OutcomeDone Outcome = "done" OutcomeFail Outcome = "failed" // OutcomeUnreachable, makineye ulaşılamadı: hedefin suçu değil. OutcomeUnreachable Outcome = "unreachable" // OutcomeUnconfirmed, adım gitti ama hedef koşup koşmadığını // bildirmedi — makinede olmuş da olabilir (bkz. ErrUnconfirmed). OutcomeUnconfirmed Outcome = "not confirmed" // OutcomeSkipped, önceki bir adım düştüğü için HİÇ DENENMEDİ. OutcomeSkipped Outcome = "not attempted" )
type Report ¶
type Report struct {
Results []Result
}
* Report, bir hedefteki koşunun tamamı. * * ⚠️ "KAÇ TANE YAPILDI" YETMİYOR, "KAÇ TANE DENENMEDİ" DE GEREKİYOR. * Yarıda kalmış bir koşuda kalan adımları saymamak, makinenin ne * durumda olduğunu okuyamamak demek — ve bu paketin reddettiği tek şey * yarım uygulanmış makine.
func Apply ¶
* Apply, adımları sırayla çalıştırır ve İLK HATADA DURUR. * * ⚠️ DEVAM ETMEK, YARIM UYGULAMANIN KENDİSİ. Örnekler soyut değil: * doğrulama adımı düşmüşken kurulum adımını koşturmak, geçersiz bir * sudoers dosyasını yerine koyar ve o makinede herkesin sudo'sunu * götürür — postern'in kendi hesabı dahil, yani makine kendini * onaramaz. Grup açılamamışken kullanıcıyı o gruba eklemek de hata * verir, ama daha kötüsü: hata mesajı asıl sebebi değil sonucu * gösterir. * * ⚠️ DENENMEYEN ADIMLAR DA RAPORA GİRİYOR. Operatör makinenin hangi * noktada kaldığını görmeli; eksik bir liste, "geri kalanı yapıldı * mı" sorusunu cevapsız bırakır.
func (Report) Unconfirmed ¶
func (Report) Unreachable ¶
type Result ¶
type Result struct {
Step Step
Outcome Outcome
// Output, hedefin çıktısı — denetim satırına giren şey.
Output string
Err error
}
Result, bir adımın sonucu ve hedefin söyledikleri.
type Revoke ¶
type Revoke struct {
User string
Mode RevokeMode
// Home, hesabın ev dizini (getent'ten okunuyor).
Home string
/*
* Scratch, ev dizini dışında silinecek YOLLAR — operatörün
* önceden bildirdikleri.
*
* ⚠️ "KULLANICIYA AİT HER ŞEYİ SİL" DEĞİL. `find / -uid N -delete`
* bir üretim makinesini bir kez mahveder: yanlış UID, paylaşılan
* dizinde bırakılmış bir dosya, ya da bir editörün sahipliğini
* değiştirdiği sistem dosyası. Kapsam bildirilmiş yollarla
* sınırlı; dışarıda kalanlar SİLİNMİYOR, raporlanıyor.
*/
Scratch []string
/*
* UID, hesabın numarası — SİLMEDEN ÖNCE okunuyor.
*
* ⚠️ RAPOR ADI DEĞİL NUMARAYI KULLANIYOR — ÖLÇÜLDÜ. Adla arama
* `userdel`den sonra çalışmıyor: ad artık çözülmüyor, find hata
* veriyor ve rapor SESSİZCE BOŞ dönüyor. Yani kalan dosyaları
* gösteren şey, tam da işe yarayacağı anda hiçbir şey göstermiyordu.
*/
UID int
// InJITGroup, hesabın postern tarafından açıldığının kanıtı.
InJITGroup bool
// SudoFiles, bu hesap için yazılmış postern sudo dosyaları.
SudoFiles []string
// PrincipalsFile, hesabın principals dosyası (PrincipalsPath ile
// çözülmüş); boşsa dosya yok.
PrincipalsFile string
/*
* DeleteGroups, hesapla birlikte silinecek gruplar — çağıranın
* "postern açtı ve başka kimse kullanmıyor" diye ÖLÇTÜĞÜ gruplar
* (GroupUsage). Plan bunu yeniden ölçmüyor; hesap silindikten sonra
* groupdel'e veriyor. Yalnızca silme kipinde.
*/
DeleteGroups []string
}
Revoke, geri alınacak hak.
type RevokeMode ¶
type RevokeMode string
RevokeMode, hakkın nasıl geri alınacağı.
const ( /* * ModeLock, kalıcı hesaplar için: hesap kilitleniyor, süreçleri * kesiliyor, ama SİLİNMİYOR. * * ⚠️ SİLMEMEK BİR EKSİKLİK DEĞİL, KARAR. Silinen hesabın UID'si * yeniden kullanılabiliyor; sonraki hesap aynı numarayı alırsa * öncekinin bıraktığı bütün dosyaların sahibi oluyor. Kalıcı * hesap, dosya sahipliğinin çıpası — "bunu kim bıraktı" * sorusunun altı ay sonraki tek cevabı. */ ModeLock RevokeMode = "lock" /* * ModeDelete, JIT hesapları için: hesap ve dosyaları siliniyor. * * ⚠️ YALNIZCA POSTERN'İN AÇTIĞI HESAPLARDA. JIT hesabının o * makinede kalıcı bir işi yok; ama aynı ada sahip, postern'den * önce var olan bir hesabı silmek başka bir şey olurdu. */ ModeDelete RevokeMode = "delete" )
type Runner ¶
type Runner interface {
// Exec, komutu çalıştırır; stdin boş değilse girdi olarak verir.
Exec(ctx context.Context, command, stdin string) (string, error)
}
* Runner, hedefte tek bir komut çalıştırabilen şey. * * ⚠️ ARAYÜZ, SSH'A BAĞLI DEĞİL. Uygulamanın kararları — nerede durulacak, * ne kaydedilecek — bir bağlantı kurmadan ölçülebilmeli; yoksa "ilk * hatada dur" gibi bir kuralın testi, gerçek bir makineye ve gerçek bir * arızaya bağlı olurdu.
type SSHRunner ¶
type SSHRunner struct {
// contains filtered or unexported fields
}
* SSHRunner, açık bir yönetim bağlantısı üzerinde adımları çalıştırır. * * ⚠️ BAĞLANTI ADIM BAŞINA DEĞİL, KOŞU BAŞINA. Her adım için yeniden * bağlanmak, hedefin günlüğüne bir yayılım koşusu başına onlarca root * girişi yazardı — makinenin sahibi için okunamaz bir iz.
func Connect ¶
func Connect(ctx context.Context, t model.Target, authority *ca.CA, actor, reason string) (*SSHRunner, error)
* Connect, hedefe yönetim hesabıyla bağlanır ve bir Runner döner. * * actor ve reason hedefin kendi sshd günlüğüne düşen KeyID'ye giriyor * (bkz. upstream.DialManagement). * * ⚠️ HATA upstream'İN SINIFIYLA, ErrUnreachable'A SARILMADAN DÖNÜYOR — * VE İLK HÂLİ SARIYORDU. Sarmak, reddedilen bir sertifikayı ve değişmiş * bir host anahtarını da "ulaşılamadı" kovasına atıyordu: panelde "hedef * sertifikayı reddetti" cümlesinin hemen altında ham metin "target could * not be reached:" diye başlıyor, iki satır birbirini çürütüyordu. Daha * önemlisi yanlış iş söylüyordu: ulaşılamayan makinede beklemek işe * yarar, CA'ya güvenmeyen makinede asla. upstream/hostkey.go bu * düzleştirmeyi ölçüp ayrı sınıflar koymuştu. Bağlantı kurulamadıysa * ortada bir Apply raporu yok; çağıran upstream.ErrRefused, * ErrHostKeyMismatch, ErrHandshake ve ErrUnreachable'a bakarak karar * veriyor.
func (*SSHRunner) Exec ¶
* Exec, tek bir adımı çalıştırır ve STDOUT'u döner. * * ⚠️ "KOMUT DÜŞTÜ" İLE "MAKİNEYE ULAŞAMADIM" AYRIMI BURADA TAHMİN DEĞİL. * `ssh` ikilisiyle koşarken elimizde yalnızca çıkış kodu vardı ve 255'i * sezgiyle yorumluyorduk — uzak komutun kendisi de 255 dönebilir. * Protokol seviyesinde belirsizlik yok: CommandError, hedefin komutu * çalıştırıp bir kod bildirdiği anlamına geliyor; öbür her şey * cevapsızlık. * * Hata sebebi (stderr) CommandError'ın metninde duruyor ve Apply onu * Result.Err olarak kaydediyor; stdout'a karıştırılmıyor, çünkü stdout * VERİ olarak da okunuyor (bkz. upstream.Conn.Exec).
type Step ¶
type Step struct {
Kind StepKind
/*
* Command, hedefte AYNEN çalıştırılacak komut — sudo dahil.
*
* ⚠️ SUDO PLANDA, UYGULAYICIDA DEĞİL. Plan operatöre gösterilen
* şey; "ne çalışacak" sorusunun cevabı burada eksiksiz durmalı.
* Uygulayıcı komutu değiştirirse, onaylanan ile koşan farklı olur.
*/
Command string
// Content, varsa komutun stdin'ine verilecek metin.
Content string
// Why, panelde ve denetim satırında görünen cümle.
Why string
// Subject, adımın üzerinde çalıştığı ad (grup ya da hesap) — rapordan
// "hangi grubu açtık" diye geri okunabilsin diye; komut metnini
// ayrıştırmak kırılgan olurdu.
Subject string
}
* Step, hedefte çalıştırılacak tek bir iş. * * ⚠️ DEĞİŞKEN İÇERİK Command'A GİRMİYOR. sudoers metni Content'te * duruyor ve hedefe STDIN ile gidiyor; komut satırı sabit kalıyor. * Aksi hâlde bir grup adı ya da kural metni hedefin kabuğuna * yorumlanacak biçimde ulaşırdı — ve bu paket root yetkisiyle * çalışıyor.
func GroupDeleteSteps ¶
func GroupDeleteSteps(caps upstream.ManageCapabilities, groups []string) ([]Step, error)
* GroupDeleteSteps, postern'in açtığı ve artık boş olan grupları silen * adımlar. Adlar buraya gelmeden ölçülmüş olmalı (GroupUsage); burada * yalnızca ad biçimi ve postern-jit koruması var — kanıt grubu hiçbir * koşulda silinmiyor, başka geçici hesaplar ona bağlı.
func Plan ¶
* Plan, gözlenen durumdan istenen duruma giden adımları üretir. * * ⚠️ YÖNETİLEMEYEN HEDEFE HİÇ ADIM ÜRETİLMİYOR. Yarısı uygulanmış bir * makine, hiç dokunulmamış olandan kötü: panel yeşil görünür ve kimse * bakmaz. Eksikler yoklamada zaten adlandırılmış durumda.
func PrincipalRemoveStep ¶
* RemoveSudoFilesPlan, yalnızca postern'in yazdığı sudo dosyalarını * kaldıran adımlar — hesabı OLMAYAN bir hak için. * * ⚠️ NEDEN AYRI: sökme planı hesabın UID'sini ve üyeliğini istiyor, çünkü * silme kararı onlara bağlı. Hesap çoktan gitmişse (yedek süre, elle * silme, önceki yarım deneme) geride yalnızca sudo dosyası kalmış * olabilir ve o dosya hesap yeniden açıldığı gün yeniden yetki verir. * Yol kontrolü sökme planınınkinin aynısı.
* PrincipalRemoveStep, hesabın principals dosyasını kaldıran adım. * * ⚠️ YOL HESABIN ADIYLA BİTMEK ZORUNDA. Silinen şey `rm -f` ile gidiyor; * desenden gelen yol bir başka hesabın (ya da paylaşılan bir dosyanın) * yolu olsaydı, geri alma başka birinin girişini kapatırdı.
func RemoveSudoFilesPlan ¶
func RevokePlan ¶
func RevokePlan(caps upstream.ManageCapabilities, r Revoke) ([]Step, error)
* RevokePlan, hakkı geri alan adımları üretir. * * ⚠️ SIRA: SUDO, SÜREÇLER, SONRA HESAP. Kuralı önce almak, süreçler * ölene kadar geçen sürede yeni bir yükseltmeyi engelliyor. Süreçleri * hesaptan ÖNCE öldürmek ise zorunlu: hesap silindiğinde ad-UID eşlemesi * kayboluyor ve süreçler sahipsiz bir numarayla koşmaya devam ediyor.
type StepKind ¶
type StepKind string
StepKind, adımın türü.
const ( StepGroupAdd StepKind = "group.add" StepUserAdd StepKind = "user.add" StepUserGroup StepKind = "user.group" StepSudoStage StepKind = "sudo.stage" StepSudoCheck StepKind = "sudo.check" StepSudoInstall StepKind = "sudo.install" StepPrincipal StepKind = "principal.write" StepUserUnlock StepKind = "user.unlock" )
const ( StepSudoRemove StepKind = "sudo.remove" StepPrincipalRemove StepKind = "principal.remove" StepKill StepKind = "user.kill" StepLock StepKind = "user.lock" StepUserDel StepKind = "user.delete" StepGroupDel StepKind = "group.delete" StepScratchDel StepKind = "scratch.delete" StepReportOwned StepKind = "files.report" )
type User ¶
type User struct {
Name string
Groups []string
/*
* JIT true ise hesap GEÇİCİ: postern açıyor, süresi dolunca siliyor.
*
* ⚠️ ÜYELİK KANIT: JIT hesabı postern-jit grubuna giriyor ve sökme
* planı YALNIZCA o gruptaki hesabı siliyor. Bu üyeliği kuran tek yer
* burası; kurulmasaydı hiçbir hesap silinemez, ya da daha kötüsü,
* kanıt elle verilirdi (canlı test bir süre öyle yapıyordu).
*/
JIT bool
/*
* ExpiresAt, JIT hesabının süresi — hedefe `useradd -e` ile de
* yazılıyor.
*
* ⚠️ YEDEK, ASIL MEKANİZMA DEĞİL. Sökmeyi postern yapıyor; bu tarih,
* postern ölürse ya da hedefe ulaşamazsa hesabın sonsuza dek açık
* kalmaması için. Gün çözünürlüğünde ve süre bitiminden SONRAKİ güne
* yuvarlanıyor: yedeğin asıl süreden önce vurması, geçerli bir hakkı
* ortasından kesmek olurdu.
*/
ExpiresAt time.Time
/*
* Sudo, yalnızca bu hesaba yazılacak kural (grup kuralından ayrı).
* nil ise kullanıcı dosyası yazılmıyor. Dosya postern-user-<ad>
* adıyla gidiyor ve sökme planı onu adıyla kaldırıyor.
*/
Sudo *sudoers.Rule
}
User, hedefte bulunması istenen hesap.