Documentation
¶
Overview ¶
Package console is the human side of a lockkit product: it holds the credentials and the only backend connection, enforces the approved policy, shows every request that needs a human decision on an approval screen, and writes the audit trail. Clients (a product's CLI and MCP server) reach it over the per-profile socket and never see a secret.
A product supplies a Backend (its methods, its connection, its checks and its policy) and calls Run. The pieces Run is made of — the terminal, the approvers, the isolation checks, the request loop — are exported for products that assemble their own console.
Index ¶
- Constants
- Variables
- func Bell(io IO)
- func CloseVerifier(v fingerprint.Verifier)
- func Decode(req ipc.Request, v any) *ipc.Response
- func DenyReason(ans string) string
- func ErrResp(id int64, code int, msg string) ipc.Response
- func FormatChanges(changes []config.Change) []string
- func Harden() error
- func Level(ctx context.Context, quantum time.Duration, start time.Time)
- func Loosens(changes []config.Change) bool
- func OKResp(id int64, v any) ipc.Response
- func PeerFrom(ctx context.Context) (ipc.Cred, bool)
- func PeerText(ctx context.Context) string
- func PendingResp(id int64, lock string, p Pending) ipc.Response
- func ProcessText(pid int) string
- func Run(ctx context.Context, p kit.Product, b Backend, o Options) error
- func SameUserRefusal(p kit.Product) string
- func SameUserRefused(p kit.Product, sys *sysconf.Config, allowSameUser bool) error
- func StampPeer(ctx context.Context, b *audit.Base)
- func TicketParam(params json.RawMessage) string
- func WithPeer(ctx context.Context, c ipc.Cred) context.Context
- func WithdrawnResp(id int64, tk string) ipc.Response
- type Approval
- type Approver
- type Ask
- type Asker
- type AsyncApprover
- type Backend
- type Beller
- type Credentials
- type Decision
- type Env
- type Finding
- type Handler
- type Hub
- type HubOptions
- type HubTicket
- type IO
- type Isolation
- func (iso *Isolation) Boundary() string
- func (iso *Isolation) Level() int
- func (iso *Isolation) Listen(path string) (net.Listener, error)
- func (iso *Isolation) PeerCheck() func(ipc.Cred) bool
- func (iso *Isolation) RefuseX11IfProduction(production bool) error
- func (iso *Isolation) SocketPath(projectKey, profile string) (string, error)
- type IsolationEnv
- type Key
- type KeySource
- type LineSource
- type Loop
- type Options
- type Outcome
- type PIIBackend
- type Pending
- type Policy
- type Profile
- type Result
- type Screen
- type Server
- func (s *Server) Approve(ctx context.Context, id int64, sc Screen) Approval
- func (s *Server) Audit(rec audit.Record)
- func (s *Server) AuditBase(ctx context.Context, b audit.Base)
- func (s *Server) Auto() bool
- func (s *Server) CheckPolicy()
- func (s *Server) Close()
- func (s *Server) Command(ctx context.Context, line string)
- func (s *Server) End(reason string)
- func (s *Server) Ended() (string, bool)
- func (s *Server) Handle(ctx context.Context, req ipc.Request) ipc.Response
- func (s *Server) Hub() *Hub
- func (s *Server) Key(k Key)
- func (s *Server) Level(ctx context.Context, start time.Time)
- func (s *Server) Loop() *Loop
- func (s *Server) Lost()
- func (s *Server) Policy() Policy
- func (s *Server) PolicyPending() bool
- func (s *Server) Println(line string)
- func (s *Server) Safe(text string) string
- func (s *Server) Sanitizer() *sanitize.Sanitizer
- func (s *Server) Session(ctx context.Context) (Session, error)
- func (s *Server) ShowResults() bool
- func (s *Server) Status() StatusResult
- func (s *Server) Tick()
- func (s *Server) UnmaskAllowed() bool
- type ServerConfig
- type Session
- type Sized
- type StatusLine
- type StatusResult
- type Terminal
- func (t *Terminal) Ask(ctx context.Context, prompt string, timeout time.Duration) (string, bool)
- func (t *Terminal) AskSecret(ctx context.Context, prompt string) ([]byte, error)
- func (t *Terminal) Bell()
- func (t *Terminal) Interactive() error
- func (t *Terminal) Keys() <-chan Key
- func (t *Terminal) Lines() <-chan string
- func (t *Terminal) Println(s string)
- func (t *Terminal) Restore()
- func (t *Terminal) SetKeyHandler(f func(Key))
- func (t *Terminal) SetStatus(s string)
- func (t *Terminal) Width() int
- type Withdrawer
- type Withdrawn
Constants ¶
const ( // ApprovalTimeout bounds every human answer. ApprovalTimeout = 5 * time.Minute // PolicyPoll is how often the config files are checked for changes. PolicyPoll = 2 * time.Second // ResponseQuantum levels the response time of a request whose answer // could otherwise tell the agent how long a human or a backend took. ResponseQuantum = 250 * time.Millisecond )
Timeouts of a console session. The idle timeout and the maximum session length are the profile's.
const CredentialsNone = "none"
CredentialsNone is the credentials mode of a backend that needs no secret (a local file, a socket the account owns).
const FingerDeadline = 30 * time.Second
FingerDeadline bounds the wait for a finger after a typed yes.
const HubApproval = "hub"
HubApproval is the audit approval of a request handed to an AsyncApprover.
const StandaloneReason = "the lock was restarted without its hub (standalone)"
StandaloneReason is the reason of the plans a standalone start withdraws, as the hub and the plans store record it.
Variables ¶
var ErrConfig = errors.New("configuration error")
ErrConfig marks usage and configuration errors (exit code 3).
var ErrSecretTimeout = errors.New("console: no password entered")
ErrSecretTimeout is AskSecret's error when no answer comes within ApprovalTimeout.
var ErrTooManyNoMatch = errors.New("fingerprint: three consecutive non-matching fingers")
ErrTooManyNoMatch is set on the third consecutive no-match: the caller ends the console.
var Paint = ui.Painter{On: true}
Paint styles console lines.
Functions ¶
func CloseVerifier ¶
func CloseVerifier(v fingerprint.Verifier)
CloseVerifier releases a verifier that holds a connection (fprintd's).
func Decode ¶
Decode decodes the params of req into v, refusing unknown fields; nil when they decode (or are absent), else the error response.
func DenyReason ¶
DenyReason is the reason of an answer "n <text>" or "no <text>" (n and no in any case), trimmed, cut at 200 bytes and escaped; "" for any other answer.
func FormatChanges ¶
FormatChanges renders a policy diff, loosenings in red. Every value is escaped: a pattern or a host could otherwise carry terminal escapes that hide a loosening from the human.
func Harden ¶
func Harden() error
Harden keeps the secret out of core dumps and away from same-user ptrace and /proc/<pid>/mem readers: RLIMIT_CORE=0 and PR_SET_DUMPABLE=0.
func Level ¶
Level waits until the next multiple of quantum since start, so that the response time tells nothing finer than quantum (0 or less: no wait).
func PendingResp ¶ added in v0.3.0
PendingResp is the answer to a request waiting for a decision taken elsewhere: a result, not an error, so that the agent stops and waits rather than retries. It names p.Instance when set (hub mode).
func ProcessText ¶
ProcessText names the process behind pid on Linux: its executable path when the kernel lets the console read it (a process of its own account), else the name the process gave itself, labelled as such; "" elsewhere. The uid and pid are the kernel's; the name is the process's choice.
func Run ¶
Run is a product's `console` command: the isolation checks, the policy review, the production confirmation, the credentials and the connection, the backend's checks, the socket, then the request loop until Ctrl-C, logout, :quit or a timeout.
func SameUserRefusal ¶
SameUserRefusal is the one text of the same-user refusal of p.
func SameUserRefused ¶
SameUserRefused is the refusal of a console of p without a separated setup (sys nil, as sysconf.Load returns it), or nil.
func StampPeer ¶
StampPeer sets the peer fields of b from the client behind ctx: its uid, pid and account name. The process name, chosen by the agent, stays on the screen (PeerText) and out of the audit log.
func TicketParam ¶ added in v0.4.0
func TicketParam(params json.RawMessage) string
TicketParam is the hub ticket a request names in its params ("ticket"), "" without one.
Types ¶
type Approval ¶
type Approval struct {
Approved bool
// Auto: approved by auto mode, without a prompt.
Auto bool
// Retype: the human typed Screen.Retype.
Retype bool
// Refusal is the answer to send when neither Approved nor Retype: the
// human denied (with the reason they typed after "n"), the approval
// timed out, the client left, or no finger matched. For a Pending
// request it is the pending answer (PendingResp).
Refusal ipc.Response
// Pending: an AsyncApprover took the request; it is decided elsewhere.
Pending *Pending
}
Approval is the outcome of Env.Approve.
type Approver ¶
type Approver interface {
Approve(ctx context.Context, d Decision) Result
// Mode is the audit value: "session" or "desktop+fingerprint".
Mode() string
}
Approver asks the human for one decision. Each call is one decision: a fingerprint match never carries over.
func NewDesktopTypedApprover ¶
NewDesktopTypedApprover is the level-1 approver: typed answers in a window the agent can type into, audited apart from the session shape.
func NewFingerprintApprover ¶
func NewFingerprintApprover(io IO, v fingerprint.Verifier, finger string, deadline time.Duration) Approver
NewFingerprintApprover wraps the typed approval with a fingerprint of the dedicated finger (system.toml `finger`), named in the prompt (level 2).
func NewTypedApprover ¶
NewTypedApprover is the session approver: typed answers in a login session of the console account, audited as "session".
type Ask ¶ added in v0.3.0
type Ask struct {
// Frame is the colour of the screen's frame.
Frame string
// Production asks for the profile name typed instead of "y".
Production bool
// Retype, when set, is an answer that asks to retype values; Ask then
// returns Approval.Retype. RetypeNote follows it in the prompt:
// "(<Retype> to retype<RetypeNote>) ". An answer equal to the approval
// answer is not offered.
Retype, RetypeNote string
// For names the decision in the fingerprint prompt (Decision.For); ""
// is "to approve the request above".
For string
// Screen is the approval screen (Decision.Screen).
Screen *Screen
}
Ask is the prompt that closes an approval screen.
type Asker ¶ added in v0.3.0
type Asker struct {
IO IO
Approver Approver
// Profile is the profile name, typed to approve on production.
Profile string
// Lock names the product in a pending answer (kit.Product.Name).
Lock string
// End ends the session (too many non-matching fingers); nil: ignored.
End func(reason string)
// Waiting, when set, is told when a decision starts (true) and ends
// (false) waiting for the human.
Waiting func(on bool)
}
Asker takes the human decisions of a console: it closes an approval screen with the prompt and turns the answer into an Approval. Server uses one; a product that draws its own approval screen (and prompts inside it, for values to type) uses one too, so that every approval is decided in one place.
type AsyncApprover ¶ added in v0.3.0
type AsyncApprover interface {
Approver
// Submit hands d over. Pending.Ticket is set even when err is not nil
// (the hub refused it or is unreachable): the request stays pending,
// is never asked on the terminal instead, and the approver pushes the
// ticket again.
Submit(ctx context.Context, d Decision) (Pending, error)
}
AsyncApprover is an Approver whose decisions are taken elsewhere (by lockhub): Submit hands the request over and returns at once, and the request is answered "pending" instead of waiting for a human here. The decision comes back later, through the hub, never through this console. Approve still takes the console's own decisions (a policy change, unmask) and the console's auto mode is off: the hub has its own.
type Backend ¶
type Backend interface {
// Methods are the product's IPC methods, beyond the console's own
// (hello, status, change.request, logout). A name the console serves
// is refused at start.
Methods() map[string]Handler
// Connect opens a session with the credentials the console obtained
// (asked, from the OS keychain, or from Vault). It is called at start
// and again after a lost connection or an expired login. The console
// wipes c.Secret once Connect returns.
Connect(ctx context.Context, c Credentials) (Session, error)
// Checks audits a new session: what the backend account can do beyond
// the profile (a red warning, typed "continue" to go on), or a reason
// to refuse it. On a production profile every finding refuses.
Checks(ctx context.Context, s Session) []Finding
// LoadPolicy reads the current policy from the config files. The
// console calls it at start and every PolicyPoll.
LoadPolicy() (Policy, error)
// Status is the product's part of the status method's answer.
Status(s Session) any
}
Backend is what a product plugs into the console: its methods, its connection to the backend it guards, the checks of that connection and its policy. Everything else — the terminal, the isolation, the approval screen, the modes, the policy review, the session limits, the audit log, the socket — is the console's.
type Beller ¶
type Beller interface {
Bell()
}
Beller is implemented by an IO that can ring the terminal bell, so that a human away from the console notices a prompt waiting for them.
type Credentials ¶
type Credentials struct {
User string
// Secret is nil for a profile with credentials "none".
Secret []byte
}
Credentials are what Connect needs: the user and its secret.
type Decision ¶
type Decision struct {
// Prompt is the typed prompt, already framed by the caller.
Prompt string
// Expected is the typed answer: "y" (any case) or the production
// profile name (exact).
Expected string
// Production selects the exact match.
Production bool
// Retype is the typed answer that asks to retype placeholders ("" when
// not offered).
Retype string
// For names the decision in the fingerprint prompt ("to approve the
// request"): a finger is never offered without saying what it decides.
For string
// Screen is the approval screen the decision is about (nil for the
// console's own decisions). A terminal approver ignores it: the screen
// is already on the terminal; an AsyncApprover sends it on.
Screen *Screen
}
Decision names what a human is approving.
type Env ¶
type Env interface {
// Approve shows s and asks the human (or approves it in auto mode, see
// Screen.NeverAuto). The decision is audited before Approve returns.
Approve(ctx context.Context, id int64, s Screen) Approval
// Auto reports auto mode in effect (never on a production profile).
Auto() bool
// ShowResults reports the clear results switch (Ctrl-O).
ShowResults() bool
// UnmaskAllowed reports whether clients may ask for unmasked output
// (:unmask), each such request approved by the human.
UnmaskAllowed() bool
// Session returns the live session, reconnecting first when the login
// expired (credentials_ttl, a Vault lease) or the connection was lost
// (Lost).
Session(ctx context.Context) (Session, error)
// Lost drops the session after a lost connection: the next Session
// reconnects.
Lost()
// Policy is the approved policy in force.
Policy() Policy
// Audit writes rec as is; AuditBase stamps b with the profile and the
// client behind ctx first.
Audit(rec audit.Record)
AuditBase(ctx context.Context, b audit.Base)
// Println writes one console line; Safe escapes client text for it.
Println(line string)
Safe(s string) string
// Sanitizer scrubs error text for a client or the audit log.
Sanitizer() *sanitize.Sanitizer
// Level levels a response time (Options.Quantum) from start.
Level(ctx context.Context, start time.Time)
// End ends the session after the current request.
End(reason string)
}
Env is what a Handler may use of the console.
type Finding ¶
type Finding struct {
// Refuse stops the console (or the reconnection) whatever the profile.
Refuse bool
Text string
}
Finding is one result of Checks.
type Handler ¶
Handler serves one product method. env is the console: the approval screen, the modes, the session, the audit log.
type Hub ¶ added in v0.3.0
type Hub struct {
Client *hubclient.Client
Store *plans.Store
Instance string
// contains filtered or unexported fields
}
Hub is a console's hub mode: the plans awaiting a decision and the hub client. A product runs a decided ticket with Client.Begin, then Client.Finish.
func OpenHub ¶ added in v0.3.0
OpenHub opens the hub mode of product p: it checks o, reads the pinned hub key and the instance key, and opens the plans and the hub state under the product's state directory (stateDir as Options.StateDir; now nil is time.Now). Run calls it; a product with its own console lifecycle calls it too, then Approver and Watch.
func (*Hub) Approver ¶ added in v0.3.0
func (h *Hub) Approver(base Approver) AsyncApprover
Approver is the hub approver: requests (a Decision with a Screen) go to the hub, the console's own decisions to base.
func (*Hub) Close ¶ added in v0.3.0
Close releases the plans store (its directory lock): Run closes its hub when it returns.
func (*Hub) RenewResp ¶ added in v0.5.0
RenewResp answers the run of expired ticket tk (Client.Begin returned ticket.ErrExpired): the ticket is renewed (Client.Renew, decision H1) and the agent hears pending with the new ticket, which it runs once decided. A ticket that cannot be renewed answers CodeTimeout.
type HubOptions ¶ added in v0.3.0
type HubOptions struct {
// URL is the hub's base URL ("http://lockhub:8443").
URL string
// KeyFile holds the pinned hub key (hubclient.ReadHubKey).
KeyFile string
// Instance is the lock instance's name (hubclient.ValidInstance).
Instance string
// PlainHTTP lets URL be http:// to a hub on a network of its own
// (--hub-plain-http, hubclient.Config.PlainHTTP).
PlainHTTP bool
// DevicesFile holds the approval devices this lock pins
// (--devices-file, hubclient.ReadDevices): decisions taken on a device
// must carry its signature, and the clear screen is sealed to each.
DevicesFile string
// RequireDevice lists the risk classes whose approvals must be signed
// by a pinned device (--require-device, comma-separated).
RequireDevice string
// SealClear seals the clear screen to the pinned devices
// (--seal-clear, hubclient.Config.SealClear).
SealClear bool
// FacadeKeysFile holds the facade keys this lock pins
// (--facade-keys-file, hubclient.ReadFacades): a reveal (Ctrl-O on a
// local facade) is sealed only to a key one of them signed.
FacadeKeysFile string
// Home is the lock's home volume, where the instance key lives
// (hubclient.InstanceKey, made by `hub-key`); "" is the user's home.
Home string
}
HubOptions put a console in hub mode: requests are decided on lockhub, never on this terminal. They are the console flags `lock up` writes: --hub URL --hub-key-file PATH --instance NAME, all three or none. There is no discovery: without them the console is standalone.
func (HubOptions) Args ¶ added in v0.4.0
func (h HubOptions) Args() []string
Args are the flags of h, as Register reads them back: what a product that starts its console again (a handoff) passes on.
func (HubOptions) Enabled ¶ added in v0.3.0
func (h HubOptions) Enabled() bool
Enabled reports whether any hub flag is set.
func (*HubOptions) Register ¶ added in v0.3.0
func (h *HubOptions) Register(fs *flag.FlagSet)
Register adds the hub flags to fs.
type HubTicket ¶ added in v0.3.0
type HubTicket struct {
// PlanID is the plan in Hub.Store, made by Caller.
PlanID string
Caller plans.Caller
Risk ticket.Risk
// Masked is the screen as it leaves the lock (render.MaskScreen).
Masked render.MaskedScreen
// Requester and Workspace are shown by the hub (display only).
Requester ticket.Requester
Workspace string
}
HubTicket is what a product adds to a Screen in hub mode: the plan the decision is about and the masked screen the hub shows. A Screen without one is refused in hub mode, never asked on the terminal.
type IO ¶
type IO interface {
Println(s string)
// Ask prints prompt and reads one line. It returns false when no answer
// came within timeout, when ctx ended (Ctrl-C, client gone) or when the
// terminal is closed.
Ask(ctx context.Context, prompt string, timeout time.Duration) (string, bool)
// AskSecret reads a line with echo off.
AskSecret(ctx context.Context, prompt string) ([]byte, error)
}
IO is the console's terminal. Ask and AskSecret discard pending input before prompting, so type-ahead never answers a prompt.
type Isolation ¶
type Isolation struct {
Product kit.Product
Sys *sysconf.Config
Display sysconf.Display
Self int
GID int
// Desktop is the desktop shape; Verifier is then its fingerprint
// reader (nil at level 1 and in the session shape).
Desktop bool
Verifier fingerprint.Verifier
// contains filtered or unexported fields
}
Isolation is how the console is kept apart from the agent: in a separated setup (sysconf) the console runs as the service account and serves the client group over a shared socket, either in its own login session (the session shape: the kernel keeps the agent out of its terminal) or in a terminal of the console account opened on the agent's desktop (the desktop shape: every approval also takes a fingerprint). Without one it refuses to start (except when IsolationEnv.AllowSameUser is set by a product's test build).
func CheckIsolation ¶
CheckIsolation applies the separation rules before any prompt and before the console writes any state. Warnings go to the terminal; a violation is an error. The production rule on X11 needs the profile and is applied later, by RefuseX11IfProduction.
func (*Isolation) Boundary ¶
Boundary names where the approval screen stands, for the status screen.
func (*Isolation) Level ¶
Level is the security level of the isolation: 2 for the desktop shape with a fingerprint reader, 1 for the desktop shape without, 0 for the session shape (the kernel is the boundary).
func (*Isolation) PeerCheck ¶
PeerCheck decides which peers are served: the console's own account and, in a separated setup, allowed_uids and the client group. The group is looked up at every connection: a uid removed from it loses access at its next connection, not at the next console start. Nil means the console's own uid only.
func (*Isolation) RefuseX11IfProduction ¶
RefuseX11IfProduction applies the production rule once the profile is known: a production profile never runs in an X11 session.
type IsolationEnv ¶
type IsolationEnv struct {
LoadSys func() (*sysconf.Config, error)
Display func() sysconf.Display
UID func() int
UserName func() (string, error)
LoginUID func() (int, bool)
TerminalOwner func() (int, bool)
ClientAllowed func(sys *sysconf.Config, uid int) bool
ClientGID func(sys *sysconf.Config) (int, error)
// AccountExists reports whether uid names an account.
AccountExists func(uid int) bool
// Relay reports a session opened through a relay (sysconf.SessionRelay).
Relay func() string
// MachinectlUIDs lists the callers of running `machinectl shell`
// processes (sysconf.MachinectlShellUIDs).
MachinectlUIDs func() ([]int, bool)
// DesktopEnv reports <PREFIX>DESKTOP=1: the desktop shape is asked for.
DesktopEnv func() bool
// DisplaySet reports a non-empty DISPLAY.
DisplaySet func() bool
// ParentChain lists the ancestors of the process, from the parent up
// (sysconf.ParentChain).
ParentChain func() ([]sysconf.Proc, error)
// NewVerifier opens the fingerprint reader of user.
NewVerifier func(user string) (fingerprint.Verifier, error)
// EnrolledTimeout bounds the start-up question to fprintd; past it, the
// reader counts as unavailable. 0 is 10s.
EnrolledTimeout time.Duration
// AllowSameUser lets the console run without a separated setup, in the
// agent's account. Only a product's test build sets it (a build tag):
// no config file, flag, environment variable or client may.
AllowSameUser bool
}
IsolationEnv holds what CheckIsolation reads from the system; tests replace its functions (RealIsolationEnv is the real one).
func RealIsolationEnv ¶
func RealIsolationEnv(p kit.Product, tty *os.File) IsolationEnv
RealIsolationEnv is the environment of a console of p on terminal tty (nil when unknown).
type Key ¶
type Key int
Key is a control key the human pressed, outside a line. Keys reach the console only from its own terminal: no client can send one.
type KeySource ¶
KeySource is implemented by an IO that reports the control keys pressed in the console: on Keys between prompts, to the handler during one.
type LineSource ¶
type LineSource interface {
Lines() <-chan string
}
LineSource is implemented by an IO that also delivers the lines the human types between requests (console commands such as :review).
type Loop ¶
type Loop struct {
IO IO
// Handle serves one request (required).
Handle func(ctx context.Context, req ipc.Request) ipc.Response
// Command runs a line typed by the human between requests.
Command func(ctx context.Context, line string)
// Key handles a control key pressed between prompts; with a KeySource
// IO it also handles the keys pressed during a prompt.
Key func(Key)
// Tick runs every second (session timeouts).
Tick func()
// Poll runs every PolicyPoll (config file changes).
Poll func()
// Ended reports the end of the session (required); End ends it.
Ended func() bool
End func(reason string)
// Refresh runs after every event (the status row); Clear on return.
Refresh func()
Clear func()
// PeerAllowed decides which clients are served, from the kernel's
// credentials of the peer. Nil serves the console's own account only.
PeerAllowed func(ipc.Cred) bool
}
Loop is the console's request loop: it accepts clients, checks their peer credentials and serves their requests one at a time on one goroutine, between the lines and control keys the human types and the periodic ticks. A request whose client goes away has its context cancelled, which cancels a pending approval prompt.
func (*Loop) Serve ¶
Serve accepts clients on ln and serves their requests one at a time until ctx ends or the session ends (logout, timeouts, :quit). lines are the console commands typed by the human (nil for none); the control keys come from the IO when it is a KeySource. ln is closed on return, which removes the socket.
type Options ¶
type Options struct {
// Profile is the profile to serve; ProjectRoot the project whose
// config holds it ("" for the user config).
Profile string
ProjectRoot string
IO IO
// Auto, AllowUnmask and ShowResults are the start-up switches (all
// three refused at level 2; ShowResults refused at level 1 too).
Auto, AllowUnmask, ShowResults bool
// StateDir is the state base (kit.StateBase() when empty).
StateDir string
// TTY is the console's terminal device (nil when unknown).
TTY *os.File
// Now defaults to time.Now.
Now func() time.Time
// Quantum levels response times (Env.Level): 0 is ResponseQuantum, a
// negative value none.
Quantum time.Duration
// Sanitizer scrubs error text; nil is sanitize.New().
Sanitizer *sanitize.Sanitizer
// Isolation replaces the system the isolation checks read (tests and
// a product's test build); nil is RealIsolationEnv.
Isolation *IsolationEnv
// Harden replaces Harden (tests).
Harden func() error
// VaultRead replaces secrets.VaultRead (tests).
VaultRead func(ctx context.Context, path string) (*secrets.VaultCredentials, error)
// Keychain replaces the product's OS keychain (tests).
Keychain *secrets.Keychain
// Ready, when set, is told the socket path once the console listens.
Ready func(socket string)
// Hub, when enabled, decides requests on lockhub (HubOptions); --auto
// is refused with it.
Hub HubOptions
// OnHub, when set, is called with the console's hub once it is open,
// before any request is served: where a product registers its
// hubclient.Recompute (hub.Client.SetRecompute). It runs before the
// policy review, so Run may still fail and close the hub; Run sets the
// client's OnRequest itself (Hub.Requests), replacing any set here.
OnHub func(*Hub)
}
Options configure Run.
type Outcome ¶
type Outcome int
Outcome of an approval.
const ( Approved Outcome = iota // Denied is a typed answer that is not the expected one; Answer holds it. Denied // Unanswered: no answer within the timeout, or ctx ended. Unanswered RetypeRequested // NoFinger: desktop mode, typed yes, then no match, timeout or reader // error; Err holds it. NoFinger )
type PIIBackend ¶
type PIIBackend interface {
// PIIRules returns the mask and allow rules of p.
PIIRules(p Policy) (mask, allow []string)
// CheckMask validates a pattern sent by a client and returns its
// canonical form; an error is shown to the client (invalid params).
CheckMask(pattern string) (string, error)
// SaveMask adds the canonical pattern to the config file as it is on
// disk, so the human's unconfirmed edits are neither lost nor applied.
SaveMask(pattern string) error
// WithMask returns p with the canonical pattern added (canonical form,
// sorted lists); p itself is not changed.
WithMask(p Policy, pattern string) Policy
}
PIIBackend is implemented by a backend whose policy has mask rules: the console then serves pii.list and pii.add for it. A pattern's syntax (db.table.column, a JSON path, a header name) is the product's. Adding a mask rule only tightens the policy, so it applies at once.
type Pending ¶ added in v0.3.0
type Pending struct {
// Ticket is the id the agent presents to run the request once decided.
Ticket string
// Instance is the lock instance the ticket is on (hub mode).
Instance string
}
Pending is a request handed to an AsyncApprover.
type Policy ¶
Policy is a product's policy: the diff and description of config.Policy, plus the profile the console needs. Its JSON encoding is what the console fingerprints and stores as the approved policy, so it must be canonical (sorted lists, defaults applied) and decode back into the same type.
type Profile ¶
type Profile struct {
Name string
Production bool
// Where names the backend for the human (host:port, a path): it is
// shown escaped.
Where string
// Host, Port and DefaultPort name the OS keychain item of the secret
// ("<profile>@<host>:<port>").
Host string
Port, DefaultPort int
// User is the backend account; "" asks the human (or comes from Vault).
User string
// Credentials is config.CredentialsAsk, CredentialsKeychain,
// CredentialsVault or CredentialsNone; VaultPath is read for "vault".
Credentials string
VaultPath string
// CredentialsTTL asks for the secret again (and reconnects) once the
// connection is that old; 0 keeps it for the session.
CredentialsTTL time.Duration
// IdleTimeout ends the session after that long without activity,
// MaxSession after that long in all. 0 takes 20m and 4h.
IdleTimeout, MaxSession time.Duration
}
Profile is what the console needs to know of a profile.
type Screen ¶
type Screen struct {
// Header follows the profile name on the top line ("db / shop · tier read").
Header string
// Requester names the client; "" takes the peer behind the request.
Requester string
// Note is the agent's one-line reason (untrusted, escaped, audited).
Note string
// Body are the lines of the request; Warnings are shown in red.
Body []string
Warnings []string
// Production forces the production prompt; the profile's own flag
// always does.
Production bool
// NeverAuto is never approved by auto mode (an unmasked read, a
// destructive-looking operation): the human answers every time.
NeverAuto bool
// Red frames the screen in red outside production too.
Red bool
// Retype, when set, is an answer that asks to retype values; Approve
// then returns Approval.Retype.
Retype string
// Record, when set, turns the decision record into the product's own
// (with its fields); nil writes the audit.Base.
Record func(audit.Base) audit.Record
// Hub is the plan and masked screen for the hub, in hub mode
// (HubOf(env) not nil); a request without one is refused there.
Hub *HubTicket
}
Screen is the content of an approval screen, entirely supplied by the product. The console frames it (red on production), closes it with the prompt and keeps the rules: typed profile name on production, timeout, fingerprint at level 2, audit of the decision.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server serves client requests one at a time against one session. It is not safe for concurrent use: Serve runs every request on one goroutine.
func NewServer ¶
func NewServer(cfg ServerConfig) (*Server, error)
NewServer checks cfg and returns a server enforcing cfg.Policy.
func (*Server) Approve ¶
Approve implements Env: it frames sc, approves it in auto mode or asks the human, and audits the decision.
func (*Server) CheckPolicy ¶
func (s *Server) CheckPolicy()
CheckPolicy re-reads the policy from the config files. Changes that only tighten are applied at once; a loosening waits for :review.
func (*Server) Handle ¶
Handle serves one request. None of the console's methods loosens the policy.
func (*Server) PolicyPending ¶
PolicyPending reports a loosening awaiting :review: a product refuses requests meanwhile (ipc.CodePolicyPending) when its policy matters to them.
func (*Server) Status ¶
func (s *Server) Status() StatusResult
Status is the status method's answer.
type ServerConfig ¶
type ServerConfig struct {
Product kit.Product
Backend Backend
// Policy is the approved policy the server enforces.
Policy Policy
// StateDir and ApprovedKey locate the approved policy file ("" keeps
// adoptions in memory).
StateDir string
ApprovedKey string
Session Session
Audit *audit.Log
IO IO
// Approver takes every human decision; nil means typed approvals.
Approver Approver
// Level is the isolation level (2: every decision takes a finger, and
// a typed answer alone never widens anything).
Level int
// Boundary names the desktop boundary for the status screen.
Boundary string
Now func() time.Time
// Auto, AllowUnmask and ShowResults are the switches at start.
Auto, AllowUnmask, ShowResults bool
// Reconnect opens a new session (credentials, Connect, Checks) under
// the profile in force; nil means a lost connection ends the console.
Reconnect func(ctx context.Context, p Profile) (Session, error)
// Quantum levels response times (Env.Level).
Quantum time.Duration
// PeerAllowed decides which clients are served; nil: the console's own
// account.
PeerAllowed func(ipc.Cred) bool
// Sanitizer scrubs error text; nil is sanitize.New().
Sanitizer *sanitize.Sanitizer
// Refused is the fingerprint of a loosening the human refused at start,
// not announced again until the files change.
Refused string
// RefuseProduction re-applies the start-up refusals of a production
// profile (an X11 session, core dumps that could not be disabled) when
// the policy in force becomes production; nil refuses nothing.
RefuseProduction func() error
// Hub is the hub mode (Run with HubOptions); nil standalone.
Hub *Hub
}
ServerConfig holds what a Server needs once the start-up sequence is done. Run builds it; tests build it directly.
type Sized ¶
type Sized interface {
Width() int
}
Sized is implemented by an IO that knows its width in columns.
type StatusLine ¶
type StatusLine interface {
SetStatus(string)
}
StatusLine is implemented by an IO that keeps a status row on screen.
type StatusResult ¶
type StatusResult struct {
Profile string `json:"profile"`
Production bool `json:"production"`
AutoApprove bool `json:"auto_approve"`
AllowUnmask bool `json:"allow_unmask"`
ShowResults bool `json:"show_results"`
IdleTimeoutInS int `json:"idle_timeout_in_s"`
SessionEndsInS int `json:"session_ends_in_s"`
// Level is the security level: 0 session shape, 1 or 2 desktop shape.
Level int `json:"level"`
Product any `json:"product,omitempty"`
}
StatusResult answers the status method: the console's part, and the product's (Backend.Status) under "product".
type Terminal ¶
type Terminal struct {
// contains filtered or unexported fields
}
Terminal is the IO of a real console: prompts and messages go to out, answers and console commands are read from in, which must be the controlling terminal.
Interactive puts the terminal in cbreak mode: the Terminal does the line editing itself, so that it also sees the control keys (Shift+Tab, Ctrl-O) and can keep a status line on the bottom row, above which the output scrolls. One goroutine reads in from the first prompt on, so that the console can wait for a request and for a typed command at the same time. Before each prompt the kernel's pending input is flushed and the lines already read are dropped: type-ahead never answers a prompt.
func NewTerminal ¶
NewTerminal returns the IO over in and out (normally os.Stdin and os.Stdout).
func (*Terminal) AskSecret ¶
AskSecret reads a line with echo off. Before the reader goroutine runs it uses secrets.PromptPassword; in cbreak mode it stops echoing itself; otherwise it turns the kernel echo off on in.
func (*Terminal) Interactive ¶
Interactive puts the terminal in cbreak mode (no line discipline, no kernel echo; signals and output post-processing kept) and starts reading it. Restore undoes it. An input that is not a terminal (a test pipe) stays as it is: lines are read, keys and the status line are off.
func (*Terminal) Restore ¶
func (t *Terminal) Restore()
Restore ends the status line and puts the terminal back in the mode it had before Interactive.
func (*Terminal) SetKeyHandler ¶
SetKeyHandler sets the function that handles a control key pressed while a prompt waits. It runs on the goroutine that asked.
type Withdrawer ¶ added in v0.3.0
type Withdrawer interface {
Withdraw(ctx context.Context, d Decision, ticket, reason string) error
}
Withdrawer is an AsyncApprover that can take back a ticket it submitted: a pending request the console could not record is withdrawn.
type Withdrawn ¶ added in v0.4.0
type Withdrawn struct {
Instance string
// Tickets are the tickets withdrawn now.
Tickets []string
}
Withdrawn is what a standalone start withdrew from the hub mode of one instance.
func WithdrawStandalone ¶ added in v0.4.0
WithdrawStandalone is the rollback of hub mode (a console started without --hub after running with it): every pending plan of every hub instance under stateDir (as Options.StateDir) ends Withdrawn, marked for the hub to hear of at the next start in hub mode (hubclient.Client.Sync). An instance whose plans another console holds open (plans.ErrLocked: it runs in hub mode now) is left alone. Run calls it; a product with its own console lifecycle calls it at its standalone start.