Technical reference

arachnopress

Build a zero-JS static article site from hand-written HTML fragments.

DESCRIPTION

arachnopress reads hand-written HTML articles below articles/ and builds a single-page or multi-page static site. The generated interface uses only HTML and CSS; it requires no JavaScript, cookies, or browser storage.

Generated output may be opened directly through file:// or served as static files. It requires no CGI, PHP, application server, or database.

The build uses sh, make, and standard Unix utilities on BSD, Linux, and macOS. Pygments and GNU Source-highlight are optional. A missing or failed highlighter falls back to escaped source.

FEATURES

  • Editable HTML fragments with arbitrary HTML where required; no Markdown or template pipeline.
  • HTML and CSS interface without JavaScript, cookies, or browser storage.
  • POSIX-style sh and make build for BSD, Linux, and macOS.
  • Single-page or per-article output, unlisted articles, and extensionless internal URLs.
  • Responsive CSS navigation, sortable article indexes, selectable themes, and automatic light/dark variants.
  • Pygments or Source-highlight output with escaped-source fallback and reduced span markup.
  • File-backed code, download, and image blocks with raw links, checksums, image sizing, framing, alignment, and text flow.
  • Article dates, custom branding, SVG favicons, native popovers, and a GET contact form for access-log collection.
  • Direct file:// access or static-file deployment without CGI, PHP, an application server, or a database.

DOWNLOADS

Downloadarachnopress_1.0.53.tar.gzgzipped source archive95 KiBSHA256 (arachnopress_1.0.53.tar.gz) = 58bf26d3cc332391816b5ab976e501d807505a76e5ff6ebc679ada5100cb590c

SYNOPSIS

Run make build from the project root. It builds every source article and replaces site/. Supply settings through the environment or as make command-line variables.

Maintainers run make release followed by make full to publish and integrate a generator release. Run every target from the project root. Do not store source files in or run make from site/; each successful target replaces it.

Routine buildRun from the project rootraw
cd /path/to/arachnopress
make build

# Override only the settings required for this build.
make build SITE_TITLE="Example Site" DEFAULT_THEME=solarized-light

# Use a fixed theme and omit the automatic mode control.
make build DEFAULT_THEME=solarized-light DEFAULT_THEME_AUTO=false

# Omit the Theme selector.
make build DEFAULT_THEME_SELECTOR=false

# Generate one HTML file per article.
make build SITE_MODE=multi-page

# Exclude unlisted articles.
make build SITE_MODE_UNLISTED=false

REQUIREMENTS

Required utilities

The build requires a POSIX-like operating environment with sh, make, awk, sed, grep, sort, tr, date, dirname, pwd, printf, mktemp, mkdir, rm, wc, cat, cksum, chmod, mv, cp, and find.

The release and full profiles also require cmp, tar, and gzip support in tar. mktemp is widely available but is not specified by POSIX.

Optional utilities

pygmentize
Provides Pygments syntax highlighting. This is the default highlighter when the command is available.
source-highlight
Provides GNU Source-highlight syntax highlighting when selected.
sha256, sha256sum, shasum, or openssl
Provides SHA256 values for download blocks. The first available implementation is used. Downloads remain usable without a checksum.

Browser features

The generated interface requires browser support only for HTML and CSS. It uses CSS :has(), :target, prefers-color-scheme, native details/summary, and the HTML popover API.

SOURCE LAYOUT

Maintained files

Makefile
Defines build, release, and full, their defaults, and their explicit inclusion lists.
styles.css
Defines layout, themes, syntax colours, responsive behaviour, popovers, and generated block presentation. Targets copy it into their fresh site output but do not modify it.
THIRD_PARTY_NOTICES.txt
Identifies the upstream colour schemes, sources, authors, and licences. The license popover and optional Theme popover link to this file.
licenses/
Contains the retained third-party copyright and licence notices.
tools/build.sh
Discovers, validates, orders, and renders articles. It generates the selected page layout, favicon.svg, automatic-theme rules, and embedded navigation rules in the selected build root.
tools/build-profile.sh
Stages, builds, publishes, cleans, and packages target output.
tools/theme-menu.html
Supplies theme controls and optional data-auto-dark and data-auto-light pair mappings. Its IDs are reserved as page anchors.
tools/license.txt
Supplies the authoritative static project-license text. Its first non-blank line is the displayed title.
tools/html-fragment.outlang
Configures Source-highlight to emit an HTML fragment.
articles/<slug>/article.html
Supplies one article. The directory name is the article slug and must match the article element ID.
articles/<slug>/*
Supplies article-local code, download, and image files. These paths are copied below site/articles/ and linked from the page.

The example tree shows maintained source and make build output. site/ contains deployed files and copied articles. The generator article is articles/arachnopress; GENERATOR_VERSION names release archives.

Project layout after make buildraw
.
|-- Makefile
|-- README.arachnopress
|-- THIRD_PARTY_NOTICES.txt
|-- articles
|   |-- arachnopress
|   |   |-- article.html
|   |   `-- ...
|   |-- irc
|   |   |-- article.html
|   |   `-- ...
|   `-- openbsd-rdsshd
|       |-- article.html
|       `-- ...
|-- licenses
|   |-- mit.txt
|   `-- oksolar-cc0.txt
|-- site
|   |-- THIRD_PARTY_NOTICES.txt
|   |-- articles
|   |   |-- arachnopress
|   |   |   |-- article.html
|   |   |   `-- ...
|   |   |-- irc
|   |   |   |-- article.html
|   |   |   `-- ...
|   |   `-- openbsd-rdsshd
|   |       |-- article.html
|   |       `-- ...
|   |-- favicon.svg
|   |-- index.html
|   |-- licenses
|   |   |-- mit.txt
|   |   `-- oksolar-cc0.txt
|   |-- styles.css
|   `-- theme-auto.css
|-- styles.css
`-- tools
    |-- build-profile.sh
    |-- build.sh
    |-- html-fragment.outlang
    |-- license.txt
    `-- theme-menu.html
Project layout after make build872 Braw

Source components

The Makefile defines the targets, profile defaults, and inclusion lists.

MakefileComplete build entry pointraw
.PHONY: build release full
.NOTPARALLEL:

# Site-profile defaults. Edit RELEASE_SITE_ENV for release, or PUBLIC_SITE_ENV
# for build and full. Environment and command-line make values take precedence
# without being expanded into shell source by make.
RELEASE_SITE_ENV = \
	SITE_TITLE="$${SITE_TITLE:-arachnopress}" \
	SITE_ICON="$${SITE_ICON:-䷖}" \
	SITE_ICON_COLOUR="$${SITE_ICON_COLOUR:-}" \
	SITE_ICON_PATH="$${SITE_ICON_PATH:-}" \
	SITE_ICON_PATH_THEME="$${SITE_ICON_PATH_THEME:-false}" \
	SITE_URL_STYLE="$${SITE_URL_STYLE:-html}" \
	SITE_MODE="$${SITE_MODE:-single-page}" \
	SITE_MODE_UNLISTED="$${SITE_MODE_UNLISTED:-false}" \
	DEFAULT_THEME="$${DEFAULT_THEME:-solarized-dark}" \
	DEFAULT_THEME_AUTO="$${DEFAULT_THEME_AUTO:-false}" \
	DEFAULT_THEME_SELECTOR="$${DEFAULT_THEME_SELECTOR:-true}"

PUBLIC_SITE_ENV = \
	SITE_TITLE="$${SITE_TITLE:-arachnogoat}" \
	SITE_ICON="$${SITE_ICON:-䷪}" \
	SITE_ICON_COLOUR="$${SITE_ICON_COLOUR:-}" \
	SITE_ICON_PATH="$${SITE_ICON_PATH-articles/arachnopress/arachnogoatsundual.svg}" \
	SITE_ICON_PATH_THEME="$${SITE_ICON_PATH_THEME:-true}" \
	SITE_URL_STYLE="$${SITE_URL_STYLE:-extensionless}" \
	SITE_MODE="$${SITE_MODE:-single-page}" \
	SITE_MODE_UNLISTED="$${SITE_MODE_UNLISTED:-true}" \
	DEFAULT_THEME="$${DEFAULT_THEME:-everforest-hard-dark}" \
	DEFAULT_THEME_AUTO="$${DEFAULT_THEME_AUTO:-true}" \
	DEFAULT_THEME_SELECTOR="$${DEFAULT_THEME_SELECTOR:-true}"

BUILD_ENV = \
	GENERATOR_LABEL="$${GENERATOR_LABEL:-arachnopress}" \
	GENERATOR_VERSION="$${GENERATOR_VERSION:-1.0.53}" \
	ARTICLE_ORDER="$${ARTICLE_ORDER:-title}" \
	HIGHLIGHTER="$${HIGHLIGHTER:-pygments}" \
	CODE_FOOTER_LINES="$${CODE_FOOTER_LINES:-23}"

RUNTIME_FILES = \
	THIRD_PARTY_NOTICES.txt \
	styles.css \
	licenses

RELEASE_FILES = \
	Makefile \
	README.arachnopress \
	$(RUNTIME_FILES) \
	tools/build.sh \
	tools/build-profile.sh \
	tools/html-fragment.outlang \
	tools/license.txt \
	tools/theme-menu.html

build:
	$(PUBLIC_SITE_ENV) $(BUILD_ENV) \
		sh tools/build-profile.sh build $(RUNTIME_FILES) articles

release:
	$(RELEASE_SITE_ENV) $(BUILD_ENV) \
		sh tools/build-profile.sh release \
		$(RELEASE_FILES) articles/arachnopress

full:
	$(PUBLIC_SITE_ENV) $(BUILD_ENV) \
		sh tools/build-profile.sh full $(RELEASE_FILES) articles

BUILD MODEL

Discovery and validation

Each target copies its Makefile inclusion list to a private .site-build.* tree in the project root. Build and full select every staged article; release selects articles/arachnopress. Inclusion paths must exist and contain no symbolic links.

tools/build.sh validates and renders the staged tree. Its transient files remain below TMPDIR, or /tmp when TMPDIR is unset.

Publication and ownership

The target completes the staged output before replacing site/. A failure before publication preserves the previous site/.

Exit and handled signal traps remove unpublished staging and rendering files. An untrappable termination may leave them behind.

Release and full prepare the generator-release marker and archive cleanup in staging and update the source only after publication succeeds. All targets share site/ and must not run concurrently.

ARTICLE FORMAT

Root metadata

Each article begins with a one-line article element. Its class list must include article. Its id must equal the containing directory name. data-title is required.

Optional data-created and data-modified values begin with YYYY-MM-DD. data-created defaults to the current UTC build date; data-modified defaults to data-created. Set both on published articles to keep visible metadata and date ordering stable.

data-listed defaults to true and accepts only true or false. SITE_MODE_UNLISTED=false excludes articles marked data-listed=false from generated HTML and navigation in either output mode. SITE_MODE_UNLISTED=true enables unlisted output.

Enabled single-page output embeds unlisted articles after listed articles, omits them from article indexes, and exposes them through their article, section, and subsection fragments. Enabled multi-page output writes an unlisted article as slug.html, omits it from listed-page indexes, and includes it only in its own index.

A multi-page unlisted document includes a noindex, nofollow directive. A single-page article shares index.html and cannot have a separate robots directive. Unlisted output remains publicly accessible. At least one listed article is required. Multi-page mode reserves the index slug. Targets still copy included article directories to site/articles/.

articles/getting-started/article.htmlComplete example articleraw
<article class="article" id="getting-started" data-title="Getting Started" data-created="2026-07-09" data-modified="2026-07-09">
  <header class="article-header">
    <p class="kicker">Guide</p>
    <h1>Getting Started</h1>
    <p class="summary">
      A short article built from one editable HTML fragment.
    </p>
  </header>

  <section>
    <h2>Example</h2>
    <p>Article text is normal HTML.</p>
    <h3>Subsection</h3>
    <p>Plain one-line h3 headings appear under their preceding section.</p>
  </section>
</article>

Header metadata

The source header contains an h1 and summary. The build inserts Created and a time element before its closing tag. It adds Updated when the dates differ and prefixes unlisted articles with Unlisted. The order is Unlisted, Created, Updated, using one separator.

Indexed headings

Use h2 for major sections and h3 for genuine subdivisions. Indexable headings contain plain text and close on the same source line. An h3 must follow an h2. The build preserves a valid explicit ID or derives a unique ID from the heading and article.

Article, heading, theme, generated control, and generated block IDs share one collision registry. Explicit IDs and slugs may contain ASCII letters, digits, dots, underscores, and hyphens.

Indexed heading formsraw
<h2>BUILD</h2>
<h3>Requirements</h3>
<h3 id="custom-subsection">Explicit Subsection Anchor</h3>
<h2 id="custom-anchor">Explicit Anchor</h2>

GENERATED BLOCKS

Generated markers are empty, file-backed elements contained on one source line. data-src is relative to the article directory. Missing or invalid sources render visible missing blocks.

Code blocks

data-title changes the display title; data-note adds a qualifier; and data-open values 0, false, no, closed, and collapsed close the block initially. Other values leave it open. data-header="false" emits always-visible content without a summary. A headerless block has no footer unless data-footer="true" is explicit.

For normal blocks, an omitted data-footer hides the footer when the source line count is at or below CODE_FOOTER_LINES. Explicit true or false values override that threshold. Raw links and sizes refer to the source file, whose contents are HTML-escaped.

Code block markersKeep each marker on one lineraw
<pre class="code-block" data-src="src/example.c" data-title="example.c" data-note="A short C example" data-lang="c"></pre>
<pre class="code-block" data-src="build.sh" data-lang="sh" data-open="false" data-footer="false"></pre>
<pre class="code-block" data-src="output.txt" data-lang="text" data-header="false"></pre>
<pre class="code-block" data-src="output.txt" data-title="output.txt" data-lang="text" data-header="false" data-footer="true"></pre>

Download blocks

A download marker names exactly one file. data-title changes only its display title and data-note adds a qualifier. The link continues to target data-src. The block shows the raw size and, when a supported checksum program is available, its SHA256. The obsolete data-downloads attribute is rejected; use one data-src marker per file.

The generator article has exactly one marker carrying data-release="generator". The release and full targets replace that complete line with the validated current archive name; a normal build leaves it unchanged.

Download block markersOne source file per markerraw
<div class="article-downloads" data-src="release.tar.gz" data-title="release.tar.gz" data-note="Source archive"></div>

Image blocks

data-alt supplies alternative text. data-caption adds a caption; data-title and data-note label the header. data-open, data-header, and data-footer behave as for code blocks.

data-scale accepts small, medium, large, or full and caps inline size at 20rem, 32rem, 48rem, or the article width. full is the default. Images retain their aspect ratio, do not upscale, and remain within the viewport.

data-border accepts full, fit, or none. full is the default. fit shrink-wraps the frame. none removes it and suppresses the header and footer.

data-align accepts start, center, or end; center is the default.

Above 760px, following prose and text lists may flow beside a start- or end-aligned fit or borderless block. Such a block uses at most 55% of the article measure.

Headings, generated blocks, preformatted blocks, tables, horizontal rules, consecutive images, and section ends clear the flow. Centered, full-frame, and narrower layouts keep images on their own row. Other scale, border, and alignment values are fatal. Generated images use lazy loading and asynchronous decoding.

Image block markersraw
<figure class="image-block" data-src="images/diagram.png" data-title="Diagram" data-note="Article-local image path" data-alt="Build flow diagram" data-caption="Article-local image"></figure>
<figure class="image-block" data-src="images/icon.png" data-title="Icon" data-alt="Small icon" data-scale="small" data-border="fit" data-align="start" data-header="false" data-footer="true"></figure>
<figure class="image-block" data-src="images/plain.png" data-alt="Borderless image" data-scale="small" data-border="none" data-align="end"></figure>

Missing sources

A missing or invalid source renders a missing block. A non-empty or multiline marker is fatal.

SYNTAX HIGHLIGHTING

Pygments

Pygments is the default. An omitted data-lang requests filename inference. data-lang="auto" first uses filename inference and may guess from content. An explicit value requests that lexer.

Source-highlight

Source-highlight uses tools/html-fragment.outlang. An omitted or automatic language lets the program infer its input language. The make alias is translated to makefile.

Fallback and optimisation

A missing program, unknown lexer, failed invocation, missing output definition, or empty highlighted result for non-empty input falls back to escaped raw source for that block. HIGHLIGHTER=none always uses that path.

Successful highlighted output is filtered to unwrap whitespace-only Pygments span.w and Source-highlight span.sh-normal elements. Their whitespace remains in the preformatted code content.

NAVIGATION AND THEMES

Article and section selection

The build writes title, creation-date, and modification-date order lists. ARTICLE_ORDER selects the initial list; CSS radio controls select another.

In single-page mode, URL fragments and CSS :target/:has() select articles, sections, and subsections. The first listed article is visible without a target. index.html embeds the generated navigation rules.

In multi-page mode, index.html contains the first listed article and each article has slug.html. Each file contains one article, its section index, and its generated navigation rules.

Title order is case-insensitive by title, then slug. Creation and modification orders are newest first by the corresponding source value, then slug.

Responsive index

Wide landscape layouts place article and section navigation in a vertical left pane. Narrow portrait layouts use independent horizontal article, section, and conditional subsection rows. Short and narrow layouts hide descriptive build metadata before hiding the generator identity, Contact, or license controls.

Theme state

DEFAULT_THEME selects the initial exact variant. DEFAULT_THEME_AUTO adds an Exact, Auto, Light, and Dark header control, beside the Theme button when present, and initially selects Auto. Exact applies the selected theme unchanged. The other modes use its light/dark mapping; Auto follows the browser preference. An unmapped theme remains fixed.

Both controls have equal height. The mode selector has no frame or selected background; its checked symbol uses the active heading colour.

DEFAULT_THEME_SELECTOR enables the selector from tools/theme-menu.html. Single-page selections span all articles until reload. Multi-page selections reset when another page loads. The mode control is independent and appears only when DEFAULT_THEME_AUTO is true.

tools/theme-menu.htmlComplete theme menuraw
      <div class="theme-menu" id="theme-popover" popover>
        <div class="theme-head">
          <h2>Theme</h2>
          <button class="theme-close" type="button" popovertarget="theme-popover" popovertargetaction="hide" aria-label="Close theme menu">X</button>
        </div>
        <form class="theme-list" aria-label="Theme">
          <fieldset>
            <legend>Solarized</legend>
            <input type="radio" name="theme" id="theme-solarized-dark" data-auto-dark="theme-solarized-dark" data-auto-light="theme-solarized-light" checked>
            <label for="theme-solarized-dark">Dark</label>
            <input type="radio" name="theme" id="theme-solarized-light" data-auto-dark="theme-solarized-dark" data-auto-light="theme-solarized-light">
            <label for="theme-solarized-light">Light</label>
          </fieldset>
          <fieldset>
            <legend>Selenized</legend>
            <input type="radio" name="theme" id="theme-selenized-dark" data-auto-dark="theme-selenized-dark" data-auto-light="theme-selenized-light">
            <label for="theme-selenized-dark">Dark</label>
            <input type="radio" name="theme" id="theme-selenized-black" data-auto-dark="theme-selenized-black" data-auto-light="theme-selenized-white">
            <label for="theme-selenized-black">Black</label>
            <input type="radio" name="theme" id="theme-selenized-light" data-auto-dark="theme-selenized-dark" data-auto-light="theme-selenized-light">
            <label for="theme-selenized-light">Light</label>
            <input type="radio" name="theme" id="theme-selenized-white" data-auto-dark="theme-selenized-black" data-auto-light="theme-selenized-white">
            <label for="theme-selenized-white">White</label>
          </fieldset>
          <fieldset>
            <legend>OKSolar</legend>
            <input type="radio" name="theme" id="theme-oksolar-dark" data-auto-dark="theme-oksolar-dark" data-auto-light="theme-oksolar-light">
            <label for="theme-oksolar-dark">Dark</label>
            <input type="radio" name="theme" id="theme-oksolar-light" data-auto-dark="theme-oksolar-dark" data-auto-light="theme-oksolar-light">
            <label for="theme-oksolar-light">Light</label>
          </fieldset>
          <fieldset>
            <legend>Gruvbox Material</legend>
            <input type="radio" name="theme" id="theme-gruvbox-material-hard-dark" data-auto-dark="theme-gruvbox-material-hard-dark" data-auto-light="theme-gruvbox-material-hard-light">
            <label for="theme-gruvbox-material-hard-dark">Hard Dark</label>
            <input type="radio" name="theme" id="theme-gruvbox-material-hard-light" data-auto-dark="theme-gruvbox-material-hard-dark" data-auto-light="theme-gruvbox-material-hard-light">
            <label for="theme-gruvbox-material-hard-light">Hard Light</label>
            <input type="radio" name="theme" id="theme-gruvbox-material-medium-dark" data-auto-dark="theme-gruvbox-material-medium-dark" data-auto-light="theme-gruvbox-material-medium-light">
            <label for="theme-gruvbox-material-medium-dark">Medium Dark</label>
            <input type="radio" name="theme" id="theme-gruvbox-material-medium-light" data-auto-dark="theme-gruvbox-material-medium-dark" data-auto-light="theme-gruvbox-material-medium-light">
            <label for="theme-gruvbox-material-medium-light">Medium Light</label>
            <input type="radio" name="theme" id="theme-gruvbox-material-soft-dark" data-auto-dark="theme-gruvbox-material-soft-dark" data-auto-light="theme-gruvbox-material-soft-light">
            <label for="theme-gruvbox-material-soft-dark">Soft Dark</label>
            <input type="radio" name="theme" id="theme-gruvbox-material-soft-light" data-auto-dark="theme-gruvbox-material-soft-dark" data-auto-light="theme-gruvbox-material-soft-light">
            <label for="theme-gruvbox-material-soft-light">Soft Light</label>
          </fieldset>
          <fieldset>
            <legend>Gruvbox Mix</legend>
            <input type="radio" name="theme" id="theme-gruvbox-mix-hard-dark" data-auto-dark="theme-gruvbox-mix-hard-dark" data-auto-light="theme-gruvbox-mix-hard-light">
            <label for="theme-gruvbox-mix-hard-dark">Hard Dark</label>
            <input type="radio" name="theme" id="theme-gruvbox-mix-hard-light" data-auto-dark="theme-gruvbox-mix-hard-dark" data-auto-light="theme-gruvbox-mix-hard-light">
            <label for="theme-gruvbox-mix-hard-light">Hard Light</label>
            <input type="radio" name="theme" id="theme-gruvbox-mix-medium-dark" data-auto-dark="theme-gruvbox-mix-medium-dark" data-auto-light="theme-gruvbox-mix-medium-light">
            <label for="theme-gruvbox-mix-medium-dark">Medium Dark</label>
            <input type="radio" name="theme" id="theme-gruvbox-mix-medium-light" data-auto-dark="theme-gruvbox-mix-medium-dark" data-auto-light="theme-gruvbox-mix-medium-light">
            <label for="theme-gruvbox-mix-medium-light">Medium Light</label>
            <input type="radio" name="theme" id="theme-gruvbox-mix-soft-dark" data-auto-dark="theme-gruvbox-mix-soft-dark" data-auto-light="theme-gruvbox-mix-soft-light">
            <label for="theme-gruvbox-mix-soft-dark">Soft Dark</label>
            <input type="radio" name="theme" id="theme-gruvbox-mix-soft-light" data-auto-dark="theme-gruvbox-mix-soft-dark" data-auto-light="theme-gruvbox-mix-soft-light">
            <label for="theme-gruvbox-mix-soft-light">Soft Light</label>
          </fieldset>
          <fieldset>
            <legend>Gruvbox Original</legend>
            <input type="radio" name="theme" id="theme-gruvbox-original-hard-dark" data-auto-dark="theme-gruvbox-original-hard-dark" data-auto-light="theme-gruvbox-original-hard-light">
            <label for="theme-gruvbox-original-hard-dark">Hard Dark</label>
            <input type="radio" name="theme" id="theme-gruvbox-original-hard-light" data-auto-dark="theme-gruvbox-original-hard-dark" data-auto-light="theme-gruvbox-original-hard-light">
            <label for="theme-gruvbox-original-hard-light">Hard Light</label>
            <input type="radio" name="theme" id="theme-gruvbox-original-medium-dark" data-auto-dark="theme-gruvbox-original-medium-dark" data-auto-light="theme-gruvbox-original-medium-light">
            <label for="theme-gruvbox-original-medium-dark">Medium Dark</label>
            <input type="radio" name="theme" id="theme-gruvbox-original-medium-light" data-auto-dark="theme-gruvbox-original-medium-dark" data-auto-light="theme-gruvbox-original-medium-light">
            <label for="theme-gruvbox-original-medium-light">Medium Light</label>
            <input type="radio" name="theme" id="theme-gruvbox-original-soft-dark" data-auto-dark="theme-gruvbox-original-soft-dark" data-auto-light="theme-gruvbox-original-soft-light">
            <label for="theme-gruvbox-original-soft-dark">Soft Dark</label>
            <input type="radio" name="theme" id="theme-gruvbox-original-soft-light" data-auto-dark="theme-gruvbox-original-soft-dark" data-auto-light="theme-gruvbox-original-soft-light">
            <label for="theme-gruvbox-original-soft-light">Soft Light</label>
          </fieldset>
          <fieldset>
            <legend>Tomorrow</legend>
            <input type="radio" name="theme" id="theme-tomorrow" data-auto-dark="theme-tomorrow-night" data-auto-light="theme-tomorrow">
            <label for="theme-tomorrow">Light</label>
            <input type="radio" name="theme" id="theme-tomorrow-night" data-auto-dark="theme-tomorrow-night" data-auto-light="theme-tomorrow">
            <label for="theme-tomorrow-night">Night</label>
            <input type="radio" name="theme" id="theme-tomorrow-eighties" data-auto-dark="theme-tomorrow-eighties" data-auto-light="theme-tomorrow">
            <label for="theme-tomorrow-eighties">Eighties</label>
            <input type="radio" name="theme" id="theme-tomorrow-blue" data-auto-dark="theme-tomorrow-blue" data-auto-light="theme-tomorrow">
            <label for="theme-tomorrow-blue">Blue</label>
            <input type="radio" name="theme" id="theme-tomorrow-bright" data-auto-dark="theme-tomorrow-bright" data-auto-light="theme-tomorrow">
            <label for="theme-tomorrow-bright">Bright</label>
          </fieldset>
          <fieldset>
            <legend>Nord</legend>
            <input type="radio" name="theme" id="theme-nord">
            <label for="theme-nord">Dark</label>
          </fieldset>
          <fieldset>
            <legend>Everforest</legend>
            <input type="radio" name="theme" id="theme-everforest-hard-dark" data-auto-dark="theme-everforest-hard-dark" data-auto-light="theme-everforest-hard-light">
            <label for="theme-everforest-hard-dark">Hard Dark</label>
            <input type="radio" name="theme" id="theme-everforest-hard-light" data-auto-dark="theme-everforest-hard-dark" data-auto-light="theme-everforest-hard-light">
            <label for="theme-everforest-hard-light">Hard Light</label>
            <input type="radio" name="theme" id="theme-everforest-dark" data-auto-dark="theme-everforest-dark" data-auto-light="theme-everforest-light">
            <label for="theme-everforest-dark">Medium Dark</label>
            <input type="radio" name="theme" id="theme-everforest-light" data-auto-dark="theme-everforest-dark" data-auto-light="theme-everforest-light">
            <label for="theme-everforest-light">Medium Light</label>
            <input type="radio" name="theme" id="theme-everforest-soft-dark" data-auto-dark="theme-everforest-soft-dark" data-auto-light="theme-everforest-soft-light">
            <label for="theme-everforest-soft-dark">Soft Dark</label>
            <input type="radio" name="theme" id="theme-everforest-soft-light" data-auto-dark="theme-everforest-soft-dark" data-auto-light="theme-everforest-soft-light">
            <label for="theme-everforest-soft-light">Soft Light</label>
          </fieldset>
          <fieldset>
            <legend>Edge</legend>
            <input type="radio" name="theme" id="theme-edge-dark" data-auto-dark="theme-edge-dark" data-auto-light="theme-edge-light">
            <label for="theme-edge-dark">Dark</label>
            <input type="radio" name="theme" id="theme-edge-aura" data-auto-dark="theme-edge-aura" data-auto-light="theme-edge-light">
            <label for="theme-edge-aura">Aura</label>
            <input type="radio" name="theme" id="theme-edge-neon" data-auto-dark="theme-edge-neon" data-auto-light="theme-edge-light">
            <label for="theme-edge-neon">Neon</label>
            <input type="radio" name="theme" id="theme-edge-aura-dim" data-auto-dark="theme-edge-aura-dim" data-auto-light="theme-edge-light">
            <label for="theme-edge-aura-dim">Aura Dim</label>
            <input type="radio" name="theme" id="theme-edge-light" data-auto-dark="theme-edge-dark" data-auto-light="theme-edge-light">
            <label for="theme-edge-light">Light</label>
          </fieldset>
          <fieldset>
            <legend>Sonokai</legend>
            <input type="radio" name="theme" id="theme-sonokai">
            <label for="theme-sonokai">Default</label>
            <input type="radio" name="theme" id="theme-sonokai-atlantis">
            <label for="theme-sonokai-atlantis">Atlantis</label>
            <input type="radio" name="theme" id="theme-sonokai-andromeda">
            <label for="theme-sonokai-andromeda">Andromeda</label>
            <input type="radio" name="theme" id="theme-sonokai-shusia">
            <label for="theme-sonokai-shusia">Shusia</label>
            <input type="radio" name="theme" id="theme-sonokai-maia">
            <label for="theme-sonokai-maia">Maia</label>
            <input type="radio" name="theme" id="theme-sonokai-espresso">
            <label for="theme-sonokai-espresso">Espresso</label>
          </fieldset>
          <fieldset>
            <legend>Spring Night</legend>
            <input type="radio" name="theme" id="theme-spring-night">
            <label for="theme-spring-night">Normal</label>
            <input type="radio" name="theme" id="theme-spring-night-high-contrast">
            <label for="theme-spring-night-high-contrast">High Contrast</label>
          </fieldset>
          <fieldset>
            <legend>Ayu</legend>
            <input type="radio" name="theme" id="theme-ayu-dark" data-auto-dark="theme-ayu-dark" data-auto-light="theme-ayu-light">
            <label for="theme-ayu-dark">Dark</label>
            <input type="radio" name="theme" id="theme-ayu-mirage" data-auto-dark="theme-ayu-mirage" data-auto-light="theme-ayu-light">
            <label for="theme-ayu-mirage">Mirage</label>
            <input type="radio" name="theme" id="theme-ayu-light" data-auto-dark="theme-ayu-dark" data-auto-light="theme-ayu-light">
            <label for="theme-ayu-light">Light</label>
          </fieldset>
          <fieldset>
            <legend>Catppuccin</legend>
            <input type="radio" name="theme" id="theme-catppuccin-latte" data-auto-dark="theme-catppuccin-mocha" data-auto-light="theme-catppuccin-latte">
            <label for="theme-catppuccin-latte">Latte</label>
            <input type="radio" name="theme" id="theme-catppuccin-frappe" data-auto-dark="theme-catppuccin-frappe" data-auto-light="theme-catppuccin-latte">
            <label for="theme-catppuccin-frappe">Frappe</label>
            <input type="radio" name="theme" id="theme-catppuccin-macchiato" data-auto-dark="theme-catppuccin-macchiato" data-auto-light="theme-catppuccin-latte">
            <label for="theme-catppuccin-macchiato">Macchiato</label>
            <input type="radio" name="theme" id="theme-catppuccin-mocha" data-auto-dark="theme-catppuccin-mocha" data-auto-light="theme-catppuccin-latte">
            <label for="theme-catppuccin-mocha">Mocha</label>
          </fieldset>
          <fieldset>
            <legend>Rose Pine</legend>
            <input type="radio" name="theme" id="theme-rose-pine" data-auto-dark="theme-rose-pine" data-auto-light="theme-rose-pine-dawn">
            <label for="theme-rose-pine">Main</label>
            <input type="radio" name="theme" id="theme-rose-pine-moon" data-auto-dark="theme-rose-pine-moon" data-auto-light="theme-rose-pine-dawn">
            <label for="theme-rose-pine-moon">Moon</label>
            <input type="radio" name="theme" id="theme-rose-pine-dawn" data-auto-dark="theme-rose-pine" data-auto-light="theme-rose-pine-dawn">
            <label for="theme-rose-pine-dawn">Dawn</label>
          </fieldset>
          <p class="theme-third-party">Theme sources and licences: <a href="THIRD_PARTY_NOTICES.txt">third-party notices</a></p>
        </form>
      </div>
tools/theme-menu.html14 KiBraw

CONTACT AND LICENSE

Contact request

Contact opens a native popover with an optional 254-character reply address and a required 500-character message. The form submits GET to the current page and targets contact-confirmation.

The first query item is a lowercase SITE_TITLE identifier followed by _contact=1. Runs outside letters, digits, dots, underscores, and hyphens become one underscore; edge underscores are removed; an empty identifier becomes arachnopress. Defaults are arachnopress_contact=1 for release and arachnogoat_contact=1 for build and full.

Contact request shapeValues are URL-encoded by the browserraw
Browser URL with SITE_TITLE="Example Site":
https://host.example/?example_site_contact=1&reply=operator%40example.com&message=Short+message#contact-confirmation

HTTP request target logged by a server that retains query strings:
GET /?example_site_contact=1&reply=operator%40example.com&message=Short+message

Contact privacy

HTTPS encrypts the request in transit. The address and message remain in the URL, browser history, and logs that record the query string. Do not submit confidential information. The static site provides no separate message delivery or storage.

The operator extracts and processes marked requests from the server log. arachnopress provides no log-processing component.

Project license

The license popover HTML-escapes and embeds tools/license.txt. Its first non-blank line is the title. THIRD_PARTY_NOTICES.txt maps bundled colour palettes to notices below licenses/.

TARGETS

build
Routine site-generation target. Replaces site/ with the deployment files, every source article, and newly generated output. Leaves release markers and archives unchanged.
release
Maintainer target for a new generator version. Builds only the generator article with its release download absent, replaces site/, creates arachnopress_GENERATOR_VERSION.tar.gz in the project root, and updates the source marker. The archive excludes itself.
full
Maintainer target used after release. Requires the root release archive, installs it in the staged generator article, generates the configured article set, replaces site/, and updates the source marker.

No clean, install, serve, or watch target is defined.

VARIABLES

RELEASE_SITE_ENV defines release site defaults. PUBLIC_SITE_ENV defines build and full site defaults. BUILD_ENV defines shared defaults. Environment and command-line make values override them.

Site identity

SITE_TITLE
Page title and displayed brand. It also forms the contact request marker prefix. Defaults: arachnopress for release; arachnogoat for build and full.
SITE_ICON
Character shown beside SITE_TITLE and rendered into the generated SVG favicon. Defaults: U+4DD6 for release; U+4DEA for build and full.
SITE_ICON_COLOUR
Optional fixed colour for the Unicode header icon and generated favicon. The value must be #rgb or #rrggbb. When empty, the header icon follows the current article-title colour and the generated favicon uses the SVG default text fill. It is unused when SITE_ICON_PATH is set. Default: empty.
SITE_ICON_PATH
Optional relative path to an SVG in the selected build tree. The build copies it unchanged to site/favicon.svg and uses it beside SITE_TITLE. SITE_ICON and SITE_ICON_COLOUR are then unused. Defaults: empty for release; articles/arachnopress/arachnogoatsundual.svg for build and full. Direct tools/build.sh execution defaults to empty. Set it to an empty value to restore the generated text icon.
SITE_ICON_PATH_THEME
true embeds the SITE_ICON_PATH SVG in the header and maps its colour-out and colour-in classes to the current article background and title colours. The favicon remains an unchanged copy. true requires SITE_ICON_PATH. false uses the SVG as an external header image. Defaults: false for release; true for build and full.
GENERATOR_LABEL
Generator name displayed in site metadata. Changing it does not affect project identity, the contact marker, source paths, or archive names. Default: arachnopress.
GENERATOR_VERSION
Generator software version, displayed after GENERATOR_LABEL and used in release archive names. Maintainers change it for a new generator release. It must contain dot-separated digits only. Default: 1.0.53.

Navigation and rendering

DEFAULT_THEME
Exact input ID from tools/theme-menu.html, with or without the theme- prefix. Defaults: solarized-dark for release; everforest-hard-dark for build and full.
DEFAULT_THEME_AUTO
Theme mode control. true renders Exact, Auto, Light, and Dark header choices, beside the Theme button when present, and initially selects Auto. Exact applies the selected theme unchanged. The other choices use its data-auto-dark and data-auto-light mapping; Auto follows the browser preference. A theme without a mapping remains fixed. false uses the selected exact variant and omits the control. Defaults: false for release; true for build and full.
DEFAULT_THEME_SELECTOR
Theme selector state. true renders the selector in either output mode; false fixes the selection to DEFAULT_THEME. Default: true.
SITE_MODE
Output layout. single-page writes all generated articles to index.html. multi-page writes index.html plus one slug.html per article. Default: single-page.
SITE_MODE_UNLISTED
Unlisted article generation. true enables unlisted output using the selected SITE_MODE; false excludes unlisted articles from generated HTML and navigation. Defaults: false for release; true for build and full.
SITE_URL_STYLE
Page-link format. html retains .html links. extensionless keeps generated .html files but uses slug and ./ for multi-page navigation, home links, and contact actions. Defaults: html for release; extensionless for build and full. Extensionless output requires a matching web-server rewrite. Single-page output is unchanged.
ARTICLE_ORDER
Initial order. Accepted values are title; created, creation, or ctime; and modified, modification, or mtime. Default: title.
HIGHLIGHTER
pygments, source-highlight, or none. Default: pygments.
CODE_FOOTER_LINES
Non-negative line threshold for automatically hiding code block footers. Default: 23.

ENVIRONMENT

PATH
Locates required utilities and optional highlighters and checksum programs.
TMPDIR
Parent for private static-site-build.* rendering directories. Default: /tmp. Target staging uses .site-build.* below the project root.

LC_ALL is set to C. SOURCE_HIGHLIGHT_DATADIR is unset so Source-highlight uses its installed data files and the project output definition.

BUILD

Routine site build

Run make build from the project root. It defaults to single-page output, includes unlisted articles, and enables the Theme and Exact/Auto/Light/Dark controls. The extensionless URL setting has no effect in single-page mode.

Set SITE_MODE=multi-page for per-article HTML files.

Set DEFAULT_THEME_AUTO=false or DEFAULT_THEME_SELECTOR=false to omit either theme control.

Set SITE_MODE_UNLISTED=false to exclude unlisted articles.

Representative build outputCounts depend on article content and installed toolsraw
Built 2 listed and 1 unlisted articles into index.html.
Default theme: theme-everforest-hard-dark. Article order: title. URL style: extensionless.
Theme selector: true.
Theme mode control: exact / auto / light / dark (theme-everforest-hard-dark / theme-everforest-hard-light).
Pygments available: 51 highlighted, 15 escaped raw.
Built site output in /path/to/arachnopress/site.

Custom site icon

Store the SVG below an included article and define colour-out and colour-in classes. Clear SITE_ICON_PATH to restore the generated text icon.

Custom site icon buildReplace article-slug after adding an SVGraw
# Run from the project root after adding the required SVG classes.
SITE_ICON_PATH=articles/article-slug/site-icon.svg \
    SITE_ICON_PATH_THEME=true make build

Generator release

After updating GENERATOR_VERSION, run make release from the project root. It renders the generator download as missing and creates the root archive.

Release integration

Run make full from the same project root after make release. It requires the matching root archive and installs it in the generated generator article. Both targets update the source marker only after publication.

Highlighter variants

HIGHLIGHTER=none selects escaped source. Select either installed highlighter explicitly to test its output.

Highlighter buildsRun separately from the project rootraw
# Run each command separately from the project root.
make build HIGHLIGHTER=none
make build HIGHLIGHTER=pygments
make build HIGHLIGHTER=source-highlight

REBUILD AND RECOVERY

Rebuild conditions

Re-run the build after changing an article, an article-local file, styles.css, a tool input, or a build setting. Generated stylesheet and favicon URLs contain cache tokens, so rebuilding publishes new URLs when those inputs change.

Failed or interrupted builds

Correct the diagnostic and rerun the target. Before publication, a validation, staging, or rendering failure leaves the previous site/, source marker, source archives, and root release archive unchanged. A publication failure may leave site/ incomplete; rerun before serving.

After all builds stop, remove files left by an untrappable termination. Inspect the wildcard expansions before removal.

Temporary-file recoveryRun from the project root after all builds stopraw
# Run from the project root after confirming that no build is running.
rm -rf "${TMPDIR:-/tmp}"/static-site-build.*
rm -rf .site-build.*

# Rebuild and publish a complete site tree.
make build

Remove generated output

No clean target is defined. Remove site/ without changing maintained source. Release replaces the current-version root archive but does not remove older root archives. Remove obsolete root archives by exact name.

Remove generated outputRun from the project rootraw
# Run from the project root when no build is running.
rm -rf site

FILES

site/
Fresh published output from the most recent target. Do not store maintained articles or run make in this directory.
site/index.html
Generated entry point. It contains every generated article in single-page mode or the first listed article in multi-page mode.
site/<slug>.html
Multi-page output for each listed article and each enabled unlisted article.
site/favicon.svg
Generated fallback or unchanged copy of SITE_ICON_PATH. Its link contains a cache version token.
site/theme-auto.css
Generated when DEFAULT_THEME_AUTO=true and a reachable theme has an automatic mapping. It contains light/dark rules derived from styles.css and the mappings in tools/theme-menu.html. Its link contains the shared stylesheet cache token.
site/styles.css
Copied site stylesheet.
THIRD_PARTY_NOTICES.txt
Maintained notice for bundled colour palettes. Targets copy it into site/ and release archives.
licenses/
Retained third-party licence notices. Targets copy the directory into site/ and release archives.
articles/<slug>/*
Maintained article source and published raw, download, and image assets. Targets copy them to site/articles/<slug>/.
arachnopress_GENERATOR_VERSION.tar.gz
Maintainer release archive produced by make release and consumed by make full from the same project root. Its top-level directory has the same name without the .tar.gz suffix.
README.arachnopress
The compact operator and interface reference.

EXIT STATUS

A target exits zero after its complete staged tree is published as site/, after release packaging when applicable, and after managed source cleanup. It exits non-zero on invalid settings or article structure, missing required tool inputs, duplicate or invalid IDs, no articles, or an unhandled command or filesystem failure.

Optional highlighting and checksum failures are not fatal. The build either emits escaped raw source or omits the checksum.

DIAGNOSTICS

Missing release archive
Run make release with the same generator version before make full. Run both commands from the same project root, not from site/.
DEFAULT_THEME does not exist in theme menu / Automatic theme mapping does not exist
Use IDs from tools/theme-menu.html. Define data-auto-dark and data-auto-light together, and include the source theme in its pair.
No listed articles found
Add or select at least one article not marked data-listed="false".
Extensionless article directory names must not contain dots / Extensionless article URL conflicts with a site root path
Rename the article directory and matching article ID. The slug must not contain a dot or match another generated root path.
Unsafe SITE_ICON_PATH / SITE_ICON_PATH_THEME requires SVG class
Use a readable relative .svg file in the staged tree. A themed icon must contain colour-out and colour-in class tokens.
Article, heading, or generated-marker format error
Apply the ARTICLE FORMAT and GENERATED BLOCKS rules. Keep indexed headings and empty generated markers on one source line.

Missing or invalid generated block files appear as visible missing blocks. They do not produce these fatal diagnostics.

SERVING

Local file access

Generated documentation may be bundled with a project and opened directly as site/index.html through file://. Single-page output works directly; multi-page output requires SITE_URL_STYLE=html. Contact submission is for HTTP or HTTPS deployment.

Local HTTP inspection

Serve site/ without changing its relative paths. The Python 3 command provides HTTP only, creates no project files, and does not rewrite extensionless URLs. Stop it with an interrupt.

Local HTTP serverRun after a build from the project rootraw
# Run from the project root after make build.
python3 -m http.server 8000 --directory site

Static deployment

Deploy site/ without changing its relative paths. Serve contact forms over HTTPS. The access log must retain query strings for contact messages to be available there.

OpenBSD httpd example

The OpenBSD httpd(8) example serves the site and rewrites extensionless article paths. Replace its host and document-root paths, validate the configuration, and reload httpd.

Its final rule appends .html internally to a missing final path component without a dot. Keep specific rules before it.

/etc/httpd.confReplace host, certificate, and document-root pathsraw
server "arachnogoat.com" {
	listen on * port 80

	location "/.well-known/acme-challenge/*" {
		root "/acme"
		request strip 2
	}

	location * {
		block return 301 "https://$HTTP_HOST$REQUEST_URI"
	}
}

server "arachnogoat.com" {
	listen on * tls port 443

	tls {
		certificate "/etc/ssl/arachnogoat.com.fullchain.pem"
		key "/etc/ssl/private/arachnogoat.com.key"
	}

	root "/htdocs/arachnogoat.com"

	# Required when arachnopress uses SITE_URL_STYLE=extensionless.
	location not found match "/[^./]+$" {
		request rewrite "$DOCUMENT_URI.html"
	}
}

CAVEATS

Trusted input

Article HTML and a themed SITE_ICON_PATH SVG are trusted author content. The build escapes generated text and code, but does not sanitize embedded article or SVG markup. Review both before building.

Single-page scale

Single-page output contains every generated article and highlighted code span. Large code-heavy collections increase HTML size and DOM cost. Use multi-page mode, focused excerpts, raw downloads, or HIGHLIGHTER=none when that cost becomes material.

Browser compatibility

CSS and popover support varies on older clients. No JavaScript compatibility layer is provided.

SEE ALSO

sh(1), make(1), awk(1), sed(1), tar(1), and cksum(1). On OpenBSD, see also httpd(8) for the optional deployment example.

LICENSE

arachnopress is distributed under the BSD 3-Clause license. See tools/license.txt or the License control in the site metadata. Bundled colour palettes retain their upstream values and third-party licences; see THIRD_PARTY_NOTICES.txt and licenses/.

Patch Notes

bsdsshd.rd

OpenBSD/amd64 installer ramdisk and miniroot targets with sshd access.

Downloadbsdsshd.rd.patchApplies below /usr/src46 KiBSHA256 (bsdsshd.rd.patch) = 155372ff85ab2b04a2be06bec0b2c86a9cab1397d400c27b5e51e697d1c828f1

DESCRIPTION

The patch adds separate bsdsshd.rd and minirootXX_sshd.img targets. The ramdisk configures networking from /auto_install.conf, starts sshd, and leaves the installer environment available to the remote root user.

The default retains the local installer menu and shell. RDSSHD_CONSOLE_LOCK=yes removes the local installer userland session. Boot-loader and kernel console output remain visible.

The target supports remote storage preparation and installation, including softraid(4) work with bioctl(8). Use autoinstall(8), install.site(5), and siteXX.tgz for fully unattended installation.

The patch adds files only. Stock source files, Makefiles, bsd.rd, minirootXX.img, and cdXX.iso targets are unchanged. Enhanced targets are available only through Makefile.rdsshd.

PATCH

The aggregate patch contains the build wrapper, kernel configurations, overlay patches, ramdisk files, and plain-text reference.

bsdsshd.rd.patchraw
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/Makefile.rdsshd
@@ -0,0 +1,411 @@
+#	$OpenBSD$
+
+.include <bsd.own.mk>
+
+RDSSHD_AUTHORIZED_KEYS?=
+RDSSHD_HOST_KEY?=
+RDSSHD_CONSOLE_LOCK?=	no
+RDSSHD_KERNEL_STACK_PROTECTOR?=	yes
+RDSSHD_PORT?=	22
+RDSSHD_AI_IF?=	em0
+RDSSHD_AI_HOSTNAME?=	rdinstall
+RDSSHD_AI_IPV4?=	autoconf
+RDSSHD_AI_NETMASK?=	255.255.255.0
+RDSSHD_AI_ROUTE?=	none
+RDSSHD_AI_IPV6?=	none
+RDSSHD_AI_DOMAIN?=	my.domain
+RDSSHD_AI_DNS?=	none
+RDSSHD_FSSIZE?=	32768
+
+RDSSHD_BASEDIR=	${.CURDIR}/..
+RDSSHD_TOP=	${.CURDIR}/../../../..
+RDSSHD_UTILS=	${RDSSHD_BASEDIR}/../../miniroot
+RDSSHD_MTREE=	${RDSSHD_UTILS}/mtree.conf
+RDSSHD_EFIBOOT=	${DESTDIR}/usr/mdec/BOOTX64.EFI \
+		${DESTDIR}/usr/mdec/BOOTIA32.EFI
+RDSSHD_MOUNT_ARGS_MSDOS=	-o-s
+
+RDSSHD_RAMDISK=	RAMDISK_CD_SSHD
+RDSSHD_KERNEL=	${.OBJDIR}/bsd
+.if ${RDSSHD_KERNEL_STACK_PROTECTOR:L} == "no"
+RDSSHD_RAMDISK=	RAMDISK_CD_SSHD_NO_PROPOLICE
+RDSSHD_KERNEL=	${.OBJDIR}/bsd.no-propolice
+.endif
+RDSSHD_FS=	miniroot${OSrev}_sshd.img
+RDSSHD_BUILDOBJDIR=	${.OBJDIR}/build
+RDSSHD_KERNELOBJDIR=	${RDSSHD_BUILDOBJDIR}/kernel/${RDSSHD_RAMDISK}
+RDSSHD_INSTBIN=	${RDSSHD_BUILDOBJDIR}/instbin
+RDSSHD_STAGE=	${.OBJDIR}/stage
+RDSSHD_RDOBJDIR=	${.OBJDIR}/rdobj
+RDSSHD_SSHOBJDIR=	${.OBJDIR}/sshobj
+RDSSHD_INITOBJDIR=	${.OBJDIR}/initobj
+RDSSHD_INITSRCDIR=	${.OBJDIR}/initsrc
+RDSSHD_MOUNT_POINT=	${.OBJDIR}/mnt
+RDSSHD_VND=	${.OBJDIR}/vnd
+RDSSHD_BOOT=	${.OBJDIR}/boot
+RDSSHD_OBJCHECK=	${.OBJDIR}/.rdsshd-obj-ok
+RDSSHD_MAKEFILE=	${.CURDIR}/Makefile.rdsshd
+RDSSHD_INITMAKEFILE=	${.CURDIR}/Makefile.init
+RDSSHD_KERNELCONF=	${.CURDIR}/${RDSSHD_RAMDISK}
+RDSSHD_BASECONF=	${RDSSHD_TOP}/sys/arch/${MACHINE}/conf/RAMDISK_CD
+RDSSHD_KERNELCONFDEPS=	${RDSSHD_BASECONF}
+RDSSHD_LISTS=	${RDSSHD_BASEDIR}/list ${.CURDIR}/list.sshd \
+		${RDSSHD_STAGE}/list.console
+RDSSHD_INITSRC=	${RDSSHD_INITSRCDIR}/init.c
+RDSSHD_INIT=	${RDSSHD_STAGE}/init
+RDSSHD_INSTALLERPATCH=	${.CURDIR}/installer.sshd.patch
+RDSSHD_CONSOLEPATCH=	${.CURDIR}/console-lock.sshd.patch
+RDSSHD_INITPATCH=	${.CURDIR}/init.sshd.patch
+RDSSHD_SSHDCONF=	${.CURDIR}/sshd_config
+RDSSHD_MRMAKEFSARGS?=	-s 20m \
+		-o rdroot,minfree=0,bsize=4096,fsize=512,density=4096
+RDSSHD_BINS=	${RDSSHD_STAGE}/bin/sshd \
+		${RDSSHD_STAGE}/bin/ssh-keygen \
+		${RDSSHD_STAGE}/bin/sshd-session \
+		${RDSSHD_STAGE}/bin/sshd-auth
+
+RDSSHD_INITDEP=
+.if ${RDSSHD_CONSOLE_LOCK:L} == "yes"
+RDSSHD_INITDEP=	${RDSSHD_INIT}
+.endif
+
+.PHONY: rdsshd
+rdsshd: bsdsshd.rd ${RDSSHD_FS}
+
+.PHONY: rdsshd-objcheck
+rdsshd-objcheck: ${RDSSHD_OBJCHECK}
+
+${RDSSHD_OBJCHECK}:
+	@if [ "${.OBJDIR}" = "${.CURDIR}" ]; then \
+		echo "private object directory is not active;" >&2; \
+		echo "run 'make -f Makefile.rdsshd obj' separately first" >&2; \
+		exit 1; \
+	fi
+	touch $@
+
+${RDSSHD_KERNEL}: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE} \
+	    ${RDSSHD_KERNELCONF} ${RDSSHD_KERNELCONFDEPS}
+	install -d -o ${BUILDUSER} -g ${WOBJGROUP} ${RDSSHD_KERNELOBJDIR}
+	su ${BUILDUSER} -c \
+	    'config -b ${RDSSHD_KERNELOBJDIR} -s ${RDSSHD_TOP}/sys \
+	    ${RDSSHD_KERNELCONF} && cd ${RDSSHD_KERNELOBJDIR} && \
+	    MAKEOBJDIR=${RDSSHD_KERNELOBJDIR} ${MAKE} clean && \
+	    exec env MAKEOBJDIR=${RDSSHD_KERNELOBJDIR} ${MAKE} ${MFLAGS}'
+	cp -p ${RDSSHD_KERNELOBJDIR}/bsd $@
+
+bsdsshd.gz: bsdsshd.rd
+	objcopy -g -x -R .comment -R .SUNW_ctf \
+	    -K rd_root_size -K rd_root_image \
+	    bsdsshd.rd bsdsshd.strip
+	gzip -9cn bsdsshd.strip > bsdsshd.gz
+.if !empty(RDSSHD_HOST_KEY)
+	chmod 600 bsdsshd.strip $@
+.endif
+
+.PHONY: rdsshd-stock-deps rdsshd-instbin
+rdsshd-stock-deps: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
+	@_objroot=`cd ${RDSSHD_BASEDIR} && ${MAKE} -V BSDOBJDIR`; \
+	if [ ! -d "$$_objroot" ]; then \
+		echo "normal OpenBSD object root does not exist: $$_objroot" >&2; \
+		echo "create it before building rdsshd" >&2; \
+		exit 1; \
+	fi
+	cd ${RDSSHD_TOP}/lib && ${MAKE} obj
+	cd ${RDSSHD_TOP}/distrib/special && ${MAKE} obj
+	cd ${RDSSHD_BASEDIR} && ${MAKE} obj
+	@_srcdir=`cd ${RDSSHD_BASEDIR} && pwd`; \
+	_objdir=`cd ${RDSSHD_BASEDIR} && ${MAKE} -V .OBJDIR`; \
+	if [ "$$_objdir" = "$$_srcdir" ] || [ ! -d "$$_objdir" ]; then \
+		echo "normal ramdisk_cd object directory is not active" >&2; \
+		exit 1; \
+	fi
+	cd ${RDSSHD_TOP}/distrib/special/libstubs && ${MAKE} ${MFLAGS}
+
+rdsshd-instbin: rdsshd-stock-deps
+	cd ${RDSSHD_BASEDIR} && ${MAKE} ${MFLAGS} instbin
+	install -d ${RDSSHD_BUILDOBJDIR}
+	@_objdir=`cd ${RDSSHD_BASEDIR} && ${MAKE} -V .OBJDIR`; \
+	if [ ! -f "$$_objdir/instbin" ]; then \
+		echo "stock ramdisk_cd instbin was not built" >&2; \
+		exit 1; \
+	fi; \
+	cp -p "$$_objdir/instbin" ${RDSSHD_INSTBIN}
+
+bsdsshd.rd: rdsshd-files ${RDSSHD_INITDEP} rdsshd-instbin \
+	    ${RDSSHD_KERNEL}
+	install -d ${RDSSHD_RDOBJDIR}
+	rm -f ${RDSSHD_RDOBJDIR}/instbin \
+	    ${RDSSHD_RDOBJDIR}/mr.fs \
+	    ${RDSSHD_RDOBJDIR}/bsd.rd
+	cp -p ${RDSSHD_INSTBIN} ${RDSSHD_RDOBJDIR}/instbin
+	rm -rf ${RDSSHD_RDOBJDIR}/mr.fs.d
+	install -d -o root -g wheel ${RDSSHD_RDOBJDIR}/mr.fs.d
+	mtree -def ${RDSSHD_MTREE} -p ${RDSSHD_RDOBJDIR}/mr.fs.d -u
+	CURDIR=${RDSSHD_BASEDIR} OBJDIR=${RDSSHD_RDOBJDIR} OSrev=${OSrev} \
+	    TARGDIR=${RDSSHD_RDOBJDIR}/mr.fs.d UTILS=${RDSSHD_UTILS} \
+	    RELEASEDIR=${RELEASEDIR} sh ${RDSSHD_UTILS}/runlist.sh \
+	    ${RDSSHD_LISTS}
+	rm ${RDSSHD_RDOBJDIR}/mr.fs.d/instbin
+	makefs ${RDSSHD_MRMAKEFSARGS} ${RDSSHD_RDOBJDIR}/mr.fs \
+	    ${RDSSHD_RDOBJDIR}/mr.fs.d
+	cp -p ${RDSSHD_KERNEL} ${RDSSHD_RDOBJDIR}/bsd.rd
+	rdsetroot ${RDSSHD_RDOBJDIR}/bsd.rd ${RDSSHD_RDOBJDIR}/mr.fs
+	cp ${RDSSHD_RDOBJDIR}/bsd.rd $@
+.if !empty(RDSSHD_HOST_KEY)
+	chmod 600 ${RDSSHD_RDOBJDIR}/mr.fs \
+	    ${RDSSHD_RDOBJDIR}/bsd.rd $@
+.endif
+
+${RDSSHD_FS}: bsdsshd.gz
+	-umount -f ${RDSSHD_MOUNT_POINT} >/dev/null 2>&1
+	@if [ -e ${RDSSHD_VND} ] && [ ! -s ${RDSSHD_VND} ]; then \
+		rm -f ${RDSSHD_VND}; \
+	fi
+	@if [ -e ${RDSSHD_VND} ]; then \
+		echo "stale private vnd state; run" \
+		    "'make -f Makefile.rdsshd unconfig-rdsshd' first" >&2; \
+		exit 1; \
+	fi
+	install -d ${RDSSHD_MOUNT_POINT}
+	dd if=/dev/zero of=${RDSSHD_FS} bs=512 count=${RDSSHD_FSSIZE}
+	vnconfig -v ${.OBJDIR}/${RDSSHD_FS} > ${RDSSHD_VND}
+	fdisk -yi -l ${RDSSHD_FSSIZE} -b 960 -f ${DESTDIR}/usr/mdec/mbr \
+	    `cat ${RDSSHD_VND}`
+	echo '/ *' | disklabel -wAT- `cat ${RDSSHD_VND}`
+	newfs -t msdos /dev/r`cat ${RDSSHD_VND}`i
+	mount ${RDSSHD_MOUNT_ARGS_MSDOS} /dev/`cat ${RDSSHD_VND}`i \
+	    ${RDSSHD_MOUNT_POINT}
+	mkdir -p ${RDSSHD_MOUNT_POINT}/efi/boot
+	cp ${RDSSHD_EFIBOOT} ${RDSSHD_MOUNT_POINT}/efi/boot
+	umount ${RDSSHD_MOUNT_POINT}
+	newfs -O 1 -m 0 -o space -i 524288 -c ${RDSSHD_FSSIZE} \
+	    /dev/r`cat ${RDSSHD_VND}`a
+	mount /dev/`cat ${RDSSHD_VND}`a ${RDSSHD_MOUNT_POINT}
+	objcopy -S -R .comment ${DESTDIR}/usr/mdec/boot ${RDSSHD_BOOT}
+	installboot -v -r ${RDSSHD_MOUNT_POINT} `cat ${RDSSHD_VND}` \
+	    ${DESTDIR}/usr/mdec/biosboot ${RDSSHD_BOOT}
+	install -c -m 555 -o root -g wheel bsdsshd.gz \
+	    ${RDSSHD_MOUNT_POINT}/bsd
+	df -i ${RDSSHD_MOUNT_POINT}
+	umount ${RDSSHD_MOUNT_POINT}
+	vnconfig -u `cat ${RDSSHD_VND}`
+	rm -f ${RDSSHD_VND}
+.if !empty(RDSSHD_HOST_KEY)
+	chmod 600 $@
+.endif
+
+.PHONY: rdsshd-check rdsshd-files rdsshd-scripts
+rdsshd-check:
+	@if [ -z "${RDSSHD_AUTHORIZED_KEYS}" ]; then \
+		echo "set RDSSHD_AUTHORIZED_KEYS to a public key file" >&2; \
+		exit 1; \
+	fi
+	@case "${RDSSHD_CONSOLE_LOCK:L}" in \
+	yes|no) ;; \
+	*) echo "RDSSHD_CONSOLE_LOCK must be yes or no" >&2; exit 1;; \
+	esac
+	@case "${RDSSHD_KERNEL_STACK_PROTECTOR:L}" in \
+	yes|no) ;; \
+	*) echo "RDSSHD_KERNEL_STACK_PROTECTOR must be yes or no" >&2; exit 1;; \
+	esac
+
+${RDSSHD_INITSRC}: ${RDSSHD_OBJCHECK} ${RDSSHD_TOP}/sbin/init/init.c \
+	    ${RDSSHD_INITPATCH}
+	install -d ${RDSSHD_INITSRCDIR}
+	rm -f ${RDSSHD_INITSRC}.tmp ${RDSSHD_INITSRC}.tmp.orig \
+	    ${RDSSHD_INITSRC}.tmp.rej
+	cp ${RDSSHD_TOP}/sbin/init/init.c ${RDSSHD_INITSRC}.tmp
+	cd ${RDSSHD_INITSRCDIR} && \
+	    patch -f -s -p0 -F0 < ${RDSSHD_INITPATCH}
+	mv ${RDSSHD_INITSRC}.tmp ${RDSSHD_INITSRC}
+
+${RDSSHD_INIT}: ${RDSSHD_INITSRC} \
+	    ${RDSSHD_TOP}/sbin/init/pathnames.h ${RDSSHD_INITMAKEFILE} \
+	    ${RDSSHD_BASEDIR}/../../special/init/Makefile \
+	    ${RDSSHD_BASEDIR}/../../special/Makefile.inc
+	install -d ${RDSSHD_INITOBJDIR} ${RDSSHD_STAGE}
+	cd ${RDSSHD_BASEDIR}/../../special/init && \
+	    MAKEOBJDIR=${RDSSHD_INITOBJDIR} ${MAKE} ${MFLAGS} \
+	    -f ${RDSSHD_INITMAKEFILE} \
+	    RDSSHD_INITSRCDIR=${RDSSHD_INITSRCDIR} init
+	install -c -s ${RDSSHD_INITOBJDIR}/init $@
+
+rdsshd-scripts: ${RDSSHD_OBJCHECK} ${RDSSHD_UTILS}/install.sub \
+	    ${RDSSHD_UTILS}/dot.profile \
+	    ${RDSSHD_INSTALLERPATCH} ${RDSSHD_CONSOLEPATCH}
+	install -d ${RDSSHD_STAGE}
+	rm -f ${RDSSHD_STAGE}/install.sub.tmp \
+	    ${RDSSHD_STAGE}/install.sub.tmp.orig \
+	    ${RDSSHD_STAGE}/install.sub.tmp.rej \
+	    ${RDSSHD_STAGE}/dot.profile.tmp \
+	    ${RDSSHD_STAGE}/dot.profile.tmp.orig \
+	    ${RDSSHD_STAGE}/dot.profile.tmp.rej
+	cp ${RDSSHD_UTILS}/install.sub ${RDSSHD_STAGE}/install.sub.tmp
+	cp ${RDSSHD_UTILS}/dot.profile ${RDSSHD_STAGE}/dot.profile.tmp
+	cd ${RDSSHD_STAGE} && \
+	    patch -f -s -p0 -F0 < ${RDSSHD_INSTALLERPATCH}
+.if ${RDSSHD_CONSOLE_LOCK:L} == "yes"
+	cd ${RDSSHD_STAGE} && \
+	    patch -f -s -p0 -F0 < ${RDSSHD_CONSOLEPATCH}
+.endif
+	chmod 755 ${RDSSHD_STAGE}/install.sub.tmp
+	mv ${RDSSHD_STAGE}/install.sub.tmp ${RDSSHD_STAGE}/install.sub
+	mv ${RDSSHD_STAGE}/dot.profile.tmp ${RDSSHD_STAGE}/dot.profile
+
+rdsshd-files: rdsshd-check rdsshd-scripts ${RDSSHD_BINS} \
+	    ${RDSSHD_SSHDCONF}
+	install -d ${RDSSHD_STAGE}
+	@if [ "${RDSSHD_CONSOLE_LOCK:L}" = yes ]; then \
+		echo 'COPY ${RDSSHD_INIT} sbin/init'; \
+	fi > ${RDSSHD_STAGE}/list.console
+	sed '/^root:/s|:/bin/ksh$$|:/bin/sh|' \
+	    ${RDSSHD_UTILS}/master.passwd > ${RDSSHD_STAGE}/master.passwd
+	grep '^root:.*:/bin/sh$$' ${RDSSHD_STAGE}/master.passwd >/dev/null
+	sed -e '/^[	 ]*$$/d' -e '/^[	 ]*#/d' \
+	    < "${RDSSHD_AUTHORIZED_KEYS}" > ${RDSSHD_STAGE}/authorized_keys
+	test -s ${RDSSHD_STAGE}/authorized_keys
+	@_n=0; while IFS= read -r _key; do \
+		_n=$$((_n + 1)); \
+		if ! printf '%s\n' "$$_key" | \
+		    ${RDSSHD_STAGE}/bin/ssh-keygen -l -f - >/dev/null 2>&1; then \
+			echo "invalid public key on line $$_n of RDSSHD_AUTHORIZED_KEYS" >&2; \
+			exit 1; \
+		fi; \
+	done < ${RDSSHD_STAGE}/authorized_keys
+	grep '^sshd:' ${RDSSHD_STAGE}/master.passwd >/dev/null || \
+	    grep '^sshd:' ${RDSSHD_TOP}/etc/master.passwd >> \
+	    ${RDSSHD_STAGE}/master.passwd
+	cp ${RDSSHD_UTILS}/group ${RDSSHD_STAGE}/group
+	grep '^sshd:' ${RDSSHD_STAGE}/group >/dev/null || \
+	    grep '^sshd:' ${RDSSHD_TOP}/etc/group >> ${RDSSHD_STAGE}/group
+	{ \
+	    printf 'System hostname = %s\n' '${RDSSHD_AI_HOSTNAME}'; \
+	    printf 'Network interface to configure = %s\n' '${RDSSHD_AI_IF}'; \
+	    printf 'IPv4 address for %s = %s\n' \
+		'${RDSSHD_AI_IF}' '${RDSSHD_AI_IPV4}'; \
+	    case '${RDSSHD_AI_IPV4}' in \
+	    none|autoconf|dhcp) ;; \
+	    *) printf 'Netmask for %s = %s\n' \
+		'${RDSSHD_AI_IF}' '${RDSSHD_AI_NETMASK}'; \
+	       printf 'Default IPv4 route = %s\n' '${RDSSHD_AI_ROUTE}' ;; \
+	    esac; \
+	    printf 'IPv6 address for %s = %s\n' \
+		'${RDSSHD_AI_IF}' '${RDSSHD_AI_IPV6}'; \
+	    printf 'Network interface to configure = done\n'; \
+	    printf 'DNS domain name = %s\n' '${RDSSHD_AI_DOMAIN}'; \
+	    printf 'DNS nameservers = %s\n' '${RDSSHD_AI_DNS}'; \
+	} > ${RDSSHD_STAGE}/auto_install.conf
+	sed 's|^Port .*|Port ${RDSSHD_PORT}|' ${RDSSHD_SSHDCONF} > \
+	    ${RDSSHD_STAGE}/sshd_config
+	rm -f ${RDSSHD_STAGE}/ssh_host_ed25519_key \
+	    ${RDSSHD_STAGE}/ssh_host_ed25519_key.pub \
+	    ${RDSSHD_STAGE}/ssh_host_ed25519_key.test \
+	    ${RDSSHD_STAGE}/ssh_host_ed25519_key.test.pub \
+	    ${RDSSHD_STAGE}/sshd_config.test
+	@if [ -n "${RDSSHD_HOST_KEY}" ]; then \
+		if ! ${RDSSHD_STAGE}/bin/ssh-keygen -y -P '' \
+		    -f "${RDSSHD_HOST_KEY}" 2>/dev/null | grep -q '^ssh-ed25519 '; then \
+			echo "RDSSHD_HOST_KEY is not an unencrypted Ed25519 private key" >&2; \
+			exit 1; \
+		fi; \
+		install -c -m 600 "${RDSSHD_HOST_KEY}" \
+		    ${RDSSHD_STAGE}/ssh_host_ed25519_key; \
+	fi
+	@_key=${RDSSHD_STAGE}/ssh_host_ed25519_key; \
+	_testkey=${RDSSHD_STAGE}/ssh_host_ed25519_key.test; \
+	_testconf=${RDSSHD_STAGE}/sshd_config.test; \
+	if [[ ! -s $$_key ]]; then \
+		_key=$$_testkey; \
+		${RDSSHD_STAGE}/bin/ssh-keygen -q -t ed25519 -N '' \
+		    -f $$_key || exit 1; \
+	fi; \
+	sed "s|^HostKey .*|HostKey $$_key|" \
+	    ${RDSSHD_STAGE}/sshd_config > $$_testconf; \
+	${RDSSHD_STAGE}/bin/sshd -t -f $$_testconf; \
+	_status=$$?; \
+	rm -f $$_testconf $$_testkey $$_testkey.pub; \
+	exit $$_status
+${RDSSHD_STAGE}/bin/sshd: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
+	install -d ${RDSSHD_STAGE}/bin ${RDSSHD_SSHOBJDIR}/sshd
+	cd ${RDSSHD_TOP}/usr.bin/ssh/sshd && \
+	    MAKEOBJDIR=${RDSSHD_SSHOBJDIR}/sshd ${MAKE} ${MFLAGS} \
+	    LDSTATIC="${STATIC}" ZLIB=no
+	install -c -s ${RDSSHD_SSHOBJDIR}/sshd/sshd $@
+
+${RDSSHD_STAGE}/bin/ssh-keygen: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
+	install -d ${RDSSHD_STAGE}/bin ${RDSSHD_SSHOBJDIR}/ssh-keygen
+	cd ${RDSSHD_TOP}/usr.bin/ssh/ssh-keygen && \
+	    MAKEOBJDIR=${RDSSHD_SSHOBJDIR}/ssh-keygen ${MAKE} ${MFLAGS} \
+	    LDSTATIC="${STATIC}" ZLIB=no
+	install -c -s ${RDSSHD_SSHOBJDIR}/ssh-keygen/ssh-keygen $@
+
+${RDSSHD_STAGE}/bin/sshd-session: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
+	install -d ${RDSSHD_STAGE}/bin ${RDSSHD_SSHOBJDIR}/sshd-session
+	cd ${RDSSHD_TOP}/usr.bin/ssh/sshd-session && \
+	    MAKEOBJDIR=${RDSSHD_SSHOBJDIR}/sshd-session ${MAKE} ${MFLAGS} \
+	    LDSTATIC="${STATIC}" ZLIB=no
+	install -c -s ${RDSSHD_SSHOBJDIR}/sshd-session/sshd-session $@
+
+${RDSSHD_STAGE}/bin/sshd-auth: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
+	install -d ${RDSSHD_STAGE}/bin ${RDSSHD_SSHOBJDIR}/sshd-auth
+	cd ${RDSSHD_TOP}/usr.bin/ssh/sshd-auth && \
+	    MAKEOBJDIR=${RDSSHD_SSHOBJDIR}/sshd-auth ${MAKE} ${MFLAGS} \
+	    LDSTATIC="${STATIC}" ZLIB=no
+	install -c -s ${RDSSHD_SSHOBJDIR}/sshd-auth/sshd-auth $@
+
+.PHONY: unconfig-rdsshd
+unconfig-rdsshd:
+	-umount -f ${RDSSHD_MOUNT_POINT} >/dev/null 2>&1
+	@if [ -e ${RDSSHD_VND} ] && [ ! -s ${RDSSHD_VND} ]; then \
+		rm -f ${RDSSHD_VND}; \
+	elif [ -f ${RDSSHD_VND} ]; then \
+		_vnd=`cat ${RDSSHD_VND}`; \
+		_unit=$${_vnd#vnd}; \
+		if [ "vnd$$_unit" != "$$_vnd" ] || [ -z "$$_unit" ]; then \
+			echo "invalid private vnd state: $$_vnd" >&2; exit 1; \
+		fi; \
+		case "$$_unit" in \
+		*[!0-9]*) echo "invalid private vnd state: $$_vnd" >&2; exit 1;; \
+		esac; \
+		_info=`vnconfig -l "$$_vnd"` || exit 1; \
+		case "$$_info" in \
+		"$$_vnd: not in use") rm -f ${RDSSHD_VND} ;; \
+		"$$_vnd: covering ${.OBJDIR}/${RDSSHD_FS} on "*) \
+			vnconfig -u "$$_vnd" && rm -f ${RDSSHD_VND} ;; \
+		*) echo "refusing to detach vnd not owned by rdsshd: $$_info" >&2; \
+			exit 1 ;; \
+		esac; \
+	fi
+
+.ifdef RELEASEDIR
+.PHONY: install-rdsshd
+install-rdsshd: bsdsshd.gz ${RDSSHD_FS}
+	cp bsdsshd.gz ${RELEASEDIR}/bsdsshd.rd
+	cp ${RDSSHD_FS} ${RELEASEDIR}
+.if empty(RDSSHD_HOST_KEY)
+	chmod a+r ${RELEASEDIR}/bsdsshd.rd
+.else
+	chmod 600 ${RELEASEDIR}/bsdsshd.rd ${RELEASEDIR}/${RDSSHD_FS}
+.endif
+.endif
+
+.PHONY: clean-rdsshd clean cleandir
+clean-rdsshd: unconfig-rdsshd
+	rm -f bsdsshd.rd bsdsshd.gz bsdsshd.strip ${RDSSHD_FS} \
+	    ${.OBJDIR}/bsd ${.OBJDIR}/bsd.no-propolice ${RDSSHD_BOOT} \
+	    ${RDSSHD_OBJCHECK}
+	rm -rf ${RDSSHD_BUILDOBJDIR} ${RDSSHD_STAGE} ${RDSSHD_RDOBJDIR} \
+	    ${RDSSHD_SSHOBJDIR} ${RDSSHD_INITOBJDIR} ${RDSSHD_INITSRCDIR}
+	-rmdir ${RDSSHD_MOUNT_POINT}
+
+clean cleandir: clean-rdsshd
+
+.PHONY: prepare-unpatch-rdsshd
+prepare-unpatch-rdsshd: clean-rdsshd
+	@if [ -L ${.CURDIR}/obj ]; then \
+		_obj=`readlink ${.CURDIR}/obj`; \
+		echo "empty private object directory may be removed: $$_obj"; \
+		rm -f ${.CURDIR}/obj; \
+	fi
+
+.include <bsd.obj.mk>
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/Makefile.init
@@ -0,0 +1,5 @@
+#	$OpenBSD$
+
+.PATH: ${RDSSHD_INITSRCDIR}
+CPPFLAGS+=	-I${.CURDIR}/../../../sbin/init
+.include "${.CURDIR}/Makefile"
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/README.rdsshd
@@ -0,0 +1,599 @@
+BSDSSHD.RD                 bsdsshd.rd build notes                 BSDSSHD.RD
+
+NAME
+     bsdsshd.rd - OpenBSD/amd64 installer ramdisk with sshd access
+
+DESCRIPTION
+     The patch adds separate bsdsshd.rd and minirootXX_sshd.img targets.
+     The ramdisk configures networking from /auto_install.conf, starts sshd,
+     and leaves the installer environment available to the remote root user.
+
+     The default retains the local installer menu and shell.
+     RDSSHD_CONSOLE_LOCK=yes removes the local installer userland session.
+     Boot-loader and kernel console output remain visible.
+
+     The target supports remote storage preparation and installation, including
+     softraid(4) work with bioctl(8).  Use autoinstall(8), install.site(5), and
+     siteXX.tgz when full unattended installation is appropriate.
+
+     The patch adds files only.  Stock source files, Makefiles, bsd.rd,
+     minirootXX.img, and cdXX.iso targets are unchanged.  Enhanced targets are
+     available only through Makefile.rdsshd.
+
+SYNOPSIS
+     Run the obj target separately, then build with at least one authorized
+     public key:
+
+           make -f Makefile.rdsshd obj
+           make -f Makefile.rdsshd rdsshd \
+               RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub
+
+SOURCE LAYOUT
+     The patch adds one source directory containing eleven files:
+
+           distrib/amd64/ramdisk_cd/rdsshd/Makefile.rdsshd
+           distrib/amd64/ramdisk_cd/rdsshd/Makefile.init
+           distrib/amd64/ramdisk_cd/rdsshd/README.rdsshd
+           distrib/amd64/ramdisk_cd/rdsshd/console-lock.sshd.patch
+           distrib/amd64/ramdisk_cd/rdsshd/init.sshd.patch
+           distrib/amd64/ramdisk_cd/rdsshd/installer.sshd.patch
+           distrib/amd64/ramdisk_cd/rdsshd/list.sshd
+           distrib/amd64/ramdisk_cd/rdsshd/root.profile
+           distrib/amd64/ramdisk_cd/rdsshd/sshd_config
+           distrib/amd64/ramdisk_cd/rdsshd/RAMDISK_CD_SSHD
+           distrib/amd64/ramdisk_cd/rdsshd/RAMDISK_CD_SSHD_NO_PROPOLICE
+
+TARGETS
+     rdsshd
+             Builds bsdsshd.rd and minirootXX_sshd.img.
+
+     install-rdsshd
+             Defined when RELEASEDIR is set.  Installs compressed bsdsshd.rd
+             and minirootXX_sshd.img:
+
+                   make -f Makefile.rdsshd install-rdsshd \
+                       RELEASEDIR=/path/to/release
+
+     unconfig-rdsshd
+             Unmounts the private mount point and detaches its recorded vnd
+             after an interrupted miniroot build.
+
+     clean-rdsshd, clean, cleandir
+             Remove private rdsshd outputs.  Stock instbin objects remain under
+             normal OpenBSD clean ownership.
+
+     prepare-unpatch-rdsshd
+             Runs private cleanup, prints the wrapper object path, and removes
+             the rdsshd source obj symlink.
+
+BUILD MODEL
+     Private paths below the rdsshd object directory:
+
+           obj/build/kernel       enhanced kernel objects
+           obj/build/instbin      copy of stock instbin
+           obj/sshobj             static OpenSSH objects
+           obj/initsrc            optional patched init source
+           obj/initobj            optional private init objects
+           obj/stage              generated and overlay files
+           obj/rdobj              ramdisk assembly
+           obj/bsd*               copied enhanced kernels
+           obj/boot               miniroot boot file
+           obj/mnt                miniroot mount point
+           obj/vnd                vnd ownership record
+
+     The kernel is configured with config(8) -b and built below
+     obj/build/kernel.  No sys/arch/amd64/compile directory is added or used.
+     OpenSSH uses explicit private MAKEOBJDIR paths.
+
+     instbin is unmodified.  Its normal objects remain under:
+
+           ${BSDOBJDIR}/distrib/amd64/ramdisk_cd
+           ${BSDOBJDIR}/distrib/special
+           ${BSDOBJDIR}/lib
+
+     The wrapper runs the normal obj targets for lib, distrib/special, and
+     ramdisk_cd.  It builds stock distrib/special/libstubs, then invokes the
+     stock ramdisk_cd instbin target.  That target builds component objects and
+     reduced source libraries.  The wrapper does not transform crunchgen
+     configuration or generated Makefiles.  It copies the completed instbin to
+     obj/build/instbin before ramdisk assembly.
+
+     BSDOBJDIR must exist and retain normal OpenBSD ownership and permissions.
+     The default is /usr/obj, owned by build:wobj with mode 770.  The build
+     rejects source-directory object fallback for both the wrapper and
+     ramdisk_cd.
+
+     Normal clean and cleandir targets own the shared instbin objects.
+     clean-rdsshd owns only the private copy and enhanced outputs.  Do not run
+     rdsshd concurrently with a normal build or clean using the same instbin
+     objects.
+
+     No parent Makefile or SUBDIR list is changed.  Normal top-level builds and
+     cleans do not enter the rdsshd source directory.
+
+     Normal and enhanced media use separate boot files, mount points, image
+     names, and vnd state.  Cleanup detaches only the recorded vnd covering the
+     absolute enhanced image path.  A missing, malformed, or reused vnd is
+     reported and left attached.
+
+KERNEL
+     RAMDISK_CD_SSHD includes stock RAMDISK_CD, then applies:
+
+           rmoption NO_PROPOLICE
+           rmoption MINIROOTSIZE
+           option MINIROOTSIZE=40960
+           pseudo-device pty 16
+
+     Removing NO_PROPOLICE enables normal kernel stack protection.
+     RDSSHD_KERNEL_STACK_PROTECTOR=no selects
+     RAMDISK_CD_SSHD_NO_PROPOLICE, which retains NO_PROPOLICE.
+
+     MINIROOTSIZE=40960 reserves a 20 MiB rdroot.  Sixteen ptys support ssh
+     sessions.  SMALL_KERNEL remains enabled.
+
+     A temporary RDSSHD_RDROOT option protects config(8)'s option-list append
+     pointer while inherited tail entries are removed.  It is absent from the
+     final configuration.
+
+RAMDISK
+     Stock bsd.rd uses the host disktab entry rdrootb:
+
+           MRMAKEFSARGS=-o disklabel=rdrootb,minfree=0,density=4096
+
+     bsdsshd.rd instead gives makefs(8) an explicit 20 MiB layout:
+
+           -s 20m \
+             -o rdroot,minfree=0,bsize=4096,fsize=512,density=4096
+
+     This does not read or modify /etc/disktab.
+
+     Stock installer programs remain in instbin.  OpenSSH is built through its
+     normal Makefiles as separate static PIE executables with zlib disabled:
+
+           /usr/sbin/sshd
+           /usr/bin/ssh-keygen
+           /usr/libexec/sshd-session
+           /usr/libexec/sshd-auth
+
+     The ssh(1) client is not included.
+
+     list.sshd adds:
+
+           /etc/ssh/sshd_config
+           /root/.ssh/authorized_keys
+           /root/.profile
+           /auto_install.conf
+           /etc/login.conf
+
+     It also adds the sshd account and pty devices.  Root retains stock
+     instbin's -sh argv link.  root.profile leaves sh mode, sets the installer
+     environment, and sets TERM=vt220.  The Port directive in sshd_config is
+     substituted only in private staging.
+
+     installer.sshd.patch modifies private copies of stock .profile and
+     install.sub.  console-lock.sshd.patch adds the locked early profile path.
+     init.sshd.patch applies only to a private init.c copy.  patch(1) runs
+     non-interactively with zero fuzz.  Context drift fails before replacement.
+
+     Image layout:
+
+           embedded rdroot                 20 MiB
+           outer miniroot                  32768 x 512 bytes (16 MiB)
+
+     The outer image follows the stock amd64 miniroot layout.  It installs BIOS
+     and EFI boot files using private boot, mount, image, and vnd paths.
+
+     minirootXX_sshd.img stores compressed bsdsshd.gz as /bsd.  It does not
+     store uncompressed bsdsshd.rd.
+
+     Example amd64 artifact sizes:
+
+           boot                    87 KiB
+           bsdsshd.gz             9.0 MiB
+           bsdsshd.rd            27.2 MiB
+           bsdsshd.strip         26.7 MiB
+           minirootXX_sshd.img   16.0 MiB
+
+     These are observations, not fixed limits.  Console locking adds a private
+     static init and may increase the compressed kernel size.
+
+SSHD
+     sshd permits public-key authentication for root only.  Relevant policy:
+
+           AllowUsers root
+           PermitRootLogin prohibit-password
+           PubkeyAuthentication yes
+           AuthenticationMethods publickey
+           PasswordAuthentication no
+           KbdInteractiveAuthentication no
+           Compression no
+           PermitUserRC no
+           PrintMotd no
+           PrintLastLog no
+           DisableForwarding yes
+
+     The static PIE executables retain normal OpenBSD OpenSSH compiler and
+     linker protections.  Compression is disabled at build and run time.
+
+     RDSSHD_AUTHORIZED_KEYS is mandatory.  It may contain one or more public
+     keys.  Blank and comment lines are removed.  Every remaining line must
+     pass ssh-keygen(1) public-key validation.  The installed file has mode
+     0600; /root and /root/.ssh have mode 0700.
+
+     No host key is embedded by default.  During the build, ssh-keygen creates
+     obj/stage/ssh_host_ed25519_key.test and its public key solely to validate
+     the generated configuration with the newly built sshd -t.  The build
+     removes the test configuration and key pair after validation.  They never
+     enter the ramdisk.  An interrupted build may leave them below obj/stage;
+     the next staging pass or cleandir removes them.
+
+     On first boot, the ramdisk generates:
+
+           /etc/ssh/ssh_host_ed25519_key
+
+     Normal installer CGI fetches call feed_random before generation.  An
+     unreachable fetch delays sshd until the normal CGI timeout.
+
+     RDSSHD_HOST_KEY may name an Ed25519 host private key without a passphrase.
+     The build validates it and copies it to private staging with mode 0600.
+     The newly built sshd validates the generated configuration against this
+     staged key.  Build-host ssh configuration and keys are not read or
+     modified.
+
+     An embedded key is recoverable from private objects, kernels, images, and
+     release copies.  Key-bearing kernel and media artifacts have mode 0600.
+
+     sshd starts after donetconfig.  A live numeric PID greater than one in
+     /var/run/sshd.pid suppresses restart.  The ramdisk lacks a process
+     inspection utility.  This checks liveness, not executable identity.
+
+     install.sub creates /var/run/rdsshd.ready only after sshd starts.
+     Bootstrap profiles use this marker because installer exit status alone
+     does not establish readiness.
+
+CONSOLE AND INSTALLER
+     The initial profile invokes:
+
+           install -af /auto_install.conf
+
+     /auto_install.conf answers only the early network questions.  Static IPv4
+     configuration includes netmask and default route.  autoconf, dhcp, and
+     none omit them.
+
+     With RDSSHD_CONSOLE_LOCK=no, automatic setup begins after the normal menu
+     timeout unless a local operator selects another action.  install.sub exits
+     after starting sshd.  The profile then returns to the local ramdisk shell.
+
+     With RDSSHD_CONSOLE_LOCK=yes, a private static init replaces /sbin/init in
+     the enhanced ramdisk.  Makefile.init invokes the stock special/init
+     Makefile, supplies stock pathnames.h, and keeps patched source and objects
+     private.  The bootstrap child retains inherited /dev/null descriptors and
+     never acquires /dev/console as a controlling terminal.
+
+     The locked profile branches before terminal setup.  It retries automatic
+     network and sshd setup until /var/run/rdsshd.ready exists, then sleeps.
+     It never presents a menu or shell.  Init restarts it after exit.
+     RDSSHD_CONSOLE_LOCK=no uses stock init from instbin.
+
+     After ssh login, run:
+
+           install
+
+     This starts the interactive installer.  At network prompts, choose done to
+     retain the active configuration.  Reconfiguration can drop the ssh
+     session.
+
+CAVEATS
+     RDSSHD_CONSOLE_LOCK removes the interactive local installer userland.
+     Boot-loader and kernel consoles remain active.  A local operator can still
+     change early boot state, reset, halt, or deny remote access.
+
+     RDSSHD_KERNEL_STACK_PROTECTOR=no deliberately retains NO_PROPOLICE.  This
+     may reduce image size and weakens mitigation of kernel stack corruption.
+
+     Protect RDSSHD_HOST_KEY, the object tree, all key-bearing images, and
+     installed copies.  Use a distinct host key for each machine identity.
+
+     RDSSHD_FSSIZE is not auto-sized.  A value too small for the compressed
+     kernel and boot files causes miniroot assembly to fail.
+
+     Network variables become installer response-file answers.  The build does
+     not probe the interface or validate network reachability.  Incorrect
+     values can prevent ssh access.  A locked image then has no local installer
+     userland with which to repair the configuration.
+
+BUILD
+     Build on OpenBSD/amd64 with src.tar.gz and sys.tar.gz matching the
+     installed OpenBSD release.  Normal source-build prerequisites apply.  The
+     media target requires root privileges for vnconfig(8), mount operations,
+     device access, and file ownership.  Kernel compilation runs as BUILDUSER.
+
+     Apply the patch:
+
+           cd /usr/src
+           patch -p1 < /root/bsdsshd.rd.patch
+
+     The configured BSDOBJDIR root, normally /usr/obj, must already exist.
+     Build from the added directory:
+
+           cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
+           make -f Makefile.rdsshd obj
+           make -f Makefile.rdsshd rdsshd \
+               RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub
+
+     Run obj separately.  OpenBSD make selects .OBJDIR at startup and rejects a
+     combined obj and rdsshd invocation.  No top-level /usr/src make obj is
+     required.  The wrapper creates only the normal object links required by
+     stock instbin.  Custom components retain private paths.
+
+     Remove the local installer session:
+
+           make -f Makefile.rdsshd rdsshd \
+               RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub \
+               RDSSHD_CONSOLE_LOCK=yes
+
+     Retain NO_PROPOLICE:
+
+           make -f Makefile.rdsshd rdsshd \
+               RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub \
+               RDSSHD_KERNEL_STACK_PROTECTOR=no
+
+     Embed a stable host key:
+
+           ssh-keygen -q -t ed25519 -N '' \
+               -f /root/rdsshd_host_ed25519_key
+           make -f Makefile.rdsshd rdsshd \
+               RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub \
+               RDSSHD_HOST_KEY=/root/rdsshd_host_ed25519_key
+
+     Outputs:
+
+           /usr/src/distrib/amd64/ramdisk_cd/rdsshd/obj/bsdsshd.rd
+           /usr/src/distrib/amd64/ramdisk_cd/rdsshd/obj/minirootXX_sshd.img
+
+REBUILD AND CLEAN
+     For variable or key changes, repeat rdsshd.  Normal dependency rules reuse
+     current stock instbin components, static OpenSSH, private init, and the
+     selected kernel.  Staging, ramdisk, and media are regenerated.
+
+           make -f Makefile.rdsshd rdsshd \
+               RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub
+
+     After source, compiler, flag, or Makefile changes, clean both ownership
+     domains:
+
+           cd /usr/src/distrib/amd64/ramdisk_cd
+           make cleandir
+           cd /usr/src/distrib/special
+           make cleandir
+           cd /usr/src/lib
+           make cleandir
+
+           cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
+           make -f Makefile.rdsshd cleandir
+           make -f Makefile.rdsshd obj
+           make -f Makefile.rdsshd rdsshd \
+               RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub
+
+     A top-level cleandir may replace the three stock cleans.  The next rdsshd
+     build recreates required normal object directories.  The wrapper clean
+     never removes stock objects.
+
+     After a failed stock instbin trace link, clean ramdisk_cd before retrying.
+     Its instbin.map and reduced archives may be incomplete:
+
+           cd /usr/src/distrib/amd64/ramdisk_cd
+           make cleandir
+
+     After an interrupted miniroot build:
+
+           cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
+           make -f Makefile.rdsshd unconfig-rdsshd
+
+VARIABLES
+     RDSSHD_AUTHORIZED_KEYS
+             Required file containing one or more root authorized public keys.
+
+     RDSSHD_HOST_KEY
+             Optional Ed25519 host private key without a passphrase.  An empty
+             value generates a new host key at each boot.  Default: empty.
+
+     RDSSHD_CONSOLE_LOCK
+             yes selects private init and removes the local installer session.
+             no retains the local menu and shell.  Default: no.
+
+     RDSSHD_KERNEL_STACK_PROTECTOR
+             yes removes inherited NO_PROPOLICE.  no retains it.  Default: yes.
+
+     RDSSHD_PORT
+             sshd listen port.  The generated sshd_config must pass sshd -t.
+             Default: 22.
+
+     RDSSHD_AI_IF
+             Network interface.  Default: em0.
+
+     RDSSHD_AI_HOSTNAME
+             Hostname.  Default: rdinstall.
+
+     RDSSHD_AI_IPV4
+             IPv4 address or installer keyword.  Default: autoconf.
+
+     RDSSHD_AI_NETMASK
+             Static IPv4 netmask.  Default: 255.255.255.0.
+
+     RDSSHD_AI_ROUTE
+             Static IPv4 default route.  Default: none.
+
+     RDSSHD_AI_IPV6
+             IPv6 address or installer keyword.  Default: none.
+
+     RDSSHD_AI_DOMAIN
+             DNS domain.  Default: my.domain.
+
+     RDSSHD_AI_DNS
+             DNS nameservers.  Default: none.
+
+     RDSSHD_FSSIZE
+             Outer miniroot size in 512-byte blocks.  Default: 32768 (16 MiB).
+
+     RDSSHD_MRMAKEFSARGS
+             makefs(8) arguments for the embedded rdroot.  The default creates
+             a 20 MiB filesystem matching MINIROOTSIZE=40960.  An override must
+             fit the kernel reservation.
+
+     BSDOBJDIR
+             Normal OpenBSD object root used by stock instbin.  Default:
+             /usr/obj.
+
+     DESTDIR
+             Optional prefix for installed amd64 boot files under usr/mdec.
+             Default: empty.
+
+     RELEASEDIR
+             Enables install-rdsshd and names its destination directory.
+
+     Boolean values are case-insensitive.  Values other than yes and no are
+     rejected.
+
+USE
+     Prepare a non-interactive next boot from the running system.
+     No boot prompt or console access is assumed.
+
+     Whole-device image
+             minirootXX_sshd.img is a complete disk image.  Writing it to a
+             block device replaces the device's partition table and filesystems.
+             Verify the output device before writing it.  For example, on Linux:
+
+                   dd if=./miniroot79_sshd.img of=/dev/sda bs=512
+                   sync
+                   reboot
+
+             The running OS or storage stack may deny raw writes to the initial
+             sectors of the device backing its active root filesystem.  Use the
+             boot-loader method if this cannot be changed remotely.
+
+             The image boots in UEFI or BIOS/CSM mode with Secure Boot disabled.
+             Firmware must already select the target device.  The rdroot runs
+             from memory, so the installer can reuse that device as its target.
+
+     GRUB one-shot boot
+             Copy the uncompressed ramdisk kernel.  Do not rely on gzio:
+
+                   cp /path/to/bsdsshd.rd /boot/bsdsshd.rd
+
+             Add to /etc/grub.d/40_custom:
+
+                   menuentry "OpenBSD bsdsshd.rd" {
+                           insmod part_gpt
+                           insmod ext2
+                           insmod bsd
+                           # Replace UUID with the /boot filesystem UUID.
+                           search --no-floppy --fs-uuid --set=root UUID
+                           kopenbsd /bsdsshd.rd
+                   }
+
+             Set in /etc/default/grub:
+
+                   GRUB_DEFAULT=saved
+
+             Select a one-shot boot:
+
+                   update-grub
+                   grub-reboot "OpenBSD bsdsshd.rd"
+                   grub-editenv list
+                   sync
+                   reboot
+
+             This example was tested in BIOS/CSM mode.  UEFI is untested and
+             may lack display console output.  Secure Boot is unsupported.
+             part_gpt and ext2 match the tested /boot filesystem; change them
+             for the target layout.  The GRUB build must provide the bsd module
+             and kopenbsd command.  Other boot loaders are untested.
+
+     Network booting
+             The matching OpenBSD/amd64 pxeboot(8) can load the compressed
+             bsdsshd.rd over the network in place of bsd.rd.
+
+     Remote session
+             After the enhanced kernel boots, connect when the configured
+             address accepts ssh:
+
+                   ssh -i /path/to/private_key root@host.example
+                   install
+
+             Perform required storage preparation before running install.  At
+             network prompts, choose done to retain the active connection.
+
+             RDSSHD_CONSOLE_LOCK=yes presents no local installer menu or shell.
+
+REMOVE PATCH
+     Remove private outputs and the wrapper obj symlink before reversal:
+
+           cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
+           make -f Makefile.rdsshd prepare-unpatch-rdsshd
+           cd /usr/src
+           patch -R -p1 < /root/bsdsshd.rd.patch
+           rmdir /usr/obj/distrib/amd64/ramdisk_cd/rdsshd
+           rmdir /usr/src/distrib/amd64/ramdisk_cd/rdsshd
+
+     prepare-unpatch-rdsshd prints the recorded object path before removing the
+     symlink.  /usr/obj is the default; use the printed path when different.
+     patch(1) removes added files but leaves their empty parent directory.
+     rmdir refuses non-empty directories.
+
+FILES
+     /usr/src/distrib/amd64/ramdisk_cd/rdsshd
+             Added source directory and Makefile.rdsshd entry point.
+
+     obj/bsdsshd.rd
+             Uncompressed enhanced ramdisk kernel.
+
+     obj/bsdsshd.gz
+             Stripped and compressed ramdisk kernel installed as bsdsshd.rd by
+             install-rdsshd and as /bsd in the enhanced miniroot.
+
+     obj/minirootXX_sshd.img
+             Enhanced BIOS- and EFI-bootable miniroot image.
+
+     obj/vnd
+             Private ownership record for an attached vnd during media
+             assembly.  Removed after successful assembly or safe cleanup.
+
+DIAGNOSTICS
+     private object directory is not active
+             Run make -f Makefile.rdsshd obj as a separate invocation before
+             building.
+
+     normal OpenBSD object root does not exist
+             Create BSDOBJDIR with normal OpenBSD ownership and permissions.
+
+     normal ramdisk_cd object directory is not active
+             The stock obj target did not select an object directory distinct
+             from its source directory.  Check BSDOBJDIR and rerun the build.
+
+     stock ramdisk_cd instbin was not built
+             The stock instbin target did not produce its expected output.
+             Inspect the preceding failure.  Clean ramdisk_cd before retrying
+             after a failed trace link.
+
+     invalid public key on line N of RDSSHD_AUTHORIZED_KEYS
+             Replace the indicated line with a public key accepted by
+             ssh-keygen(1).
+
+     RDSSHD_HOST_KEY is not an unencrypted Ed25519 private key
+             Supply an Ed25519 host private key without a passphrase.
+
+     stale private vnd state
+             Run unconfig-rdsshd before rebuilding the miniroot.
+
+     refusing to detach vnd not owned by rdsshd
+             The recorded device no longer covers the enhanced image.  The
+             target leaves it attached for manual inspection.
+
+SEE ALSO
+     make(1), patch(1), ssh(1), ssh-keygen(1), softraid(4), vnd(4), disktab(5),
+     install.site(5), sshd_config(5), autoinstall(8), bioctl(8), boot(8),
+     boot_amd64(8), config(8), installboot(8), makefs(8), rdsetroot(8),
+     release(8), sshd(8), vnconfig(8)
+
+BSDSSHD.RD                      July 18, 2026                       BSDSSHD.RD
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/console-lock.sshd.patch
@@ -0,0 +1,18 @@
+--- dot.profile.tmp
++++ dot.profile.tmp
+@@ -87,6 +87,15 @@
+ 	[[ -x /sbin/dhcpleased ]] && /sbin/dhcpleased 2>/dev/null
+ 	[[ -x /sbin/slaacd ]] && /sbin/slaacd 2>/dev/null
+ 
++	while [[ ! -f /var/run/rdsshd.ready ]]; do
++		/install -af /auto_install.conf
++		sleep 1
++	done
++
++	while :; do
++		sleep 3600
++	done
++
+ 	# Set up some sane tty defaults.
+ 	echo 'erase ^?, werase ^W, kill ^U, intr ^C, status ^T'
+ 	stty newcrt werase ^W intr ^C kill ^U erase ^? status ^T
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/init.sshd.patch
@@ -0,0 +1,20 @@
+--- init.c.tmp
++++ init.c.tmp
+@@ -517,7 +517,6 @@
+ 		/*
+ 		 * Start the single user session.
+ 		 */
+-		setctty(_PATH_CONSOLE);
+ 
+ #ifdef SECURE
+ 		/*
+@@ -651,8 +650,7 @@
+ 		}
+ 	}
+ 
+-	runcom_mode = FASTBOOT;
+-	return runcom;
++	return single_user;
+ }
+ 
+ /*
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/installer.sshd.patch
@@ -0,0 +1,71 @@
+--- install.sub.tmp
++++ install.sub.tmp
+@@ -1377,7 +1377,10 @@
+ 		ask_until "$_q (name, lladdr, '?', or 'done')" \
+ 		    ${_p:-$( (get_ifs netboot; get_ifs) | sed q )}
+ 
+-		[[ $resp == done ]] && break
++		if [[ $resp == done ]]; then
++			NIFS=$(ls -1 /tmp/i/hostname.* 2>/dev/null | grep -c ^)
++			break
++		fi
+ 		[[ $resp == '?'  ]] && continue
+ 
+ 		# Quote $resp to prevent user from confusing isin() by
+@@ -3170,6 +3173,21 @@
+ 	echo "\nConfiguring the root disk $ROOTDISK...\n"
+ }
+ 
++rdsshd_start() {
++	local _pid
++	if [[ -s /var/run/sshd.pid ]]; then
++		_pid=$(</var/run/sshd.pid)
++		[[ $_pid == +([0-9]) ]] && (( _pid > 1 )) && \
++		    kill -0 "$_pid" 2>/dev/null && return 0
++	fi
++	if [[ ! -s /etc/ssh/ssh_host_ed25519_key ]]; then
++		/usr/bin/ssh-keygen -q -t ed25519 -N "" \
++		    -f /etc/ssh/ssh_host_ed25519_key || return 1
++	fi
++	/usr/sbin/sshd -t -f /etc/ssh/sshd_config || return 1
++	/usr/sbin/sshd -f /etc/ssh/sshd_config || return 1
++}
++
+ do_install() {
+ 	local _rootkey _rootpass
+ 
+@@ -3190,7 +3208,20 @@
+ 
+ 	# Configure the network.
+ 	donetconfig
++	if $AI && [[ $AI_RESPFILE == /auto_install.conf ]]; then
++		start_cgiinfo
++		wait_cgiinfo
++		rdsshd_start || err_exit "Could not start ramdisk sshd."
++		>/var/run/rdsshd.ready
++		cat <<__EOT
+ 
++ramdisk sshd is running.
++Connect as root with the matching key.
++Run: install
++__EOT
++		exit 0
++	fi
++
+ 	# Fetch list of mirror servers and installer choices from previous runs.
+ 	start_cgiinfo
+ 
+--- dot.profile.tmp
++++ dot.profile.tmp
+@@ -130,6 +130,11 @@
+ 		if $timeout; then
+ 			timeout=false
+ 			echo
++			if [[ -f /auto_install.conf ]]; then
++				/install -af /auto_install.conf
++				[[ -f /var/run/rdsshd.ready ]] && break
++				continue
++			fi
+ 			REPLY=a
+ 		else
+ 			# User has made a choice; stop the read timeout.
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/list.sshd
@@ -0,0 +1,35 @@
+#	$OpenBSD$
+
+# bsdsshd.rd overlay.
+MKDIR	usr/libexec
+MKDIR	etc/ssh
+MKDIR	root
+MKDIR	root/.ssh
+
+COPY	${OBJDIR}/../stage/sshd_config			etc/ssh/sshd_config
+SPECIAL test ! -s ${OBJDIR}/../stage/ssh_host_ed25519_key || install -c -m 600 -o root -g wheel ${OBJDIR}/../stage/ssh_host_ed25519_key etc/ssh/ssh_host_ed25519_key
+COPY	${OBJDIR}/../stage/auto_install.conf		auto_install.conf
+SCRIPT	${OBJDIR}/../stage/dot.profile			.profile
+SCRIPT	${OBJDIR}/../stage/install.sub			install.sub
+SPECIAL	chmod 755 install.sub
+
+COPY	${OBJDIR}/../stage/master.passwd		etc/master.passwd
+COPY	${OBJDIR}/../stage/group			etc/group
+SPECIAL	pwd_mkdb -p -d etc master.passwd; rm etc/master.passwd
+
+COPY	${CURDIR}/rdsshd/root.profile			root/.profile
+COPY	${OBJDIR}/../stage/authorized_keys		root/.ssh/authorized_keys
+SPECIAL	chmod 700 root root/.ssh; chmod 600 root/.ssh/authorized_keys
+
+SPECIAL	cd dev; sh MAKEDEV pty0 ptm
+
+COPY	${OBJDIR}/../stage/bin/sshd			usr/sbin/sshd
+SPECIAL	chmod 511 usr/sbin/sshd
+COPY	${OBJDIR}/../stage/bin/ssh-keygen		usr/bin/ssh-keygen
+SPECIAL	chmod 555 usr/bin/ssh-keygen
+COPY	${OBJDIR}/../stage/bin/sshd-session		usr/libexec/sshd-session
+SPECIAL	chmod 511 usr/libexec/sshd-session
+COPY	${OBJDIR}/../stage/bin/sshd-auth		usr/libexec/sshd-auth
+SPECIAL	chmod 511 usr/libexec/sshd-auth
+
+COPY	${CURDIR}/../../../etc/etc.amd64/login.conf	etc/login.conf
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/root.profile
@@ -0,0 +1,16 @@
+set +o sh
+export VNAME=$(sysctl -n kern.osrelease)
+export VERSION="${VNAME%.*}${VNAME#*.}"
+export ARCH=$(sysctl -n hw.machine)
+export OBSD="OpenBSD/$ARCH $VNAME"
+PATH=/sbin:/bin:/usr/bin:/usr/sbin:/
+export PATH
+TERM=vt220
+export TERM
+umask 022
+set -o emacs
+PS1='rd# '
+export PS1
+echo
+echo "ramdisk sshd is running."
+echo "Run install to continue."
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/sshd_config
@@ -0,0 +1,14 @@
+Port ${RDSSHD_PORT}
+HostKey /etc/ssh/ssh_host_ed25519_key
+AllowUsers root
+PermitRootLogin prohibit-password
+AuthorizedKeysFile .ssh/authorized_keys
+PubkeyAuthentication yes
+AuthenticationMethods publickey
+PasswordAuthentication no
+KbdInteractiveAuthentication no
+Compression no
+PermitUserRC no
+PrintMotd no
+PrintLastLog no
+DisableForwarding yes
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/RAMDISK_CD_SSHD
@@ -0,0 +1,12 @@
+#	$OpenBSD$
+
+include "arch/amd64/conf/RAMDISK_CD"
+
+# Keep config(8)'s option append pointer valid while removing the tail.
+option		RDSSHD_RDROOT
+rmoption	NO_PROPOLICE
+rmoption	MINIROOTSIZE
+option		MINIROOTSIZE=40960
+rmoption	RDSSHD_RDROOT
+
+pseudo-device	pty	16
--- /dev/null
+++ b/distrib/amd64/ramdisk_cd/rdsshd/RAMDISK_CD_SSHD_NO_PROPOLICE
@@ -0,0 +1,11 @@
+#	$OpenBSD$
+
+include "arch/amd64/conf/RAMDISK_CD"
+
+# Keep config(8)'s option append pointer valid while removing the tail.
+option		RDSSHD_RDROOT
+rmoption	MINIROOTSIZE
+option		MINIROOTSIZE=40960
+rmoption	RDSSHD_RDROOT
+
+pseudo-device	pty	16

SYNOPSIS

Run obj separately. Supply a file containing at least one authorized public key.

Build invocationraw
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd obj
make -f Makefile.rdsshd rdsshd \
    RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub

SOURCE LAYOUT

The patch adds one source directory containing eleven files.

Added filesraw
distrib/amd64/ramdisk_cd/rdsshd/Makefile.rdsshd
distrib/amd64/ramdisk_cd/rdsshd/Makefile.init
distrib/amd64/ramdisk_cd/rdsshd/README.rdsshd
distrib/amd64/ramdisk_cd/rdsshd/console-lock.sshd.patch
distrib/amd64/ramdisk_cd/rdsshd/init.sshd.patch
distrib/amd64/ramdisk_cd/rdsshd/installer.sshd.patch
distrib/amd64/ramdisk_cd/rdsshd/list.sshd
distrib/amd64/ramdisk_cd/rdsshd/root.profile
distrib/amd64/ramdisk_cd/rdsshd/sshd_config
distrib/amd64/ramdisk_cd/rdsshd/RAMDISK_CD_SSHD
distrib/amd64/ramdisk_cd/rdsshd/RAMDISK_CD_SSHD_NO_PROPOLICE

TARGETS

rdsshd
Builds both enhanced images.
install-rdsshd
Defined when RELEASEDIR is set. Installs compressed bsdsshd.rd and minirootXX_sshd.img.
unconfig-rdsshd
Unmounts the private mount point and detaches its recorded vnd after an interrupted miniroot build.
clean-rdsshd, clean, cleandir
Remove private outputs. Normal OpenBSD clean targets retain ownership of stock instbin objects.
prepare-unpatch-rdsshd
Cleans private outputs, prints the wrapper object path, and removes the source obj symlink.
Install release filesraw
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd install-rdsshd \
    RELEASEDIR=/path/to/release
Makefile.rdsshdEnhanced wrapper and targetsraw
#	$OpenBSD$

.include <bsd.own.mk>

RDSSHD_AUTHORIZED_KEYS?=
RDSSHD_HOST_KEY?=
RDSSHD_CONSOLE_LOCK?=	no
RDSSHD_KERNEL_STACK_PROTECTOR?=	yes
RDSSHD_PORT?=	22
RDSSHD_AI_IF?=	em0
RDSSHD_AI_HOSTNAME?=	rdinstall
RDSSHD_AI_IPV4?=	autoconf
RDSSHD_AI_NETMASK?=	255.255.255.0
RDSSHD_AI_ROUTE?=	none
RDSSHD_AI_IPV6?=	none
RDSSHD_AI_DOMAIN?=	my.domain
RDSSHD_AI_DNS?=	none
RDSSHD_FSSIZE?=	32768

RDSSHD_BASEDIR=	${.CURDIR}/..
RDSSHD_TOP=	${.CURDIR}/../../../..
RDSSHD_UTILS=	${RDSSHD_BASEDIR}/../../miniroot
RDSSHD_MTREE=	${RDSSHD_UTILS}/mtree.conf
RDSSHD_EFIBOOT=	${DESTDIR}/usr/mdec/BOOTX64.EFI \
		${DESTDIR}/usr/mdec/BOOTIA32.EFI
RDSSHD_MOUNT_ARGS_MSDOS=	-o-s

RDSSHD_RAMDISK=	RAMDISK_CD_SSHD
RDSSHD_KERNEL=	${.OBJDIR}/bsd
.if ${RDSSHD_KERNEL_STACK_PROTECTOR:L} == "no"
RDSSHD_RAMDISK=	RAMDISK_CD_SSHD_NO_PROPOLICE
RDSSHD_KERNEL=	${.OBJDIR}/bsd.no-propolice
.endif
RDSSHD_FS=	miniroot${OSrev}_sshd.img
RDSSHD_BUILDOBJDIR=	${.OBJDIR}/build
RDSSHD_KERNELOBJDIR=	${RDSSHD_BUILDOBJDIR}/kernel/${RDSSHD_RAMDISK}
RDSSHD_INSTBIN=	${RDSSHD_BUILDOBJDIR}/instbin
RDSSHD_STAGE=	${.OBJDIR}/stage
RDSSHD_RDOBJDIR=	${.OBJDIR}/rdobj
RDSSHD_SSHOBJDIR=	${.OBJDIR}/sshobj
RDSSHD_INITOBJDIR=	${.OBJDIR}/initobj
RDSSHD_INITSRCDIR=	${.OBJDIR}/initsrc
RDSSHD_MOUNT_POINT=	${.OBJDIR}/mnt
RDSSHD_VND=	${.OBJDIR}/vnd
RDSSHD_BOOT=	${.OBJDIR}/boot
RDSSHD_OBJCHECK=	${.OBJDIR}/.rdsshd-obj-ok
RDSSHD_MAKEFILE=	${.CURDIR}/Makefile.rdsshd
RDSSHD_INITMAKEFILE=	${.CURDIR}/Makefile.init
RDSSHD_KERNELCONF=	${.CURDIR}/${RDSSHD_RAMDISK}
RDSSHD_BASECONF=	${RDSSHD_TOP}/sys/arch/${MACHINE}/conf/RAMDISK_CD
RDSSHD_KERNELCONFDEPS=	${RDSSHD_BASECONF}
RDSSHD_LISTS=	${RDSSHD_BASEDIR}/list ${.CURDIR}/list.sshd \
		${RDSSHD_STAGE}/list.console
RDSSHD_INITSRC=	${RDSSHD_INITSRCDIR}/init.c
RDSSHD_INIT=	${RDSSHD_STAGE}/init
RDSSHD_INSTALLERPATCH=	${.CURDIR}/installer.sshd.patch
RDSSHD_CONSOLEPATCH=	${.CURDIR}/console-lock.sshd.patch
RDSSHD_INITPATCH=	${.CURDIR}/init.sshd.patch
RDSSHD_SSHDCONF=	${.CURDIR}/sshd_config
RDSSHD_MRMAKEFSARGS?=	-s 20m \
		-o rdroot,minfree=0,bsize=4096,fsize=512,density=4096
RDSSHD_BINS=	${RDSSHD_STAGE}/bin/sshd \
		${RDSSHD_STAGE}/bin/ssh-keygen \
		${RDSSHD_STAGE}/bin/sshd-session \
		${RDSSHD_STAGE}/bin/sshd-auth

RDSSHD_INITDEP=
.if ${RDSSHD_CONSOLE_LOCK:L} == "yes"
RDSSHD_INITDEP=	${RDSSHD_INIT}
.endif

.PHONY: rdsshd
rdsshd: bsdsshd.rd ${RDSSHD_FS}

.PHONY: rdsshd-objcheck
rdsshd-objcheck: ${RDSSHD_OBJCHECK}

${RDSSHD_OBJCHECK}:
	@if [ "${.OBJDIR}" = "${.CURDIR}" ]; then \
		echo "private object directory is not active;" >&2; \
		echo "run 'make -f Makefile.rdsshd obj' separately first" >&2; \
		exit 1; \
	fi
	touch $@

${RDSSHD_KERNEL}: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE} \
	    ${RDSSHD_KERNELCONF} ${RDSSHD_KERNELCONFDEPS}
	install -d -o ${BUILDUSER} -g ${WOBJGROUP} ${RDSSHD_KERNELOBJDIR}
	su ${BUILDUSER} -c \
	    'config -b ${RDSSHD_KERNELOBJDIR} -s ${RDSSHD_TOP}/sys \
	    ${RDSSHD_KERNELCONF} && cd ${RDSSHD_KERNELOBJDIR} && \
	    MAKEOBJDIR=${RDSSHD_KERNELOBJDIR} ${MAKE} clean && \
	    exec env MAKEOBJDIR=${RDSSHD_KERNELOBJDIR} ${MAKE} ${MFLAGS}'
	cp -p ${RDSSHD_KERNELOBJDIR}/bsd $@

bsdsshd.gz: bsdsshd.rd
	objcopy -g -x -R .comment -R .SUNW_ctf \
	    -K rd_root_size -K rd_root_image \
	    bsdsshd.rd bsdsshd.strip
	gzip -9cn bsdsshd.strip > bsdsshd.gz
.if !empty(RDSSHD_HOST_KEY)
	chmod 600 bsdsshd.strip $@
.endif

.PHONY: rdsshd-stock-deps rdsshd-instbin
rdsshd-stock-deps: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
	@_objroot=`cd ${RDSSHD_BASEDIR} && ${MAKE} -V BSDOBJDIR`; \
	if [ ! -d "$$_objroot" ]; then \
		echo "normal OpenBSD object root does not exist: $$_objroot" >&2; \
		echo "create it before building rdsshd" >&2; \
		exit 1; \
	fi
	cd ${RDSSHD_TOP}/lib && ${MAKE} obj
	cd ${RDSSHD_TOP}/distrib/special && ${MAKE} obj
	cd ${RDSSHD_BASEDIR} && ${MAKE} obj
	@_srcdir=`cd ${RDSSHD_BASEDIR} && pwd`; \
	_objdir=`cd ${RDSSHD_BASEDIR} && ${MAKE} -V .OBJDIR`; \
	if [ "$$_objdir" = "$$_srcdir" ] || [ ! -d "$$_objdir" ]; then \
		echo "normal ramdisk_cd object directory is not active" >&2; \
		exit 1; \
	fi
	cd ${RDSSHD_TOP}/distrib/special/libstubs && ${MAKE} ${MFLAGS}

rdsshd-instbin: rdsshd-stock-deps
	cd ${RDSSHD_BASEDIR} && ${MAKE} ${MFLAGS} instbin
	install -d ${RDSSHD_BUILDOBJDIR}
	@_objdir=`cd ${RDSSHD_BASEDIR} && ${MAKE} -V .OBJDIR`; \
	if [ ! -f "$$_objdir/instbin" ]; then \
		echo "stock ramdisk_cd instbin was not built" >&2; \
		exit 1; \
	fi; \
	cp -p "$$_objdir/instbin" ${RDSSHD_INSTBIN}

bsdsshd.rd: rdsshd-files ${RDSSHD_INITDEP} rdsshd-instbin \
	    ${RDSSHD_KERNEL}
	install -d ${RDSSHD_RDOBJDIR}
	rm -f ${RDSSHD_RDOBJDIR}/instbin \
	    ${RDSSHD_RDOBJDIR}/mr.fs \
	    ${RDSSHD_RDOBJDIR}/bsd.rd
	cp -p ${RDSSHD_INSTBIN} ${RDSSHD_RDOBJDIR}/instbin
	rm -rf ${RDSSHD_RDOBJDIR}/mr.fs.d
	install -d -o root -g wheel ${RDSSHD_RDOBJDIR}/mr.fs.d
	mtree -def ${RDSSHD_MTREE} -p ${RDSSHD_RDOBJDIR}/mr.fs.d -u
	CURDIR=${RDSSHD_BASEDIR} OBJDIR=${RDSSHD_RDOBJDIR} OSrev=${OSrev} \
	    TARGDIR=${RDSSHD_RDOBJDIR}/mr.fs.d UTILS=${RDSSHD_UTILS} \
	    RELEASEDIR=${RELEASEDIR} sh ${RDSSHD_UTILS}/runlist.sh \
	    ${RDSSHD_LISTS}
	rm ${RDSSHD_RDOBJDIR}/mr.fs.d/instbin
	makefs ${RDSSHD_MRMAKEFSARGS} ${RDSSHD_RDOBJDIR}/mr.fs \
	    ${RDSSHD_RDOBJDIR}/mr.fs.d
	cp -p ${RDSSHD_KERNEL} ${RDSSHD_RDOBJDIR}/bsd.rd
	rdsetroot ${RDSSHD_RDOBJDIR}/bsd.rd ${RDSSHD_RDOBJDIR}/mr.fs
	cp ${RDSSHD_RDOBJDIR}/bsd.rd $@
.if !empty(RDSSHD_HOST_KEY)
	chmod 600 ${RDSSHD_RDOBJDIR}/mr.fs \
	    ${RDSSHD_RDOBJDIR}/bsd.rd $@
.endif

${RDSSHD_FS}: bsdsshd.gz
	-umount -f ${RDSSHD_MOUNT_POINT} >/dev/null 2>&1
	@if [ -e ${RDSSHD_VND} ] && [ ! -s ${RDSSHD_VND} ]; then \
		rm -f ${RDSSHD_VND}; \
	fi
	@if [ -e ${RDSSHD_VND} ]; then \
		echo "stale private vnd state; run" \
		    "'make -f Makefile.rdsshd unconfig-rdsshd' first" >&2; \
		exit 1; \
	fi
	install -d ${RDSSHD_MOUNT_POINT}
	dd if=/dev/zero of=${RDSSHD_FS} bs=512 count=${RDSSHD_FSSIZE}
	vnconfig -v ${.OBJDIR}/${RDSSHD_FS} > ${RDSSHD_VND}
	fdisk -yi -l ${RDSSHD_FSSIZE} -b 960 -f ${DESTDIR}/usr/mdec/mbr \
	    `cat ${RDSSHD_VND}`
	echo '/ *' | disklabel -wAT- `cat ${RDSSHD_VND}`
	newfs -t msdos /dev/r`cat ${RDSSHD_VND}`i
	mount ${RDSSHD_MOUNT_ARGS_MSDOS} /dev/`cat ${RDSSHD_VND}`i \
	    ${RDSSHD_MOUNT_POINT}
	mkdir -p ${RDSSHD_MOUNT_POINT}/efi/boot
	cp ${RDSSHD_EFIBOOT} ${RDSSHD_MOUNT_POINT}/efi/boot
	umount ${RDSSHD_MOUNT_POINT}
	newfs -O 1 -m 0 -o space -i 524288 -c ${RDSSHD_FSSIZE} \
	    /dev/r`cat ${RDSSHD_VND}`a
	mount /dev/`cat ${RDSSHD_VND}`a ${RDSSHD_MOUNT_POINT}
	objcopy -S -R .comment ${DESTDIR}/usr/mdec/boot ${RDSSHD_BOOT}
	installboot -v -r ${RDSSHD_MOUNT_POINT} `cat ${RDSSHD_VND}` \
	    ${DESTDIR}/usr/mdec/biosboot ${RDSSHD_BOOT}
	install -c -m 555 -o root -g wheel bsdsshd.gz \
	    ${RDSSHD_MOUNT_POINT}/bsd
	df -i ${RDSSHD_MOUNT_POINT}
	umount ${RDSSHD_MOUNT_POINT}
	vnconfig -u `cat ${RDSSHD_VND}`
	rm -f ${RDSSHD_VND}
.if !empty(RDSSHD_HOST_KEY)
	chmod 600 $@
.endif

.PHONY: rdsshd-check rdsshd-files rdsshd-scripts
rdsshd-check:
	@if [ -z "${RDSSHD_AUTHORIZED_KEYS}" ]; then \
		echo "set RDSSHD_AUTHORIZED_KEYS to a public key file" >&2; \
		exit 1; \
	fi
	@case "${RDSSHD_CONSOLE_LOCK:L}" in \
	yes|no) ;; \
	*) echo "RDSSHD_CONSOLE_LOCK must be yes or no" >&2; exit 1;; \
	esac
	@case "${RDSSHD_KERNEL_STACK_PROTECTOR:L}" in \
	yes|no) ;; \
	*) echo "RDSSHD_KERNEL_STACK_PROTECTOR must be yes or no" >&2; exit 1;; \
	esac

${RDSSHD_INITSRC}: ${RDSSHD_OBJCHECK} ${RDSSHD_TOP}/sbin/init/init.c \
	    ${RDSSHD_INITPATCH}
	install -d ${RDSSHD_INITSRCDIR}
	rm -f ${RDSSHD_INITSRC}.tmp ${RDSSHD_INITSRC}.tmp.orig \
	    ${RDSSHD_INITSRC}.tmp.rej
	cp ${RDSSHD_TOP}/sbin/init/init.c ${RDSSHD_INITSRC}.tmp
	cd ${RDSSHD_INITSRCDIR} && \
	    patch -f -s -p0 -F0 < ${RDSSHD_INITPATCH}
	mv ${RDSSHD_INITSRC}.tmp ${RDSSHD_INITSRC}

${RDSSHD_INIT}: ${RDSSHD_INITSRC} \
	    ${RDSSHD_TOP}/sbin/init/pathnames.h ${RDSSHD_INITMAKEFILE} \
	    ${RDSSHD_BASEDIR}/../../special/init/Makefile \
	    ${RDSSHD_BASEDIR}/../../special/Makefile.inc
	install -d ${RDSSHD_INITOBJDIR} ${RDSSHD_STAGE}
	cd ${RDSSHD_BASEDIR}/../../special/init && \
	    MAKEOBJDIR=${RDSSHD_INITOBJDIR} ${MAKE} ${MFLAGS} \
	    -f ${RDSSHD_INITMAKEFILE} \
	    RDSSHD_INITSRCDIR=${RDSSHD_INITSRCDIR} init
	install -c -s ${RDSSHD_INITOBJDIR}/init $@

rdsshd-scripts: ${RDSSHD_OBJCHECK} ${RDSSHD_UTILS}/install.sub \
	    ${RDSSHD_UTILS}/dot.profile \
	    ${RDSSHD_INSTALLERPATCH} ${RDSSHD_CONSOLEPATCH}
	install -d ${RDSSHD_STAGE}
	rm -f ${RDSSHD_STAGE}/install.sub.tmp \
	    ${RDSSHD_STAGE}/install.sub.tmp.orig \
	    ${RDSSHD_STAGE}/install.sub.tmp.rej \
	    ${RDSSHD_STAGE}/dot.profile.tmp \
	    ${RDSSHD_STAGE}/dot.profile.tmp.orig \
	    ${RDSSHD_STAGE}/dot.profile.tmp.rej
	cp ${RDSSHD_UTILS}/install.sub ${RDSSHD_STAGE}/install.sub.tmp
	cp ${RDSSHD_UTILS}/dot.profile ${RDSSHD_STAGE}/dot.profile.tmp
	cd ${RDSSHD_STAGE} && \
	    patch -f -s -p0 -F0 < ${RDSSHD_INSTALLERPATCH}
.if ${RDSSHD_CONSOLE_LOCK:L} == "yes"
	cd ${RDSSHD_STAGE} && \
	    patch -f -s -p0 -F0 < ${RDSSHD_CONSOLEPATCH}
.endif
	chmod 755 ${RDSSHD_STAGE}/install.sub.tmp
	mv ${RDSSHD_STAGE}/install.sub.tmp ${RDSSHD_STAGE}/install.sub
	mv ${RDSSHD_STAGE}/dot.profile.tmp ${RDSSHD_STAGE}/dot.profile

rdsshd-files: rdsshd-check rdsshd-scripts ${RDSSHD_BINS} \
	    ${RDSSHD_SSHDCONF}
	install -d ${RDSSHD_STAGE}
	@if [ "${RDSSHD_CONSOLE_LOCK:L}" = yes ]; then \
		echo 'COPY ${RDSSHD_INIT} sbin/init'; \
	fi > ${RDSSHD_STAGE}/list.console
	sed '/^root:/s|:/bin/ksh$$|:/bin/sh|' \
	    ${RDSSHD_UTILS}/master.passwd > ${RDSSHD_STAGE}/master.passwd
	grep '^root:.*:/bin/sh$$' ${RDSSHD_STAGE}/master.passwd >/dev/null
	sed -e '/^[	 ]*$$/d' -e '/^[	 ]*#/d' \
	    < "${RDSSHD_AUTHORIZED_KEYS}" > ${RDSSHD_STAGE}/authorized_keys
	test -s ${RDSSHD_STAGE}/authorized_keys
	@_n=0; while IFS= read -r _key; do \
		_n=$$((_n + 1)); \
		if ! printf '%s\n' "$$_key" | \
		    ${RDSSHD_STAGE}/bin/ssh-keygen -l -f - >/dev/null 2>&1; then \
			echo "invalid public key on line $$_n of RDSSHD_AUTHORIZED_KEYS" >&2; \
			exit 1; \
		fi; \
	done < ${RDSSHD_STAGE}/authorized_keys
	grep '^sshd:' ${RDSSHD_STAGE}/master.passwd >/dev/null || \
	    grep '^sshd:' ${RDSSHD_TOP}/etc/master.passwd >> \
	    ${RDSSHD_STAGE}/master.passwd
	cp ${RDSSHD_UTILS}/group ${RDSSHD_STAGE}/group
	grep '^sshd:' ${RDSSHD_STAGE}/group >/dev/null || \
	    grep '^sshd:' ${RDSSHD_TOP}/etc/group >> ${RDSSHD_STAGE}/group
	{ \
	    printf 'System hostname = %s\n' '${RDSSHD_AI_HOSTNAME}'; \
	    printf 'Network interface to configure = %s\n' '${RDSSHD_AI_IF}'; \
	    printf 'IPv4 address for %s = %s\n' \
		'${RDSSHD_AI_IF}' '${RDSSHD_AI_IPV4}'; \
	    case '${RDSSHD_AI_IPV4}' in \
	    none|autoconf|dhcp) ;; \
	    *) printf 'Netmask for %s = %s\n' \
		'${RDSSHD_AI_IF}' '${RDSSHD_AI_NETMASK}'; \
	       printf 'Default IPv4 route = %s\n' '${RDSSHD_AI_ROUTE}' ;; \
	    esac; \
	    printf 'IPv6 address for %s = %s\n' \
		'${RDSSHD_AI_IF}' '${RDSSHD_AI_IPV6}'; \
	    printf 'Network interface to configure = done\n'; \
	    printf 'DNS domain name = %s\n' '${RDSSHD_AI_DOMAIN}'; \
	    printf 'DNS nameservers = %s\n' '${RDSSHD_AI_DNS}'; \
	} > ${RDSSHD_STAGE}/auto_install.conf
	sed 's|^Port .*|Port ${RDSSHD_PORT}|' ${RDSSHD_SSHDCONF} > \
	    ${RDSSHD_STAGE}/sshd_config
	rm -f ${RDSSHD_STAGE}/ssh_host_ed25519_key \
	    ${RDSSHD_STAGE}/ssh_host_ed25519_key.pub \
	    ${RDSSHD_STAGE}/ssh_host_ed25519_key.test \
	    ${RDSSHD_STAGE}/ssh_host_ed25519_key.test.pub \
	    ${RDSSHD_STAGE}/sshd_config.test
	@if [ -n "${RDSSHD_HOST_KEY}" ]; then \
		if ! ${RDSSHD_STAGE}/bin/ssh-keygen -y -P '' \
		    -f "${RDSSHD_HOST_KEY}" 2>/dev/null | grep -q '^ssh-ed25519 '; then \
			echo "RDSSHD_HOST_KEY is not an unencrypted Ed25519 private key" >&2; \
			exit 1; \
		fi; \
		install -c -m 600 "${RDSSHD_HOST_KEY}" \
		    ${RDSSHD_STAGE}/ssh_host_ed25519_key; \
	fi
	@_key=${RDSSHD_STAGE}/ssh_host_ed25519_key; \
	_testkey=${RDSSHD_STAGE}/ssh_host_ed25519_key.test; \
	_testconf=${RDSSHD_STAGE}/sshd_config.test; \
	if [[ ! -s $$_key ]]; then \
		_key=$$_testkey; \
		${RDSSHD_STAGE}/bin/ssh-keygen -q -t ed25519 -N '' \
		    -f $$_key || exit 1; \
	fi; \
	sed "s|^HostKey .*|HostKey $$_key|" \
	    ${RDSSHD_STAGE}/sshd_config > $$_testconf; \
	${RDSSHD_STAGE}/bin/sshd -t -f $$_testconf; \
	_status=$$?; \
	rm -f $$_testconf $$_testkey $$_testkey.pub; \
	exit $$_status
${RDSSHD_STAGE}/bin/sshd: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
	install -d ${RDSSHD_STAGE}/bin ${RDSSHD_SSHOBJDIR}/sshd
	cd ${RDSSHD_TOP}/usr.bin/ssh/sshd && \
	    MAKEOBJDIR=${RDSSHD_SSHOBJDIR}/sshd ${MAKE} ${MFLAGS} \
	    LDSTATIC="${STATIC}" ZLIB=no
	install -c -s ${RDSSHD_SSHOBJDIR}/sshd/sshd $@

${RDSSHD_STAGE}/bin/ssh-keygen: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
	install -d ${RDSSHD_STAGE}/bin ${RDSSHD_SSHOBJDIR}/ssh-keygen
	cd ${RDSSHD_TOP}/usr.bin/ssh/ssh-keygen && \
	    MAKEOBJDIR=${RDSSHD_SSHOBJDIR}/ssh-keygen ${MAKE} ${MFLAGS} \
	    LDSTATIC="${STATIC}" ZLIB=no
	install -c -s ${RDSSHD_SSHOBJDIR}/ssh-keygen/ssh-keygen $@

${RDSSHD_STAGE}/bin/sshd-session: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
	install -d ${RDSSHD_STAGE}/bin ${RDSSHD_SSHOBJDIR}/sshd-session
	cd ${RDSSHD_TOP}/usr.bin/ssh/sshd-session && \
	    MAKEOBJDIR=${RDSSHD_SSHOBJDIR}/sshd-session ${MAKE} ${MFLAGS} \
	    LDSTATIC="${STATIC}" ZLIB=no
	install -c -s ${RDSSHD_SSHOBJDIR}/sshd-session/sshd-session $@

${RDSSHD_STAGE}/bin/sshd-auth: ${RDSSHD_OBJCHECK} ${RDSSHD_MAKEFILE}
	install -d ${RDSSHD_STAGE}/bin ${RDSSHD_SSHOBJDIR}/sshd-auth
	cd ${RDSSHD_TOP}/usr.bin/ssh/sshd-auth && \
	    MAKEOBJDIR=${RDSSHD_SSHOBJDIR}/sshd-auth ${MAKE} ${MFLAGS} \
	    LDSTATIC="${STATIC}" ZLIB=no
	install -c -s ${RDSSHD_SSHOBJDIR}/sshd-auth/sshd-auth $@

.PHONY: unconfig-rdsshd
unconfig-rdsshd:
	-umount -f ${RDSSHD_MOUNT_POINT} >/dev/null 2>&1
	@if [ -e ${RDSSHD_VND} ] && [ ! -s ${RDSSHD_VND} ]; then \
		rm -f ${RDSSHD_VND}; \
	elif [ -f ${RDSSHD_VND} ]; then \
		_vnd=`cat ${RDSSHD_VND}`; \
		_unit=$${_vnd#vnd}; \
		if [ "vnd$$_unit" != "$$_vnd" ] || [ -z "$$_unit" ]; then \
			echo "invalid private vnd state: $$_vnd" >&2; exit 1; \
		fi; \
		case "$$_unit" in \
		*[!0-9]*) echo "invalid private vnd state: $$_vnd" >&2; exit 1;; \
		esac; \
		_info=`vnconfig -l "$$_vnd"` || exit 1; \
		case "$$_info" in \
		"$$_vnd: not in use") rm -f ${RDSSHD_VND} ;; \
		"$$_vnd: covering ${.OBJDIR}/${RDSSHD_FS} on "*) \
			vnconfig -u "$$_vnd" && rm -f ${RDSSHD_VND} ;; \
		*) echo "refusing to detach vnd not owned by rdsshd: $$_info" >&2; \
			exit 1 ;; \
		esac; \
	fi

.ifdef RELEASEDIR
.PHONY: install-rdsshd
install-rdsshd: bsdsshd.gz ${RDSSHD_FS}
	cp bsdsshd.gz ${RELEASEDIR}/bsdsshd.rd
	cp ${RDSSHD_FS} ${RELEASEDIR}
.if empty(RDSSHD_HOST_KEY)
	chmod a+r ${RELEASEDIR}/bsdsshd.rd
.else
	chmod 600 ${RELEASEDIR}/bsdsshd.rd ${RELEASEDIR}/${RDSSHD_FS}
.endif
.endif

.PHONY: clean-rdsshd clean cleandir
clean-rdsshd: unconfig-rdsshd
	rm -f bsdsshd.rd bsdsshd.gz bsdsshd.strip ${RDSSHD_FS} \
	    ${.OBJDIR}/bsd ${.OBJDIR}/bsd.no-propolice ${RDSSHD_BOOT} \
	    ${RDSSHD_OBJCHECK}
	rm -rf ${RDSSHD_BUILDOBJDIR} ${RDSSHD_STAGE} ${RDSSHD_RDOBJDIR} \
	    ${RDSSHD_SSHOBJDIR} ${RDSSHD_INITOBJDIR} ${RDSSHD_INITSRCDIR}
	-rmdir ${RDSSHD_MOUNT_POINT}

clean cleandir: clean-rdsshd

.PHONY: prepare-unpatch-rdsshd
prepare-unpatch-rdsshd: clean-rdsshd
	@if [ -L ${.CURDIR}/obj ]; then \
		_obj=`readlink ${.CURDIR}/obj`; \
		echo "empty private object directory may be removed: $$_obj"; \
		rm -f ${.CURDIR}/obj; \
	fi

.include <bsd.obj.mk>

BUILD MODEL

Private objects

Object ownershipraw
Private rdsshd paths:

obj/build/kernel       enhanced kernel objects
obj/build/instbin      copy of stock instbin
obj/sshobj             static OpenSSH objects
obj/initsrc            optional patched init source
obj/initobj            optional private init objects
obj/stage              generated and overlay files
obj/rdobj              ramdisk assembly
obj/bsd*               copied enhanced kernels
obj/boot               miniroot boot file
obj/mnt                miniroot mount point
obj/vnd                vnd ownership record

Normal OpenBSD paths used by stock instbin:

${BSDOBJDIR}/distrib/amd64/ramdisk_cd
${BSDOBJDIR}/distrib/special
${BSDOBJDIR}/lib

The enhanced kernel, OpenSSH programs, optional init, staging tree, ramdisk, media, mount point, and vnd record remain private. The kernel uses config(8) -b. OpenSSH uses explicit private MAKEOBJDIR paths. No directory below sys/arch/amd64/compile is added or used.

Stock instbin

Installer instbin is unmodified. The wrapper runs the normal obj targets for lib, distrib/special, and ramdisk_cd. It builds stock distrib/special/libstubs, invokes the stock instbin target, then copies the result to obj/build/instbin below the private object directory.

Component objects, reduced libraries, and crunchgen output retain their normal paths below ${BSDOBJDIR}. The wrapper does not rewrite generated crunchgen files. Normal OpenBSD clean targets own these stock objects. clean-rdsshd owns the private copy and enhanced outputs only. Enhanced and normal builds or cleans must not run concurrently when they share instbin objects.

No parent Makefile or SUBDIR list is changed. Normal top-level builds and cleans do not enter the rdsshd source directory.

Object checks and media state

BSDOBJDIR must exist with normal OpenBSD ownership and permissions. The default is /usr/obj, owned by build:wobj with mode 770. The build rejects source-directory fallback for both the wrapper and ramdisk_cd.

Normal and enhanced media use distinct boot files, image names, mount points, and vnd state. Cleanup detaches only the recorded vnd covering the absolute enhanced image path. A missing, malformed, or reused vnd is reported and retained.

KERNEL

RAMDISK_CD_SSHD includes stock RAMDISK_CD. It changes only the inherited stack-protection option, rdroot reservation, and pty count.

RAMDISK_CD_SSHDraw
#	$OpenBSD$

include "arch/amd64/conf/RAMDISK_CD"

# Keep config(8)'s option append pointer valid while removing the tail.
option		RDSSHD_RDROOT
rmoption	NO_PROPOLICE
rmoption	MINIROOTSIZE
option		MINIROOTSIZE=40960
rmoption	RDSSHD_RDROOT

pseudo-device	pty	16

Removing NO_PROPOLICE enables normal kernel stack protection. RDSSHD_KERNEL_STACK_PROTECTOR=no selects a separate overlay that retains NO_PROPOLICE.

RAMDISK_CD_SSHD_NO_PROPOLICEraw
#	$OpenBSD$

include "arch/amd64/conf/RAMDISK_CD"

# Keep config(8)'s option append pointer valid while removing the tail.
option		RDSSHD_RDROOT
rmoption	MINIROOTSIZE
option		MINIROOTSIZE=40960
rmoption	RDSSHD_RDROOT

pseudo-device	pty	16

MINIROOTSIZE=40960 reserves a 20 MiB rdroot. Sixteen ptys support ssh sessions. SMALL_KERNEL remains enabled.

The temporary RDSSHD_RDROOT option protects config(8)'s option-list append pointer while inherited tail entries are removed. It is absent from the final configuration.

RAMDISK

Embedded filesystem

Stock bsd.rd uses the host disktab entry rdrootb.

Stock rdrootraw
MRMAKEFSARGS=-o disklabel=rdrootb,minfree=0,density=4096

The enhanced target gives makefs(8) an explicit 20 MiB layout. It does not read or modify /etc/disktab.

Enhanced rdrootraw
RDSSHD_MRMAKEFSARGS?=	-s 20m \
		-o rdroot,minfree=0,bsize=4096,fsize=512,density=4096

Programs and files

Stock installer programs remain in instbin. OpenSSH is built through its normal Makefiles as separate static PIE executables with zlib disabled. The ssh(1) client is not included.

The separate executables are /usr/sbin/sshd, /usr/bin/ssh-keygen, /usr/libexec/sshd-session, and /usr/libexec/sshd-auth.

Ramdisk additionsraw
#	$OpenBSD$

# bsdsshd.rd overlay.
MKDIR	usr/libexec
MKDIR	etc/ssh
MKDIR	root
MKDIR	root/.ssh

COPY	${OBJDIR}/../stage/sshd_config			etc/ssh/sshd_config
SPECIAL test ! -s ${OBJDIR}/../stage/ssh_host_ed25519_key || install -c -m 600 -o root -g wheel ${OBJDIR}/../stage/ssh_host_ed25519_key etc/ssh/ssh_host_ed25519_key
COPY	${OBJDIR}/../stage/auto_install.conf		auto_install.conf
SCRIPT	${OBJDIR}/../stage/dot.profile			.profile
SCRIPT	${OBJDIR}/../stage/install.sub			install.sub
SPECIAL	chmod 755 install.sub

COPY	${OBJDIR}/../stage/master.passwd		etc/master.passwd
COPY	${OBJDIR}/../stage/group			etc/group
SPECIAL	pwd_mkdb -p -d etc master.passwd; rm etc/master.passwd

COPY	${CURDIR}/rdsshd/root.profile			root/.profile
COPY	${OBJDIR}/../stage/authorized_keys		root/.ssh/authorized_keys
SPECIAL	chmod 700 root root/.ssh; chmod 600 root/.ssh/authorized_keys

SPECIAL	cd dev; sh MAKEDEV pty0 ptm

COPY	${OBJDIR}/../stage/bin/sshd			usr/sbin/sshd
SPECIAL	chmod 511 usr/sbin/sshd
COPY	${OBJDIR}/../stage/bin/ssh-keygen		usr/bin/ssh-keygen
SPECIAL	chmod 555 usr/bin/ssh-keygen
COPY	${OBJDIR}/../stage/bin/sshd-session		usr/libexec/sshd-session
SPECIAL	chmod 511 usr/libexec/sshd-session
COPY	${OBJDIR}/../stage/bin/sshd-auth		usr/libexec/sshd-auth
SPECIAL	chmod 511 usr/libexec/sshd-auth

COPY	${CURDIR}/../../../etc/etc.amd64/login.conf	etc/login.conf
Ramdisk additions1.3 KiBraw

The overlay adds sshd configuration, authorized keys, the root profile, login classes, early network answers, the sshd account, and pty devices. Root retains stock instbin's -sh argv link. The root profile leaves sh mode, sets the installer environment, and selects TERM=vt220. The sshd Port directive is substituted only in private staging.

Root profileraw
set +o sh
export VNAME=$(sysctl -n kern.osrelease)
export VERSION="${VNAME%.*}${VNAME#*.}"
export ARCH=$(sysctl -n hw.machine)
export OBSD="OpenBSD/$ARCH $VNAME"
PATH=/sbin:/bin:/usr/bin:/usr/sbin:/
export PATH
TERM=vt220
export TERM
umask 022
set -o emacs
PS1='rd# '
export PS1
echo
echo "ramdisk sshd is running."
echo "Run install to continue."

Overlay patches

Each patch applies only to a private copy of a stock file. patch(1) runs non-interactively with zero fuzz. Context drift fails before replacement.

Installer integrationraw
--- install.sub.tmp
+++ install.sub.tmp
@@ -1377,7 +1377,10 @@
 		ask_until "$_q (name, lladdr, '?', or 'done')" \
 		    ${_p:-$( (get_ifs netboot; get_ifs) | sed q )}
 
-		[[ $resp == done ]] && break
+		if [[ $resp == done ]]; then
+			NIFS=$(ls -1 /tmp/i/hostname.* 2>/dev/null | grep -c ^)
+			break
+		fi
 		[[ $resp == '?'  ]] && continue
 
 		# Quote $resp to prevent user from confusing isin() by
@@ -3170,6 +3173,21 @@
 	echo "\nConfiguring the root disk $ROOTDISK...\n"
 }
 
+rdsshd_start() {
+	local _pid
+	if [[ -s /var/run/sshd.pid ]]; then
+		_pid=$(</var/run/sshd.pid)
+		[[ $_pid == +([0-9]) ]] && (( _pid > 1 )) && \
+		    kill -0 "$_pid" 2>/dev/null && return 0
+	fi
+	if [[ ! -s /etc/ssh/ssh_host_ed25519_key ]]; then
+		/usr/bin/ssh-keygen -q -t ed25519 -N "" \
+		    -f /etc/ssh/ssh_host_ed25519_key || return 1
+	fi
+	/usr/sbin/sshd -t -f /etc/ssh/sshd_config || return 1
+	/usr/sbin/sshd -f /etc/ssh/sshd_config || return 1
+}
+
 do_install() {
 	local _rootkey _rootpass
 
@@ -3190,7 +3208,20 @@
 
 	# Configure the network.
 	donetconfig
+	if $AI && [[ $AI_RESPFILE == /auto_install.conf ]]; then
+		start_cgiinfo
+		wait_cgiinfo
+		rdsshd_start || err_exit "Could not start ramdisk sshd."
+		>/var/run/rdsshd.ready
+		cat <<__EOT
 
+ramdisk sshd is running.
+Connect as root with the matching key.
+Run: install
+__EOT
+		exit 0
+	fi
+
 	# Fetch list of mirror servers and installer choices from previous runs.
 	start_cgiinfo
 
--- dot.profile.tmp
+++ dot.profile.tmp
@@ -130,6 +130,11 @@
 		if $timeout; then
 			timeout=false
 			echo
+			if [[ -f /auto_install.conf ]]; then
+				/install -af /auto_install.conf
+				[[ -f /var/run/rdsshd.ready ]] && break
+				continue
+			fi
 			REPLY=a
 		else
 			# User has made a choice; stop the read timeout.
Installer integration1.7 KiBraw
Locked profile pathraw
--- dot.profile.tmp
+++ dot.profile.tmp
@@ -87,6 +87,15 @@
 	[[ -x /sbin/dhcpleased ]] && /sbin/dhcpleased 2>/dev/null
 	[[ -x /sbin/slaacd ]] && /sbin/slaacd 2>/dev/null
 
+	while [[ ! -f /var/run/rdsshd.ready ]]; do
+		/install -af /auto_install.conf
+		sleep 1
+	done
+
+	while :; do
+		sleep 3600
+	done
+
 	# Set up some sane tty defaults.
 	echo 'erase ^?, werase ^W, kill ^U, intr ^C, status ^T'
 	stty newcrt werase ^W intr ^C kill ^U erase ^? status ^T
Private init behaviorraw
--- init.c.tmp
+++ init.c.tmp
@@ -517,7 +517,6 @@
 		/*
 		 * Start the single user session.
 		 */
