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
- func NormalizeSSHUserCATrust(caKeys []string, principals map[string][]string) ([]string, map[string][]string, error)
- type AppliedException
- type Control
- type ControlSnapshot
- type Engine
- func (e Engine) ApplySSHHostCertificate(ctx context.Context, line string, hostNames []string) (SSHAccessResult, error)
- func (e Engine) ApplySSHUserCATrust(ctx context.Context, caKeys []string, principals map[string][]string) (SSHAccessResult, error)
- func (e Engine) EnsureMarketplaceRootSSH(ctx context.Context, request MarketplaceRootSSH) (MarketplaceRootSSHObservation, error)
- func (e Engine) HostNames(explicit []string) []string
- func (e Engine) LoadEvidence(workspaceRoot string) (Evidence, error)
- func (e Engine) RemoveMarketplaceRootSSH(ctx context.Context, owner string) error
- func (e Engine) Repair(ctx context.Context, opts RepairOptions) RepairReport
- func (e Engine) SaveEvidence(workspaceRoot string, evidence Evidence) (string, error)
- func (e Engine) ValidateHostCertificate(ctx context.Context, line string, hostNames []string) (string, 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) ReadDirNames(path string) ([]string, error)
- 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 MarketplaceRootSSH
- type MarketplaceRootSSHObservation
- type Mode
- type Options
- type Outcome
- type Output
- type Packet
- type Remediation
- type RemediationCapability
- type RepairOptions
- type RepairReport
- type RepairStep
- type SSHAccess
- type SSHAccessResult
- type SSHAccessState
- type Scope
- 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" // ControlSSHUserCA and ControlSSHHostCertificate judge the SSH certificate // access delivered to this node (owner user CA trust, host certificate). // They appear once node custody records SSH access, or when StackKits // SSH access files are found without such a record. ControlSSHUserCA = "ssh.user_ca_trust" ControlSSHHostCertificate = "ssh.host_certificate" )
Control identifiers. They are stable: Techstack keys findings on them.
const ( SSHAccessDir = "/etc/ssh/stackkit" SSHTrustedUserCAKeysPath = SSHAccessDir + "/trusted_user_ca_keys" SSHPrincipalsDir = SSHAccessDir + "/principals" SSHPrincipalsPattern = SSHPrincipalsDir + "/%u" SSHUserCADropIn = "/etc/ssh/sshd_config.d/01-stackkit-ssh-user-ca.conf" SSHHostCertificateDropIn = "/etc/ssh/sshd_config.d/01-stackkit-ssh-host-certificate.conf" )
StackKits-managed SSH certificate access. The user CA trust and the host certificate each have their own sshd drop-in, sorted before distribution drop-ins so their first-match settings win, and their own files under /etc/ssh. sshd reads the CA keys and principals files at every authentication, so emptying them revokes at once; only the drop-ins need a reload.
const ( ReasonSSHCleanupPending = "ssh_access_cleanup_pending" ReasonHostCertificateExpired = "ssh_host_certificate_expired" ReasonHostCertificateExpiring = "ssh_host_certificate_expiring" ReasonHostCertificateNotForKey = "ssh_host_certificate_key_mismatch" )
Reason codes of the SSH access controls.
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.
func NormalizeSSHUserCATrust ¶ added in v0.53.1
func NormalizeSSHUserCATrust(caKeys []string, principals map[string][]string) ([]string, map[string][]string, error)
NormalizeSSHUserCATrust validates CA keys and principals and returns their canonical form: comment-free authorized-key lines and sorted, de-duplicated principals. An empty key list is a revocation and carries no principals.
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) ApplySSHHostCertificate ¶ added in v0.53.1
func (e Engine) ApplySSHHostCertificate(ctx context.Context, line string, hostNames []string) (SSHAccessResult, error)
ApplySSHHostCertificate installs the host certificate beside the host key it certifies and points sshd at it. An empty line removes a StackKits-installed host certificate. A change sshd rejects or does not take is rolled back.
func (Engine) ApplySSHUserCATrust ¶ added in v0.53.1
func (e Engine) ApplySSHUserCATrust(ctx context.Context, caKeys []string, principals map[string][]string) (SSHAccessResult, error)
ApplySSHUserCATrust makes this server trust exactly the given user CA keys for exactly the given principals. Revocations take effect first, by emptying the files sshd reads at every authentication, and are never undone: a failed change is rolled back only to what both the previous and the new trust allow. A full revocation (no keys) that cannot reload sshd still holds and reports the pending cleanup.
func (Engine) EnsureMarketplaceRootSSH ¶ added in v0.53.1
func (e Engine) EnsureMarketplaceRootSSH(ctx context.Context, request MarketplaceRootSSH) (MarketplaceRootSSHObservation, error)
EnsureMarketplaceRootSSH uses the central host-security writer and validates sshd's effective connection policy before reloading. The dedicated key is never installed in the owner's ordinary authorized_keys file.
func (Engine) HostNames ¶ added in v0.53.1
HostNames returns the names a host certificate for this server must carry: the explicit names, or the system hostname.
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) RemoveMarketplaceRootSSH ¶ added in v0.53.1
RemoveMarketplaceRootSSH revokes only this installation's exception and key.
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.
func (Engine) ValidateHostCertificate ¶ added in v0.53.1
func (e Engine) ValidateHostCertificate(ctx context.Context, line string, hostNames []string) (string, string, error)
ValidateHostCertificate checks one host certificate for this server: an OpenSSH host certificate, signed, currently valid, with a finite lifetime, naming every host name as a principal and certifying one of sshd's configured host keys. It returns the canonical line and the host key path.
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 ¶
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 ¶
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.
func (Evidence) OverallFor ¶ added in v0.51.3
OverallFor returns the overall state of the evidence restricted to scope. Under ScopeEnforced only the enforced controls count: a control the evidence does not carry is unknown, never compliant, and an approved exception is not drift. Evidence that is no longer fresh has every control unknown (see AtTime), so it never reads as compliant under any scope.
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)
// ReadDirNames lists the entry names of a directory, sorted. A missing
// directory is an fs.ErrNotExist error.
ReadDirNames(path string) ([]string, 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 LocalHost ¶
type LocalHost struct{}
LocalHost is the real node.
func (LocalHost) ReadDirNames ¶ added in v0.53.1
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 MarketplaceRootSSH ¶ added in v0.53.1
MarketplaceRootSSH grants the application's own key access only from its observed Docker interface. It leaves the owner's global SSH policy intact.
type MarketplaceRootSSHObservation ¶ added in v0.53.1
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
// SSHAccess is the SSH certificate access recorded in node custody. nil
// leaves the SSH access controls out.
SSHAccess *SSHAccessState
}
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 SSHAccess ¶ added in v0.53.1
SSHAccess is the SSH certificate access StackKits holds on this server: the owner's user CA keys, the principals each local user accepts, and one host certificate. No CA keys means no CA trust (revoked or never delivered).
type SSHAccessResult ¶ added in v0.53.1
type SSHAccessResult struct {
Changes []string `json:"changes,omitempty"`
// CleanupPending names a revocation effect that could not be completed;
// the revocation itself already holds (the CA keys file is empty).
CleanupPending []string `json:"cleanupPending,omitempty"`
// HostCertificatePath is where the host certificate was installed.
HostCertificatePath string `json:"hostCertificatePath,omitempty"`
}
SSHAccessResult reports one SSH access change.
type SSHAccessState ¶ added in v0.53.1
type SSHAccessState struct {
Desired SSHAccess
// Recorded is false when custody never received SSH access; then the
// controls appear only if StackKits-managed files are found anyway.
Recorded bool
// CleanupPending names revocation effects not yet completed on the target.
CleanupPending []string
// Failure explains why custody could not be read; the controls are unknown.
Failure string
// HostNames are the names a host certificate must carry. Empty uses the
// system hostname.
HostNames []string
}
SSHAccessState is what node custody records for SSH access. Verify judges the host against it and repair reinstalls it.
type Scope ¶ added in v0.51.3
type Scope string
Scope selects which controls decide a verdict on the host.
const ( // ScopeAll judges every control, the continuous evidence included. ScopeAll Scope = "all" // ScopeEnforced judges only the controls the baseline itself enforces. The // continuous evidence is still observed and reported; it just does not // decide the verdict. ScopeEnforced Scope = "enforced" )
func ParseScope ¶ added in v0.51.3
ParseScope reads a scope name. The empty string is ScopeAll.
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" )