Documentation
¶
Overview ¶
Package hostsecurity observes and repairs the StackKits host security baseline on one node and produces versioned, expiring evidence of it.
It is the node-side half of continuous host-security evidence: Techstack dispatches the operation through the pinned StackKits CLI, and a standalone (Standard Mode) operator runs the same commands without any account. Every host interaction goes through the Host interface so the decisions (what is compliant, what may be repaired without cutting the management path) are testable without root and on any operating system.
Index ¶
- Constants
- func EnforcedControls() []string
- func EvidencePath(workspaceRoot string) string
- type AppliedException
- type Control
- type ControlSnapshot
- type Engine
- func (e Engine) LoadEvidence(workspaceRoot string) (Evidence, error)
- func (e Engine) Repair(ctx context.Context, opts RepairOptions) RepairReport
- func (e Engine) SaveEvidence(workspaceRoot string, evidence Evidence) (string, error)
- func (e Engine) Verify(ctx context.Context, options Options) Evidence
- type Evidence
- type Exception
- type ExceptionRecord
- type ExceptionStatus
- type ExceptionsFile
- type FirewallPolicy
- type Host
- type Listener
- type LocalHost
- func (LocalHost) Getenv(key string) string
- func (LocalHost) Geteuid() int
- func (LocalHost) LookPath(name string) (string, error)
- func (LocalHost) MkdirAll(path string, mode os.FileMode) error
- func (LocalHost) Now() time.Time
- func (LocalHost) ReadFile(path string) ([]byte, error)
- func (LocalHost) Remove(path string) error
- func (LocalHost) Run(ctx context.Context, name string, args ...string) (Output, error)
- func (LocalHost) Sleep(ctx context.Context, d time.Duration) error
- func (LocalHost) Stat(path string) (fs.FileInfo, error)
- func (LocalHost) WriteFile(path string, data []byte, mode os.FileMode) error
- type ManagedSysctl
- type ManagementPath
- type Mode
- type Options
- type Outcome
- type Output
- type Packet
- type Remediation
- type RemediationCapability
- type RepairOptions
- type RepairReport
- type RepairStep
- type SiteKind
- type State
- type StepStatus
Constants ¶
const ( HomeFirewallTable = "stackkits_home_host_security" HomeFirewallChain = "stackkits_home_host_base" CloudFirewallTable = "stackkits_cloud_host_security" )
Names of the firewall tables. The home table is owned by this package; the cloud table is owned by the Cloud host-security executor, which this package only observes.
const ( // EvidenceSchemaVersion identifies the host security evidence document. EvidenceSchemaVersion = "stackkit.host-security-evidence/v1" // RepairSchemaVersion identifies the repair plan and record document. RepairSchemaVersion = "stackkit.host-security-repair/v1" // ExceptionsSchemaVersion identifies the owner-controlled exceptions file. ExceptionsSchemaVersion = "stackkit.host-security-exceptions/v1" // BaselineVersion is the version of the controls and expected values this // package verifies. Consumers compare it to know which baseline an // observation was judged against. BaselineVersion = "stackkit.host-security-baseline/1.0.0" )
const ( ControlFirewall = "firewall.default_inbound" ControlSSHPassword = "ssh.password_authentication" ControlSSHRootLogin = "ssh.root_login" ControlSSHPort = "ssh.port" ControlBruteForce = "bruteforce.fail2ban" ControlUnattended = "updates.unattended_upgrades" ControlPendingSecurity = "updates.pending_security" ControlRebootRequired = "updates.reboot_required" ControlSysctl = "kernel.sysctl" ControlListeners = "exposure.listeners" ControlPublishedPorts = "exposure.published_ports" ControlCertificateExpiry = "tls.certificate_expiry" )
Control identifiers. They are stable: Techstack keys findings on them.
const ( // DefaultFreshness is how long one observation may be relied on. DefaultFreshness = 15 * time.Minute MinFreshness = time.Minute MaxFreshness = 24 * time.Hour // StateDir holds what this package records on the host so a later // verification can tell drift from the last applied policy. StateDir = "/etc/stackkit/host-security" HomeFirewallPolicyPath = StateDir + "/home-firewall-policy.json" HomeFirewallRuleset = StateDir + "/home-firewall.nft" )
const DefaultExceptionsPath = "/etc/stackkit/host-security/exceptions.json"
DefaultExceptionsPath is the owner-controlled exceptions file. It is local to the host: no service writes it, and consumers of the evidence only read the exceptions it reports back.
const ReasonExposedPublishedPort = "exposed_published_port"
ReasonExposedPublishedPort marks a container-published port bound beyond loopback that is not a declared service. The host's input firewall does not filter such a port: Docker forwards it after DNAT, so it never reaches the input hook.
const ReasonNoAuthorizedKey = "no_authorized_key"
ReasonNoAuthorizedKey marks ssh.password_authentication drift that cannot be repaired yet because disabling password logins would lock the owner out: no account (or not the account in use) holds an authorized SSH key.
Variables ¶
This section is empty.
Functions ¶
func EnforcedControls ¶
func EnforcedControls() []string
EnforcedControls lists the control IDs the baseline enforces.
func EvidencePath ¶
EvidencePath is where local evidence for a workspace is written.
Types ¶
type AppliedException ¶
type AppliedException struct {
Owner string `json:"owner"`
Reason string `json:"reason"`
ExpiresAt time.Time `json:"expires_at"`
}
AppliedException names the owner decision that turned a drifted control into an approved exception.
type Control ¶
type Control struct {
ID string `json:"id"`
State State `json:"state"`
Expected string `json:"expected"`
Observed string `json:"observed"`
ObservedAt time.Time `json:"observed_at"`
Reason string `json:"reason,omitempty"`
ReasonCode string `json:"reason_code,omitempty"`
Remediation Remediation `json:"remediation"`
Exception *AppliedException `json:"exception,omitempty"`
}
Control is the judged state of one baseline control.
type ControlSnapshot ¶
ControlSnapshot is one control's state at a moment.
type Engine ¶
type Engine struct {
Host Host
}
Engine observes and repairs one node through Host.
func (Engine) LoadEvidence ¶
LoadEvidence reads evidence written by SaveEvidence. The result must be read through AtTime: an old observation is unknown, whatever it once said.
func (Engine) Repair ¶
func (e Engine) Repair(ctx context.Context, opts RepairOptions) RepairReport
Repair restores drifted controls. Without opts.Apply it only plans. It never changes a control it cannot observe, never changes one the owner approved an exception for, and never cuts the management path: a change that would is refused before it is made.
func (Engine) SaveEvidence ¶
SaveEvidence writes the evidence under the workspace's .stackkit directory for local use and returns the path. The file is owner-readable only: it describes the host's exposure.
type Evidence ¶
type Evidence struct {
SchemaVersion string `json:"schema_version"`
Kit string `json:"kit,omitempty"`
Mode Mode `json:"mode"`
SiteKind SiteKind `json:"site_kind"`
BaselineVersion string `json:"baseline_version"`
NodeRef string `json:"node_ref,omitempty"`
PlanHash string `json:"plan_hash,omitempty"`
ObservedAt time.Time `json:"observed_at"`
ExpiresAt time.Time `json:"expires_at"`
FreshnessSecs int `json:"freshness_budget_seconds"`
Overall State `json:"overall"`
Controls []Control `json:"controls"`
Exceptions []ExceptionRecord `json:"exceptions,omitempty"`
Notices []string `json:"notices,omitempty"`
}
Evidence is the stackkit.host-security-evidence/v1 document.
type Exception ¶
type Exception struct {
Control string `json:"control"`
Owner string `json:"owner"`
Reason string `json:"reason"`
ExpiresAt time.Time `json:"expires_at"`
}
Exception is one owner-approved deviation from a baseline control.
func LoadExceptions ¶
LoadExceptions reads the owner-controlled file. A missing file is no exceptions. A file another user could have edited is refused: an exception is an owner decision, so its provenance is part of its validity. The notices explain anything that was ignored.
type ExceptionRecord ¶
type ExceptionRecord struct {
Control string `json:"control"`
Owner string `json:"owner,omitempty"`
Reason string `json:"reason,omitempty"`
ExpiresAt time.Time `json:"expires_at,omitzero"`
Status ExceptionStatus `json:"status"`
Detail string `json:"detail,omitempty"`
}
ExceptionRecord reports one declared exception and whether it applied, so a deviation the owner approved stays visible.
type ExceptionStatus ¶
type ExceptionStatus string
ExceptionStatus says what became of one declared exception.
const ( ExceptionActive ExceptionStatus = "active" ExceptionUnused ExceptionStatus = "unused" ExceptionExpired ExceptionStatus = "expired" ExceptionInvalid ExceptionStatus = "invalid" )
type ExceptionsFile ¶
type ExceptionsFile struct {
SchemaVersion string `json:"schema_version"`
Exceptions []Exception `json:"exceptions"`
}
ExceptionsFile is the stackkit.host-security-exceptions/v1 document.
type FirewallPolicy ¶
type FirewallPolicy struct {
// SSHPorts are the ports sshd listens on. They are never closed to the
// management path.
SSHPorts []int
// Peers are the client addresses of SSH sessions in use when the policy was
// built; they keep reaching sshd after the policy loads.
Peers []netip.Addr
// ManagementSources are owner-configured networks that may always reach
// sshd (a jump host, an office network).
ManagementSources []netip.Prefix
// DeclaredOnly restricts LAN, overlay and container traffic to declared
// service ports. Without declared-service authority the policy trusts
// those sources on every port and drops everything else.
DeclaredOnly bool
// DeclaredTCP and DeclaredUDP are the declared service ports.
DeclaredTCP []int
DeclaredUDP []int
}
FirewallPolicy is the desired inbound policy of a home host. One model renders the nftables ruleset and answers Admits, so the text the host loads and the decision a test asks about cannot drift apart.
func (FirewallPolicy) Admits ¶
func (p FirewallPolicy) Admits(packet Packet) bool
Admits reports whether the policy lets the packet reach the host.
func (FirewallPolicy) Render ¶
func (p FirewallPolicy) Render() string
Render returns the nftables table text. It is one table with one input base chain whose policy is drop: anything no rule names is refused.
func (FirewallPolicy) RenderRuleset ¶
func (p FirewallPolicy) RenderRuleset() string
RenderRuleset is the file nft loads: the create/delete preamble makes the load idempotent, and nft applies the whole file as one transaction.
func (FirewallPolicy) Validate ¶
func (p FirewallPolicy) Validate() error
Validate refuses a policy that could lock the host away from its owner.
type Host ¶
type Host interface {
// LookPath resolves an executable like exec.LookPath.
LookPath(name string) (string, error)
// ReadFile reads a file like os.ReadFile.
ReadFile(path string) ([]byte, error)
// Stat reports file metadata like os.Stat.
Stat(path string) (fs.FileInfo, error)
// Run executes a command by name or absolute path. A nonzero exit is
// reported in Output.ExitCode with a nil error; the error is for a command
// that could not run or a context that ended.
Run(ctx context.Context, name string, args ...string) (Output, error)
// WriteFile atomically replaces a file with the given mode.
WriteFile(path string, data []byte, mode os.FileMode) error
// Remove deletes a file; a missing file is not an error.
Remove(path string) error
// MkdirAll creates a directory and its parents like os.MkdirAll.
MkdirAll(path string, mode os.FileMode) error
// Getenv reads one environment variable of the running process.
Getenv(key string) string
// Geteuid is the effective user ID of the running process.
Geteuid() int
// Now is the host clock.
Now() time.Time
// Sleep waits for d or until ctx ends.
Sleep(ctx context.Context, d time.Duration) error
}
Host is every interaction the baseline has with the node.
type ManagedSysctl ¶
ManagedSysctl is one kernel parameter the baseline manages. The baseline accepts a stricter value than it sets: Minimum is the lowest acceptable.
func ManagedSysctls ¶
func ManagedSysctls() []ManagedSysctl
ManagedSysctls lists the kernel parameters the baseline manages.
type ManagementPath ¶
type ManagementPath struct {
SSHPorts []int `json:"ssh_ports"`
SessionPeers []string `json:"session_peers,omitempty"`
ManagementSources []string `json:"management_sources,omitempty"`
SessionUser string `json:"session_user,omitempty"`
KeyAccounts []string `json:"key_accounts,omitempty"`
InSSHSession bool `json:"in_ssh_session"`
// SSHInstalled is false when the host has no sshd, in which case there is
// no ssh path to preserve.
SSHInstalled bool `json:"ssh_installed"`
// contains filtered or unexported fields
}
ManagementPath is everything a repair must not cut: the ports sshd listens on, the SSH sessions in use right now, the owner's configured management networks, and the accounts that hold a login key.
type Options ¶
type Options struct {
Kit string
Mode Mode
SiteKind SiteKind
NodeRef string
PlanHash string
// Freshness is the budget after which the evidence expires. Zero selects
// DefaultFreshness.
Freshness time.Duration
// DeclaredListeners is the declared-service authority. nil means no
// authority is available, which makes exposed listeners unknown rather
// than compliant.
DeclaredListeners []Listener
DeclaredAuthority string
// ManagementSources are owner-configured networks that always reach sshd.
ManagementSources []netip.Prefix
// ExceptionsPath is the owner-controlled exceptions file; empty means none.
ExceptionsPath string
// WorkspaceRoot locates custody material (certificates). Empty skips it.
WorkspaceRoot string
}
Options select what a verification judges the host against.
type Packet ¶
type Packet struct {
Source netip.Addr
Interface string
Protocol string // tcp, udp or icmp
Port int // destination port
SourcePort int
Established bool
Invalid bool
}
Packet is the minimal description of an inbound packet the firewall policy can be asked about. It is the test seam that lets a caller ask "does this policy still admit the management path" without parsing rule text.
type Remediation ¶
type Remediation struct {
Capability RemediationCapability `json:"capability"`
Action string `json:"action,omitempty"`
}
Remediation describes how a drifted control is restored.
type RemediationCapability ¶
type RemediationCapability string
RemediationCapability says who can restore a control.
const ( // RemediationAutomatic means `stackkit host security repair --apply` can // restore the control without cutting the management path. RemediationAutomatic RemediationCapability = "automatic" // RemediationManual means another StackKits command or the owner must act. RemediationManual RemediationCapability = "manual" // RemediationNone means nothing to restore. RemediationNone RemediationCapability = "none" )
type RepairOptions ¶
type RepairOptions struct {
Options
// Controls restricts the repair to the named controls. Empty repairs every
// drifted control that can be restored automatically.
Controls []string
// Apply carries the plan out. It is the only switch that changes the host.
Apply bool
}
RepairOptions select what to repair. Without Apply nothing is changed.
type RepairReport ¶
type RepairReport struct {
SchemaVersion string `json:"schema_version"`
Outcome Outcome `json:"outcome"`
Applied bool `json:"applied"`
StartedAt time.Time `json:"started_at"`
FinishedAt time.Time `json:"finished_at"`
Management ManagementPath `json:"management_path"`
Steps []RepairStep `json:"steps"`
Before Evidence `json:"before"`
After *Evidence `json:"after,omitempty"`
Firewall *FirewallPolicy `json:"firewall_policy,omitempty"`
}
RepairReport is the stackkit.host-security-repair/v1 document.
type RepairStep ¶
type RepairStep struct {
Control string `json:"control"`
Status StepStatus `json:"status"`
Summary string `json:"summary,omitempty"`
Changes []string `json:"changes,omitempty"`
Reason string `json:"reason,omitempty"`
// ReasonCode is a stable code for a refusal, for example no_authorized_key.
ReasonCode string `json:"reason_code,omitempty"`
Before *ControlSnapshot `json:"before,omitempty"`
After *ControlSnapshot `json:"after,omitempty"`
}
RepairStep reports one control: what would be, or was, done to it.
type State ¶
type State string
State is the outcome of judging one control or the whole host.
const ( // StateCompliant means the control was observed and matches the baseline. StateCompliant State = "compliant" // StateDrifted means the control was observed and differs from the baseline. StateDrifted State = "drifted" // StateUnknown means the control could not be observed or the observation // is no longer fresh. It is never compliant. StateUnknown State = "unknown" // StateException means the control is drifted but the owner approved the // deviation with an unexpired, attributed exception. StateException State = "exception" )
type StepStatus ¶
type StepStatus string
StepStatus is what became of one control in a repair.
const ( StepPlanned StepStatus = "planned" StepApplied StepStatus = "applied" StepNoop StepStatus = "noop" StepBlocked StepStatus = "blocked" StepManual StepStatus = "manual" StepFailed StepStatus = "failed" )