confedit

package
v0.54.2 Latest Latest
Warning

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

Go to latest
Published: Sep 6, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

Documentation

Overview

Package confedit changes one declared setting in one declared config file.

A fix that hardens SSH or switches on automatic updates has to edit a file, and no sudo rule can express "may write this one line of this one file". So the same pattern the agent already uses for reading /etc/shadow is used here: the grant names our own signed binary, and the list of what may be touched lives in typed Go where it can be read and tested.

The list is deliberately tiny. Every entry is a setting we are confident about on a server we have never seen, and nothing else can be reached — not a file path from the cloud, not a directive name, not a value outside the shape below.

Index

Constants

View Source
const BackupSuffix = ".ghostpsy-backup"

BackupSuffix is what a copy of an edited file is called.

ghostpsy no longer takes such a copy: it installs a file of its own beside the distribution's, so an undo is `rm` and there is nothing of this server's to keep. The name survives because the by-hand advice tells a person editing the main file themselves to take a copy first, and that copy needs a name to suggest.

Variables

This section is empty.

Functions

func AccessJSON

func AccessJSON() ([]byte, error)

AccessJSON is what the delegated read prints.

func ByHandCommands

func ByHandCommands(s Setting, value, content string) []string

ByHandCommands is the change written out for somebody to run themselves.

It was built to sit beside a change ghostpsy had just made, labelled "the same thing by hand". That reading is gone: ghostpsy now writes a file of its own rather than editing one somebody else owns, so there is nothing to be the equivalent of.

What is left is the case we refuse — a main configuration file that already sets this directive above its Include line, where a drop-in would be read and ignored. ghostpsy will not edit a line it did not write, so these commands are the answer to "then how do I do it", and they are the same shape as the advice a dangerous change carries: the risk, the check, and something to paste.

func DropInDirFor

func DropInDirFor(s Setting) string

DropInDirFor is where this setting's drop-in goes, for a message that names it.

Derived from the destination rather than decided again, so a message can never name a directory the file does not go into. Dest does not depend on the value: every value of a setting writes the same path, which is what makes the undo one `rm`.

func DropInSourceDir

func DropInSourceDir() string

DropInSourceDir is where the shipped files live, for a message that tells somebody where to look.

func DropInWins

func DropInWins(s Setting, mainConfig string) (bool, string)

DropInWins reports whether a drop-in for this setting would take effect, and says why not when it would not.

mainConfig is the content of the service's main configuration file.

func Effective

func Effective(s Setting, value, reported string) (string, bool)

Effective reports what the service says it is running with, and whether that counts as the value that was asked for.

The string is always what the service reported, whether it matched or not — on a match it can be the synonym, because OpenSSH prints `without-password` for `prohibit-password`. Callers use it to say what a failed check found.

It takes the text rather than running the command. Verify used to shell out to `sshd -T` from here, which meant a privileged command that appeared in no registry and in no grant file — nobody reading /etc/sudoers.d/ghostpsy would know it ran. Now the command is declared, the caller runs it through privexec, and the judgement happens on the output.

func ReadPasswd

func ReadPasswd() (string, error)

ReadPasswd returns /etc/passwd. It is world-readable on every Linux system, so this needs no privilege and must never be granted any.

func SSHConfigPath

func SSHConfigPath() string

SSHConfigPath is the main SSH configuration file, for a command that reads it.

func ServiceName

func ServiceName(s Setting) string

ServiceName is what to call the thing being checked, in words a person reads.

It exists so a message about a setting does not have to know which service is behind it. A check that wrote "the SSH server" itself could only ever be used for an SSH setting, which is how one half of the safety net became SSH-only.

func Value

func Value(s Setting, content string) (string, bool)

Value reads what the setting is currently set to, ignoring comments.

func WriteDropIns

func WriteDropIns(root string) ([]string, error)

WriteDropIns writes every shipped file this server can use, and returns the paths it wrote.

root is "" in production and a temporary directory in tests. It is a parameter rather than an environment variable because these paths also appear in the sudo grant, where they must always be the real ones — so the redirection has to be visible at the call site, not hidden inside a path helper.

The installer calls this as root. It is safe to call again on an upgrade: each file is written whole to a temporary name and renamed over the old one, which works even though the old one is read-only.

Types

type Access

type Access struct {
	// AccountsWithKeys is how many accounts have at least one SSH key.
	AccountsWithKeys int `json:"accounts_with_keys"`

	// NonRootAccountsWithKeys is how many of those are not root. Refusing root
	// logins outright is only safe when this is at least one.
	NonRootAccountsWithKeys int `json:"non_root_accounts_with_keys"`
}

Access is how many ways into this machine there are over SSH.

func AccessFromKeyFiles

func AccessFromKeyFiles(passwdContent string, keyFiles []string) Access

AccessFromKeyFiles counts the ways into this machine from a list of authorized_keys paths.

The split matters. /etc/passwd is world-readable, so the agent already knows every account, its home and its shell without any privilege at all. The only thing it cannot do unprivileged is look inside a home directory. So that one step is the only thing granted — `find … -name authorized_keys -size +0` — and the judgement happens here, as an ordinary user.

This replaced `ghostpsy read-ssh-access`, which did the whole job as root and told a reviewer nothing about what it read.

The grant searches /root and /home. An account whose home is somewhere else is not counted, and that is the safe direction to be wrong in: a key we miss makes this machine look like it has fewer ways in, so a hardening change is refused rather than allowed. Being refused costs a message; being allowed wrongly costs the server.

func ReadAccess

func ReadAccess() (Access, error)

ReadAccess counts the ways into this machine.

It needs root: an authorized_keys file lives in a home directory this agent cannot read. So it is reached the same way /etc/shadow is — through the agent's own signed binary, which returns only the counts.

func (Access) AllowsChange

func (a Access) AllowsChange(need WayIn) (bool, string)

AllowsChange reports whether this much access is enough for a setting to be safe, and says what is missing when it is not.

type Change

type Change struct {
	Setting Setting
	Value   string
}

Change is one setting at one value ghostpsy is allowed to write.

func Applicable

func Applicable(root string) []Change

Applicable is the changes this server can actually take.

A drop-in needs somewhere to go. rocky-9 has no /etc/apt/apt.conf.d, so shipping the two apt files there would leave them sitting unusable forever and make `ghostpsy dropins` offer a fix that cannot happen. The sudo grant follows the same rule one level down, through Command.NeedsPath.

root is "" in production and a temporary directory in tests.

func Changes

func Changes() []Change

Changes lists every setting-and-value pair, in a stable order.

A setting with no allowed values contributes nothing: it is explained, never written, and a grant line for it would claim a power we do not have.

func (Change) DropIn

func (c Change) DropIn() DropIn

DropIn is the file that makes this change, and the file that undoes it.

type DangerousChange

type DangerousChange struct {
	// Setting and Value name the change, in the same words the safe path uses.
	Setting string
	Value   string

	// Risk says what could go wrong, in plain words. Not a warning label — the
	// specific bad outcome, so the reader can tell whether it applies to them.
	Risk string

	// CheckFirst is what to confirm before running the commands. The one step
	// that turns a dangerous change into a safe one.
	CheckFirst string

	// Commands are what to run, ready to paste. Advice somebody has to translate
	// is advice they will get wrong.
	Commands []string
}

DangerousChange is a change ghostpsy will explain but never make.

func Dangerous

func Dangerous() []DangerousChange

Dangerous returns every declared dangerous change.

func (*DangerousChange) Error

func (d *DangerousChange) Error() string

Error makes a dangerous change a refusal a caller can simply pass on.

The words are the whole message: a person reading "ghostpsy will not do this" with nothing after it learns only that we are unhelpful.

func (*DangerousChange) Script

func (d *DangerousChange) Script() string

Script is the commands as one block, ready to copy.

type DropIn

type DropIn struct {
	// Source ships with the agent. Fixed content, owned by root.
	Source string

	// Dest is where it is installed to. One per setting, so an undo is `rm` of one
	// exact path.
	Dest string

	// Mode is passed to `install -m`, so it is the literal text of a command
	// argument and of the grant line.
	Mode string

	Content string
}

DropIn is the file that carries one setting at one value.

type Setting

type Setting struct {
	// Key is how the service names it. It never names a file path.
	Key string

	File      string
	Directive string
	Style     Style

	// Allow is every value accepted, written out one by one.
	//
	// It is a list rather than a pattern because three other things are generated
	// from it and a pattern cannot be enumerated: the sudo grant needs one line
	// per value, the shipped drop-in files need one file per value, and the app
	// needs the values it may offer. `^[3-6]$` cannot produce those; {"3","4",
	// "5","6"} can.
	//
	// Empty means there is no value of this setting ghostpsy sets itself. The entry
	// exists to explain the change, not to make it — see Dangerous.
	Allow []string

	// Dangerous is the values ghostpsy explains but never sets, by value.
	//
	// A change can be worth making and still be one we must not make: only the
	// person who knows the server can judge whether it survives. Leaving such a
	// change out entirely is not caution, it is unhelpfulness — they will do it
	// from memory instead. See danger.go.
	Dangerous map[string]DangerousChange

	// Units are the services that must be reloaded for a change to this file to
	// take effect, most specific name first.
	//
	// This is where the unit name comes from, instead of from the distribution.
	// internal/action/catalog.go used to pick `ssh` or `sshd` by looking for
	// /etc/debian_version or /etc/redhat-release — a guess about the machine when the
	// machine could simply be asked which unit it has.
	//
	// Empty is a real answer: apt re-reads apt.conf.d on every periodic run, so
	// nothing has to be reloaded, and naming a unit would grant a restart nobody needs.
	Units []string

	// NeedsAWayIn says what must still be true for this change to be safe.
	//
	// Closing a door is not automatically safe. Turning off password logins on a
	// server nobody has a key for locks everybody out, and the machine then looks
	// perfectly healthy from outside: sshd is up, the port accepts the connection,
	// and every login is refused. Found by locking myself out of a real machine.
	NeedsAWayIn WayIn

	// Why says in plain words what this setting does and why changing it is
	// safe. It reaches the approval screen.
	Why string
}

Setting is one thing this agent is allowed to change.

func All

func All() []Setting

All returns every declared setting, in a stable order, so `ghostpsy actions` can print the whole list of what this agent may change.

func Check

func Check(key, value string) (Setting, error)

Check returns the setting for key, if the value is one it accepts.

func Lookup

func Lookup(key string) (Setting, bool)

Lookup returns the declared setting, or false.

type Style

type Style string

Style is how a file writes its settings.

const (
	// StyleSSH is `Directive value`, with # for comments. sshd_config.
	StyleSSH Style = "ssh"

	// StyleAPTConf is `Name::Path "value";`. apt.conf.d files.
	StyleAPTConf Style = "apt.conf"
)

type WayIn

type WayIn string

WayIn is what has to remain possible after a change.

const (
	// WayInNothing means this setting cannot affect anybody's ability to log in.
	WayInNothing WayIn = ""

	// WayInAnyKey means at least one account must have an SSH key, or turning off
	// password logins leaves no way in at all.
	WayInAnyKey WayIn = "any_key"

	// WayInOtherAccountKey means some account *other than root* must have a key.
	// Needed before root logins are refused outright.
	WayInOtherAccountKey WayIn = "other_account_key"
)

Jump to

Keyboard shortcuts

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