hostsecurity

package
v0.49.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

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

View Source
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.

View Source
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"
)
View Source
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.

View Source
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"
)
View Source
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.

View Source
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.

View Source
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

func EvidencePath(workspaceRoot string) string

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

type ControlSnapshot struct {
	State    State  `json:"state"`
	Observed string `json:"observed"`
}

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

func (e Engine) LoadEvidence(workspaceRoot string) (Evidence, error)

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

func (e Engine) SaveEvidence(workspaceRoot string, evidence Evidence) (string, error)

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.

func (Engine) Verify

func (e Engine) Verify(ctx context.Context, options Options) Evidence

Verify observes every control and returns the judged evidence. It changes nothing on the host. A control it cannot observe is unknown with a reason, never a failure and never compliant.

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.

func (Evidence) AtTime

func (e Evidence) AtTime(now time.Time) Evidence

AtTime returns the evidence as it must be read at the given time: once past its freshness budget every control becomes unknown, because a measurement that old proves nothing about the host now.

func (Evidence) Judge

func (e Evidence) Judge(now time.Time) State

Judge returns the overall state of the evidence at the given time. It never trusts the stored Overall field: it is recomputed from the controls, and evidence past its expiry is unknown regardless of what it once said.

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

func LoadExceptions(host Host, path string) (exceptions []Exception, notices []string)

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 Listener

type Listener struct {
	Transport string `json:"transport"`
	Port      int    `json:"port"`
}

Listener is one declared service listener.

type LocalHost

type LocalHost struct{}

LocalHost is the real node.

func (LocalHost) Getenv

func (LocalHost) Getenv(key string) string

func (LocalHost) Geteuid

func (LocalHost) Geteuid() int

func (LocalHost) LookPath

func (LocalHost) LookPath(name string) (string, error)

func (LocalHost) MkdirAll

func (LocalHost) MkdirAll(path string, mode os.FileMode) error

func (LocalHost) Now

func (LocalHost) Now() time.Time

func (LocalHost) ReadFile

func (LocalHost) ReadFile(path string) ([]byte, error)

func (LocalHost) Remove

func (LocalHost) Remove(path string) error

func (LocalHost) Run

func (LocalHost) Run(ctx context.Context, name string, args ...string) (Output, error)

func (LocalHost) Sleep

func (LocalHost) Sleep(ctx context.Context, d time.Duration) error

func (LocalHost) Stat

func (LocalHost) Stat(path string) (fs.FileInfo, error)

func (LocalHost) WriteFile

func (LocalHost) WriteFile(path string, data []byte, mode os.FileMode) error

type ManagedSysctl

type ManagedSysctl struct {
	Key     string
	Minimum int
}

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 Mode

type Mode string

Mode is the lifecycle mode the evidence was produced under.

const (
	// ModeStandard is the account-free standalone StackKits CLI.
	ModeStandard Mode = "standard"
	// ModeAdvanced is a deployment managed by Techstack through the pinned CLI.
	ModeAdvanced Mode = "advanced"
)

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 Outcome

type Outcome string

Outcome summarizes a whole repair.

const (
	OutcomeNothingToDo Outcome = "nothing_to_do"
	OutcomePlanned     Outcome = "planned"
	OutcomeApplied     Outcome = "applied"
	OutcomeManual      Outcome = "manual_required"
	OutcomeBlocked     Outcome = "blocked"
	OutcomeFailed      Outcome = "failed"
)

type Output

type Output struct {
	Stdout   string
	Stderr   string
	ExitCode int
}

Output is what a command printed and how it exited.

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 SiteKind

type SiteKind string

SiteKind selects which site-specific expectations apply.

const (
	SiteHome    SiteKind = "home"
	SiteCloud   SiteKind = "cloud"
	SiteUnknown SiteKind = "unknown"
)

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"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL