bastion 0.60.0
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
package main
|
||||
|
||||
// askquarantine.go — the machine holds an account whose asks keep being
|
||||
// refused, the way quarantine.go holds a Host pairing: five refused asks
|
||||
// inside ten minutes hold that account's asks for ten minutes, doubling on
|
||||
// every repeat, forgiven after a clean day (the same arithmetic,
|
||||
// quarantineEntry.strike). Asking is how an agent on this computer reaches
|
||||
// the owner, so an agent that keeps asking for what is refused is the local
|
||||
// form of a Host probing the management lane.
|
||||
//
|
||||
// Keyed by the asking account (the kernel's answer on ask.sock, not anything
|
||||
// the asker says), and kept in the owner's factor directory beside the
|
||||
// requests themselves, never on the heartbeat: which accounts on this
|
||||
// computer asked for what is not a Host's business.
|
||||
//
|
||||
// What strikes: the owner denying an ask, and refusals that are the asker's
|
||||
// own doing (not an agent this bastion sees, an ask the owner refused in the
|
||||
// last day, too many waiting). What does not: the owner having no code set
|
||||
// up, a store that could not be written — this machine's state, not the
|
||||
// asker's.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const askQuarantineFile = "ask_quarantine.json"
|
||||
|
||||
var askQuarantineMu sync.Mutex
|
||||
|
||||
func askQuarantinePath(dir string) string { return filepath.Join(dir, askQuarantineFile) }
|
||||
|
||||
func askQuarantineLoad(dir string) map[string]*quarantineEntry {
|
||||
m := map[string]*quarantineEntry{}
|
||||
if raw, err := os.ReadFile(askQuarantinePath(dir)); err == nil {
|
||||
_ = json.Unmarshal(raw, &m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func askQuarantineSave(dir string, m map[string]*quarantineEntry) error {
|
||||
raw, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return atomicWrite(askQuarantinePath(dir), raw)
|
||||
}
|
||||
|
||||
// askStrike counts one refused ask from account. The hold that starts on the
|
||||
// last strike is audited, so the owner sees it in Activity.
|
||||
func askStrike(dir, account string, now time.Time) {
|
||||
if dir == "" || account == "" {
|
||||
return
|
||||
}
|
||||
askQuarantineMu.Lock()
|
||||
defer askQuarantineMu.Unlock()
|
||||
m := askQuarantineLoad(dir)
|
||||
e := m[account]
|
||||
if e == nil {
|
||||
e = &quarantineEntry{}
|
||||
m[account] = e
|
||||
}
|
||||
held := e.strike(now)
|
||||
askQuarantinePrune(m, now)
|
||||
_ = askQuarantineSave(dir, m)
|
||||
if held {
|
||||
dcnAudit(dir, askHoldOp, 0, account, "held until "+e.Until+": "+strconv.Itoa(quarantineStrikes)+
|
||||
" refused asks within "+askWindowWords())
|
||||
}
|
||||
}
|
||||
|
||||
const askHoldOp = "local_ask_hold"
|
||||
|
||||
// askQuarantinePrune drops the accounts there is nothing left to remember
|
||||
// about: not held, no refusal inside the window, and no hold within the
|
||||
// forgiving day (strike would reset their doubling anyway). Accounts are the
|
||||
// kernel's names, and a user with subordinate ids can present thousands of
|
||||
// them, so without this the file grows by one entry per id that was ever
|
||||
// refused and is rewritten whole on every strike.
|
||||
func askQuarantinePrune(m map[string]*quarantineEntry, now time.Time) {
|
||||
for acct, e := range m {
|
||||
if e == nil {
|
||||
delete(m, acct)
|
||||
continue
|
||||
}
|
||||
if quarantinedAt(e, now) {
|
||||
continue
|
||||
}
|
||||
recent := false
|
||||
for _, ts := range e.Refusals {
|
||||
if t, err := time.Parse(time.RFC3339, ts); err == nil && now.Sub(t) < quarantineWindow {
|
||||
recent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if recent {
|
||||
continue
|
||||
}
|
||||
if t, err := time.Parse(time.RFC3339, e.LastHold); err == nil && now.Sub(t) <= quarantineForgive {
|
||||
continue
|
||||
}
|
||||
delete(m, acct)
|
||||
}
|
||||
}
|
||||
|
||||
// askHeldUntil: when account may ask again, or the zero time.
|
||||
func askHeldUntil(dir, account string, now time.Time) time.Time {
|
||||
if dir == "" || account == "" {
|
||||
return time.Time{}
|
||||
}
|
||||
askQuarantineMu.Lock()
|
||||
defer askQuarantineMu.Unlock()
|
||||
e := askQuarantineLoad(dir)[account]
|
||||
if !quarantinedAt(e, now) {
|
||||
return time.Time{}
|
||||
}
|
||||
t, _ := time.Parse(time.RFC3339, e.Until)
|
||||
return t
|
||||
}
|
||||
|
||||
// askHoldRefusal is what a held account is told.
|
||||
func askHoldRefusal(account string, until time.Time) string {
|
||||
return fmt.Sprintf("asks from %s are on hold until %s: %d of them were refused within %s. "+
|
||||
"The machine's owner can lift the hold", account, until.Local().Format("15:04"), quarantineStrikes,
|
||||
askWindowWords())
|
||||
}
|
||||
|
||||
func askWindowWords() string { return strconv.Itoa(int(quarantineWindow.Minutes())) + " minutes" }
|
||||
|
||||
// askQuarantineClear — the owner lifts the hold, and the strikes with it.
|
||||
func askQuarantineClear(dir, account string) error {
|
||||
askQuarantineMu.Lock()
|
||||
defer askQuarantineMu.Unlock()
|
||||
m := askQuarantineLoad(dir)
|
||||
if _, ok := m[account]; !ok {
|
||||
return fmt.Errorf("asks from %s are not held, and none of theirs were refused recently", account)
|
||||
}
|
||||
delete(m, account)
|
||||
if err := askQuarantineSave(dir, m); err != nil {
|
||||
return err
|
||||
}
|
||||
dcnAudit(dir, askHoldOp, 0, account, "lifted by the owner")
|
||||
return nil
|
||||
}
|
||||
|
||||
// cmdQuarantineClearAsker is `quarantine clear --asker <account>`. The holds
|
||||
// live in the owner's factor directory, so only the account that holds it —
|
||||
// the administrator, on a whole-machine install — finds them.
|
||||
func cmdQuarantineClearAsker(account string) error {
|
||||
if account == "" {
|
||||
return errors.New("which account? the hold names it")
|
||||
}
|
||||
dir := existingMachineFactorDir()
|
||||
if dir == "" {
|
||||
return errors.New("no holds on asks are kept for this account. On a machine the bastion is installed " +
|
||||
"for as a whole, they are the administrator's: `sudo -H privatae-bastion quarantine clear --asker " +
|
||||
account + "`")
|
||||
}
|
||||
if err := askQuarantineClear(dir, account); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("agents running as %s may ask this machine again\n", account)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user