-		setctty(_PATH_CONSOLE);
 
 #ifdef SECURE
 		/*
@@ -651,8 +650,7 @@
 		}
 	}
 
-	runcom_mode = FASTBOOT;
-	return runcom;
+	return single_user;
 }
 
 /*

Miniroot media

The outer image follows the stock amd64 BIOS and EFI miniroot layout. It uses private boot, mount, image, and vnd paths. The default size is 32768 512-byte blocks, or 16 MiB.

BIOS and EFI media assemblyraw
	dd if=/dev/zero of=${RDSSHD_FS} bs=512 count=${RDSSHD_FSSIZE}
	vnconfig -v ${.OBJDIR}/${RDSSHD_FS} > ${RDSSHD_VND}
	fdisk -yi -l ${RDSSHD_FSSIZE} -b 960 -f ${DESTDIR}/usr/mdec/mbr \
	    `cat ${RDSSHD_VND}`
	echo '/ *' | disklabel -wAT- `cat ${RDSSHD_VND}`
	newfs -t msdos /dev/r`cat ${RDSSHD_VND}`i
	mount ${RDSSHD_MOUNT_ARGS_MSDOS} /dev/`cat ${RDSSHD_VND}`i \
	    ${RDSSHD_MOUNT_POINT}
	mkdir -p ${RDSSHD_MOUNT_POINT}/efi/boot
	cp ${RDSSHD_EFIBOOT} ${RDSSHD_MOUNT_POINT}/efi/boot
	umount ${RDSSHD_MOUNT_POINT}
	newfs -O 1 -m 0 -o space -i 524288 -c ${RDSSHD_FSSIZE} \
	    /dev/r`cat ${RDSSHD_VND}`a
	mount /dev/`cat ${RDSSHD_VND}`a ${RDSSHD_MOUNT_POINT}
	objcopy -S -R .comment ${DESTDIR}/usr/mdec/boot ${RDSSHD_BOOT}
	installboot -v -r ${RDSSHD_MOUNT_POINT} `cat ${RDSSHD_VND}` \
	    ${DESTDIR}/usr/mdec/biosboot ${RDSSHD_BOOT}
	install -c -m 555 -o root -g wheel bsdsshd.gz \
	    ${RDSSHD_MOUNT_POINT}/bsd

The miniroot stores compressed bsdsshd.gz as /bsd. It does not store uncompressed bsdsshd.rd.

Observed sizes

These amd64 sizes are observations from a default build, not fixed limits. Console locking may increase the compressed kernel size.

Example artifactsraw
boot                    87 KiB
bsdsshd.gz             9.0 MiB
bsdsshd.rd            27.2 MiB
bsdsshd.strip         26.7 MiB
minirootXX_sshd.img   16.0 MiB

SSHD

Authentication policy

sshd permits root public-key authentication only. Forwarding, user rc, interactive authentication, passwords, and compression are disabled. Static PIE executables retain normal OpenBSD OpenSSH compiler and linker protections.

sshd_configraw
Port ${RDSSHD_PORT}
HostKey /etc/ssh/ssh_host_ed25519_key
AllowUsers root
PermitRootLogin prohibit-password
AuthorizedKeysFile .ssh/authorized_keys
PubkeyAuthentication yes
AuthenticationMethods publickey
PasswordAuthentication no
KbdInteractiveAuthentication no
Compression no
PermitUserRC no
PrintMotd no
PrintLastLog no
DisableForwarding yes

Authorized keys

RDSSHD_AUTHORIZED_KEYS is mandatory. It may contain one or more public keys. Blank and comment lines are removed. Every retained line must pass ssh-keygen(1) public-key validation.

The installed file has mode 0600. /root and /root/.ssh have mode 0700.

authorized_keys validationraw
	sed -e '/^[	 ]*$$/d' -e '/^[	 ]*#/d' \
	    < "${RDSSHD_AUTHORIZED_KEYS}" > ${RDSSHD_STAGE}/authorized_keys
	test -s ${RDSSHD_STAGE}/authorized_keys
	@_n=0; while IFS= read -r _key; do \
		_n=$$((_n + 1)); \
		if ! printf '%s\n' "$$_key" | \
		    ${RDSSHD_STAGE}/bin/ssh-keygen -l -f - >/dev/null 2>&1; then \
			echo "invalid public key on line $$_n of RDSSHD_AUTHORIZED_KEYS" >&2; \
			exit 1; \
		fi; \
	done < ${RDSSHD_STAGE}/authorized_keys

Build-time configuration test

If RDSSHD_HOST_KEY is empty, the build generates obj/stage/ssh_host_ed25519_key.test and its public key. These files exist only to give the newly built sshd -t a host key while it validates the generated configuration.

The build removes the test configuration and key pair after validation. They never enter the ramdisk. An interrupted build may leave them below obj/stage; the next staging pass or cleandir removes them.

Host-key and sshd_config validationraw
	rm -f ${RDSSHD_STAGE}/ssh_host_ed25519_key \
	    ${RDSSHD_STAGE}/ssh_host_ed25519_key.pub \
	    ${RDSSHD_STAGE}/ssh_host_ed25519_key.test \
	    ${RDSSHD_STAGE}/ssh_host_ed25519_key.test.pub \
	    ${RDSSHD_STAGE}/sshd_config.test
	@if [ -n "${RDSSHD_HOST_KEY}" ]; then \
		if ! ${RDSSHD_STAGE}/bin/ssh-keygen -y -P '' \
		    -f "${RDSSHD_HOST_KEY}" 2>/dev/null | grep -q '^ssh-ed25519 '; then \
			echo "RDSSHD_HOST_KEY is not an unencrypted Ed25519 private key" >&2; \
			exit 1; \
		fi; \
		install -c -m 600 "${RDSSHD_HOST_KEY}" \
		    ${RDSSHD_STAGE}/ssh_host_ed25519_key; \
	fi
	@_key=${RDSSHD_STAGE}/ssh_host_ed25519_key; \
	_testkey=${RDSSHD_STAGE}/ssh_host_ed25519_key.test; \
	_testconf=${RDSSHD_STAGE}/sshd_config.test; \
	if [[ ! -s $$_key ]]; then \
		_key=$$_testkey; \
		${RDSSHD_STAGE}/bin/ssh-keygen -q -t ed25519 -N '' \
		    -f $$_key || exit 1; \
	fi; \
	sed "s|^HostKey .*|HostKey $$_key|" \
	    ${RDSSHD_STAGE}/sshd_config > $$_testconf; \
	${RDSSHD_STAGE}/bin/sshd -t -f $$_testconf; \
	_status=$$?; \
	rm -f $$_testconf $$_testkey $$_testkey.pub; \
	exit $$_status
Host-key and sshd_config validation1.1 KiBraw

Boot-generated host key

No host key is embedded by default. First boot generates /etc/ssh/ssh_host_ed25519_key. Normal installer CGI fetches call feed_random before generation. An unreachable fetch delays sshd until the normal CGI timeout.

Embedded host key

RDSSHD_HOST_KEY may name an Ed25519 host private key without a passphrase. The build validates it and copies it to private staging with mode 0600. The newly built sshd validates the generated configuration against that staged key. Build-host ssh configuration and keys are not read or modified.

An embedded key is recoverable from private objects, kernels, images, and release copies. Key-bearing kernel and media artifacts use mode 0600.

Startup

sshd starts after donetconfig. A live numeric PID greater than one in /var/run/sshd.pid suppresses restart. The ramdisk has no process-inspection utility, so this checks liveness only.

install.sub creates /var/run/rdsshd.ready only after sshd starts. Profiles use this marker because installer exit status does not establish readiness.

sshd startupraw
rdsshd_start() {
	local _pid
	if [[ -s /var/run/sshd.pid ]]; then
		_pid=$(</var/run/sshd.pid)
		[[ $_pid == +([0-9]) ]] && (( _pid > 1 )) && \
		    kill -0 "$_pid" 2>/dev/null && return 0
	fi
	if [[ ! -s /etc/ssh/ssh_host_ed25519_key ]]; then
		/usr/bin/ssh-keygen -q -t ed25519 -N "" \
		    -f /etc/ssh/ssh_host_ed25519_key || return 1
	fi
	/usr/sbin/sshd -t -f /etc/ssh/sshd_config || return 1
	/usr/sbin/sshd -f /etc/ssh/sshd_config || return 1
}

donetconfig
if $AI && [[ $AI_RESPFILE == /auto_install.conf ]]; then
	start_cgiinfo
	wait_cgiinfo
	rdsshd_start || err_exit "Could not start ramdisk sshd."
	>/var/run/rdsshd.ready
	cat <<__EOT

ramdisk sshd is running.
Connect as root with the matching key.
Run: install
__EOT
	exit 0
fi

CONSOLE AND INSTALLER

Network bootstrap

The initial profile invokes install -af with early network answers. Static IPv4 configuration includes a netmask and default route. autoconf, dhcp, and none omit them.

Static IPv4 response fileDocumentation addresses use RFC 5737 spaceraw
System hostname = rdinstall
Network interface to configure = em0
IPv4 address for em0 = 192.0.2.10
Netmask for em0 = 255.255.255.0
Default IPv4 route = 192.0.2.1
IPv6 address for em0 = none
Network interface to configure = done
DNS domain name = example.com
DNS nameservers = 192.0.2.53

Default console

Automatic setup starts after the normal installer-menu timeout unless a local operator selects another action. The profile returns to the local ramdisk shell after sshd starts.

Default profile pathraw
if [[ -f /auto_install.conf ]]; then
	/install -af /auto_install.conf
	[[ -f /var/run/rdsshd.ready ]] && break
	continue
fi

Locked console

With RDSSHD_CONSOLE_LOCK=yes, a private static init replaces /sbin/init only in the enhanced ramdisk. It is built through the stock distrib/special/init Makefile using the dedicated wrapper and stock pathnames.h. Patched source and objects stay private.

Private init wrapperraw
#	$OpenBSD$

.PATH: ${RDSSHD_INITSRCDIR}
CPPFLAGS+=	-I${.CURDIR}/../../../sbin/init
.include "${.CURDIR}/Makefile"

The bootstrap child retains inherited /dev/null descriptors. It never acquires /dev/console as a controlling terminal. The profile branches before terminal setup, retries setup until the ready marker exists, then sleeps. It presents no menu or shell. Init restarts it after exit. The default uses stock init from instbin.

Locked profile pathraw
while [[ ! -f /var/run/rdsshd.ready ]]; do
	/install -af /auto_install.conf
	sleep 1
done

while :; do
	sleep 3600
done

Remote installer

After ssh login, run install. Complete storage preparation first. At network prompts, select done to retain the active configuration. Reconfiguration can drop the ssh session.

CAVEATS

Console locking removes the interactive local installer userland. The boot-loader and kernel consoles remain active. A local operator can still change early boot state, reset, halt, or deny remote access.

RDSSHD_KERNEL_STACK_PROTECTOR=no deliberately retains NO_PROPOLICE. This may reduce image size and weakens mitigation of kernel stack corruption.

Protect an embedded host key, the object tree, every key-bearing image, and installed copies. Use a distinct host key for each machine identity.

RDSSHD_FSSIZE is not auto-sized. A value too small for the compressed kernel and boot files causes miniroot assembly to fail.

Network variables become installer response-file answers. The build does not probe the interface or validate reachability. Incorrect values can prevent ssh access. A locked image then has no local installer userland with which to repair the configuration.

BUILD

Prerequisites and base build

Build on OpenBSD/amd64 with src.tar.gz and sys.tar.gz matching the installed OpenBSD release. Normal source-build prerequisites apply. The media target requires root for vnd, mount, device, and ownership operations. Kernel compilation runs as BUILDUSER.

Apply the patch below /usr/src. The configured BSDOBJDIR root, normally /usr/obj, must already exist.

Base buildraw
cd /usr/src
patch -p1 < /root/bsdsshd.rd.patch

cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd obj
make -f Makefile.rdsshd rdsshd \
    RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub

Run obj separately. OpenBSD make selects .OBJDIR at startup and rejects a combined obj rdsshd invocation. No top-level /usr/src make obj is required. The wrapper creates only the normal object links required by stock instbin. Custom components retain private paths.

Static network

Supply netmask and route with a static IPv4 address. The documentation addresses below use the reserved 192.0.2.0/24 range.

Static IPv4 buildraw
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd rdsshd \
    RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub \
    RDSSHD_AI_IF=em0 \
    RDSSHD_AI_HOSTNAME=rdinstall \
    RDSSHD_AI_IPV4=192.0.2.10 \
    RDSSHD_AI_NETMASK=255.255.255.0 \
    RDSSHD_AI_ROUTE=192.0.2.1 \
    RDSSHD_AI_IPV6=none \
    RDSSHD_AI_DOMAIN=example.com \
    RDSSHD_AI_DNS=192.0.2.53

Console lock

Remove local installer sessionraw
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd rdsshd \
    RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub \
    RDSSHD_CONSOLE_LOCK=yes

Kernel without stack protection

Retain NO_PROPOLICEraw
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd rdsshd \
    RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub \
    RDSSHD_KERNEL_STACK_PROTECTOR=no

Embedded host key

Generate and embed host keyraw
ssh-keygen -q -t ed25519 -N '' -f /root/rdsshd_host_ed25519_key
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd rdsshd \
    RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub \
    RDSSHD_HOST_KEY=/root/rdsshd_host_ed25519_key

Use a distinct host key per machine identity. Protect its source, object tree, and all resulting images. Derive its public-key fingerprint before deployment and compare it when establishing host trust.

Host-key fingerprintraw
ssh-keygen -y -f /root/rdsshd_host_ed25519_key |
    ssh-keygen -lf -

Outputs

Primary build outputsraw
/usr/src/distrib/amd64/ramdisk_cd/rdsshd/obj/bsdsshd.rd
/usr/src/distrib/amd64/ramdisk_cd/rdsshd/obj/minirootXX_sshd.img

REBUILD AND CLEAN

Variable or key changes

Repeat rdsshd. Normal dependency rules reuse current stock instbin components, OpenSSH, private init, and the selected kernel. Staging, ramdisk, and media are regenerated.

Rebuild variables or keysraw
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd rdsshd \
    RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub

Source or toolchain changes

Clean both ownership domains after source, compiler, flag, or Makefile changes. A top-level cleandir may replace the three stock cleans. The next enhanced build recreates required normal object directories. The wrapper clean never removes stock objects.

Rebuild changed sourcesraw
cd /usr/src/distrib/amd64/ramdisk_cd
make cleandir
cd /usr/src/distrib/special
make cleandir
cd /usr/src/lib
make cleandir

cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd cleandir
make -f Makefile.rdsshd obj
make -f Makefile.rdsshd rdsshd \
    RDSSHD_AUTHORIZED_KEYS=/root/id_ed25519.pub

Clean ramdisk_cd before retrying. Its instbin.map and reduced archives may be incomplete.

Clean stock instbin stateraw
cd /usr/src/distrib/amd64/ramdisk_cd
make cleandir

Interrupted media build

Release only the private mount and the vnd recorded as covering the enhanced image.

Release private media stateraw
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd unconfig-rdsshd

VARIABLES

RDSSHD_AUTHORIZED_KEYS
Required file containing one or more root authorized public keys.
RDSSHD_HOST_KEY
Optional Ed25519 host private key without a passphrase. An empty value generates a key at each boot. Default: empty.
RDSSHD_CONSOLE_LOCK
yes removes the local installer session. no retains the menu and shell. Default: no.
RDSSHD_KERNEL_STACK_PROTECTOR
yes removes inherited NO_PROPOLICE. no retains it. Default: yes.
RDSSHD_PORT
sshd port. The generated configuration must pass sshd -t. Default: 22.
RDSSHD_AI_IF
Network interface. Default: em0.
RDSSHD_AI_HOSTNAME
Hostname. Default: rdinstall.
RDSSHD_AI_IPV4
IPv4 address or installer keyword. Default: autoconf.
RDSSHD_AI_NETMASK
Static IPv4 netmask. Default: 255.255.255.0.
RDSSHD_AI_ROUTE
Static IPv4 default route. Default: none.
RDSSHD_AI_IPV6
IPv6 address or installer keyword. Default: none.
RDSSHD_AI_DOMAIN
DNS domain. Default: my.domain.
RDSSHD_AI_DNS
DNS nameservers. Default: none.
RDSSHD_FSSIZE
Outer miniroot size in 512-byte blocks. Default: 32768 (16 MiB).
RDSSHD_MRMAKEFSARGS
Embedded-rdroot makefs(8) arguments. The default creates a 20 MiB filesystem matching MINIROOTSIZE=40960. An override must fit the kernel reservation.
BSDOBJDIR
Normal object root used by stock instbin. Default: /usr/obj.
DESTDIR
Optional prefix for installed amd64 boot files below usr/mdec. Default: empty.
RELEASEDIR
Enables install-rdsshd and names its destination.

Boolean values are case-insensitive. Values other than yes and no are rejected.

Defaults and validationraw
RDSSHD_AUTHORIZED_KEYS?=
RDSSHD_HOST_KEY?=
RDSSHD_CONSOLE_LOCK?=	no
RDSSHD_KERNEL_STACK_PROTECTOR?=	yes
RDSSHD_PORT?=	22
RDSSHD_AI_IF?=	em0
RDSSHD_AI_HOSTNAME?=	rdinstall
RDSSHD_AI_IPV4?=	autoconf
RDSSHD_AI_NETMASK?=	255.255.255.0
RDSSHD_AI_ROUTE?=	none
RDSSHD_AI_IPV6?=	none
RDSSHD_AI_DOMAIN?=	my.domain
RDSSHD_AI_DNS?=	none
RDSSHD_FSSIZE?=	32768
RDSSHD_MRMAKEFSARGS?=	-s 20m \
		-o rdroot,minfree=0,bsize=4096,fsize=512,density=4096

rdsshd-check:
	@if [ -z "${RDSSHD_AUTHORIZED_KEYS}" ]; then \
		echo "set RDSSHD_AUTHORIZED_KEYS to a public key file" >&2; \
		exit 1; \
	fi
	@case "${RDSSHD_CONSOLE_LOCK:L}" in \
	yes|no) ;; \
	*) echo "RDSSHD_CONSOLE_LOCK must be yes or no" >&2; exit 1;; \
	esac
	@case "${RDSSHD_KERNEL_STACK_PROTECTOR:L}" in \
	yes|no) ;; \
	*) echo "RDSSHD_KERNEL_STACK_PROTECTOR must be yes or no" >&2; exit 1;; \
	esac
Defaults and validation846 Braw

USE

Prepare a non-interactive next boot from the running system. No boot prompt or console access is assumed.

Whole-device image

minirootXX_sshd.img is a complete disk image. Writing it to a block device replaces the device's partition table and filesystems. Verify the output device before writing it.

Write the miniroot from Linuxraw
# Linux example. This destroys the existing contents of /dev/sda.
dd if=./miniroot79_sshd.img of=/dev/sda bs=512
sync
reboot

The running OS or storage stack may deny raw writes to the initial sectors of the device backing its active root filesystem. Use the boot-loader method if this cannot be changed remotely.

The image boots in UEFI or BIOS/CSM mode with Secure Boot disabled. Firmware must already select the target device. The rdroot runs from memory, so the installer can reuse that device as its target.

GRUB one-shot boot

Copy the uncompressed ramdisk kernel. Do not rely on gzio.

Copy ramdisk kernelraw
# Use the uncompressed kernel; do not rely on gzio.
cp /path/to/bsdsshd.rd /boot/bsdsshd.rd
/etc/grub.d/40_customraw
menuentry "OpenBSD bsdsshd.rd" {
        insmod part_gpt
        insmod ext2
        insmod bsd
        # Replace UUID with the /boot filesystem UUID.
        search --no-floppy --fs-uuid --set=root UUID
        kopenbsd /bsdsshd.rd
}
/etc/default/grubraw
GRUB_DEFAULT=saved
Select one-shot bootraw
update-grub
grub-reboot "OpenBSD bsdsshd.rd"
grub-editenv list

# Expected output:
# next_entry=OpenBSD bsdsshd.rd

sync
reboot

This example was tested in BIOS/CSM mode. UEFI is untested and may lack display console output. Secure Boot is unsupported. part_gpt and ext2 match the tested /boot filesystem. The target layout may require different modules. The GRUB build must provide the bsd module and kopenbsd command. Other boot loaders are untested.

Network booting

The matching OpenBSD/amd64 pxeboot(8) can load the compressed bsdsshd.rd over the network in place of bsd.rd.

Remote session

After the enhanced kernel boots, connect as root when the configured address accepts ssh. Perform required storage preparation, then start the interactive installer.

Remote installraw
ssh -i /path/to/private_key root@host.example
# Perform the required storage setup.
# Enter "done" at the network prompt to preserve network configuration.
install

RDSSHD_CONSOLE_LOCK=yes presents no local installer menu or shell.

REMOVE PATCH

Prepare private state before reversal. The target prints the recorded object path and removes the source obj symlink. /usr/obj is the default; use the printed path when different. patch(1) removes added files but leaves their empty parent directory. rmdir refuses non-empty directories.

Clean and reverse patchraw
cd /usr/src/distrib/amd64/ramdisk_cd/rdsshd
make -f Makefile.rdsshd prepare-unpatch-rdsshd

cd /usr/src
patch -R -p1 < /root/bsdsshd.rd.patch
# Replace /usr/obj below if prepare-unpatch-rdsshd printed another path.
rmdir /usr/obj/distrib/amd64/ramdisk_cd/rdsshd
rmdir /usr/src/distrib/amd64/ramdisk_cd/rdsshd

FILES

/usr/src/distrib/amd64/ramdisk_cd/rdsshd
Added source directory and wrapper entry point.
obj/bsdsshd.rd
Uncompressed enhanced ramdisk kernel.
obj/bsdsshd.gz
Stripped and compressed kernel installed as bsdsshd.rd by the release target and as /bsd in the enhanced miniroot.
obj/minirootXX_sshd.img
Enhanced BIOS- and EFI-bootable miniroot image.
obj/vnd
Private vnd ownership record. Removed after successful assembly or safe cleanup.

DIAGNOSTICS

private object directory is not active
Run the wrapper obj target as a separate invocation.
normal OpenBSD object root does not exist
Create BSDOBJDIR with normal OpenBSD ownership and permissions.
normal ramdisk_cd object directory is not active
The stock obj target did not select an object directory distinct from its source directory. Check BSDOBJDIR and rerun the build.
stock ramdisk_cd instbin was not built
Inspect the preceding stock-target failure. Clean ramdisk_cd before retrying after a failed trace link.
invalid public key on line N of RDSSHD_AUTHORIZED_KEYS
Replace the indicated line with a key accepted by ssh-keygen(1).
RDSSHD_HOST_KEY is not an unencrypted Ed25519 private key
Supply an Ed25519 host private key without a passphrase.
stale private vnd state
Run unconfig-rdsshd before rebuilding the miniroot.
refusing to detach vnd not owned by rdsshd
The recorded device no longer covers the enhanced image. The target leaves it attached for manual inspection.

SEE ALSO

make(1), patch(1), ssh(1), ssh-keygen(1), softraid(4), vnd(4), disktab(5), install.site(5), sshd_config(5), autoinstall(8), bioctl(8), boot(8), boot_amd64(8), config(8), installboot(8), makefs(8), rdsetroot(8), release(8), sshd(8), and vnconfig(8).

AlmaLinux guide

IRC with Irssi and tmux

Keep a persistent Irssi connection available across SSH sessions.

DESCRIPTION

Irssi provides the IRC client. tmux keeps the client running after an SSH connection closes and allows the same terminal session to be resumed later.

The examples connect securely to irc.arachnogoat.com on port 6697 and join #chat. Replace YourNick and SERVER_PASSWORD before use.

INSTALLATION

AlmaLinux logo

Install tmux and Irssi from the AlmaLinux package repositories. Run the command from a normal shell with an account permitted to use sudo.

Install tmux and IrssiRun from a shell on AlmaLinuxraw
sudo dnf install tmux irssi

START A SESSION

tmux logo

Create a tmux session named irc. The command enters that session immediately.

Start the IRC tmux sessionraw
tmux new -s irc

Run irssi inside the new tmux session. Irssi then occupies the tmux window until it exits or the session is detached.

CONNECT

Irssi logo

Enter the following commands at the Irssi prompt. The connection uses TLS with certificate verification, then joins #chat.

Initial Irssi connectionReplace the nickname and server passwordraw
/SET nick YourNick
/CONNECT -tls -tls_verify irc.arachnogoat.com 6697 SERVER_PASSWORD
/JOIN #chat

NAVIGATION

Irssi normally places server status and the joined channel in separate windows. Use /WIN 1 for the first window and /WIN 2 for the second. The status bar shows the actual window numbers when they differ.

AUTOMATIC CONNECTION

Add a named network, its server, and the channel to Irssi, then save the configuration. Irssi will reconnect and join #chat on later starts.

Persistent Irssi configurationReplace SERVER_PASSWORD before useraw
/NETWORK ADD ArachnoIRC
/SERVER ADD -auto -tls -tls_verify -network ArachnoIRC irc.arachnogoat.com 6697 SERVER_PASSWORD
/CHANNEL ADD -auto #chat ArachnoIRC
/SAVE

/SAVE writes the supplied server password to the user's Irssi configuration. Protect that account and its ~/.irssi directory accordingly.

DETACH AND RETURN

Leave Irssi running

Press Ctrl-b, release both keys, then press d. tmux detaches while Irssi and its IRC connection continue running.

Resume the session

After reconnecting through SSH, attach to the existing tmux session from the shell.

Resume the IRC tmux sessionraw
tmux attach -t irc

SEE ALSO

See tmux(1) and the Irssi built-in /HELP command for additional session, window, and connection controls.

Message sent

X

Thank you. Your message has been sent.