ov-layers/skills/sshd/SKILL.md
OpenSSH server and client on port 22 for remote access. Use when working with SSH access, remote login, or sshd configuration in containers/VMs.
npx skillsauth add overthinkos/overthink-plugins sshdInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
| Property | Value |
|----------|-------|
| Ports | 22 |
| Install files | layer.yml |
openssh-server (RPM / deb) — SSH daemonopenssh-clients (RPM) / openssh-client (deb, singular) — SSH client tools (ssh, scp, sftp)openssh (pac) — Arch metapackage bundling both daemon and clientsudo (rpm / pac / deb) — required for the NOPASSWD rule this layer writesrpm: (Fedora), pac: (Arch), deb: (Debian/Ubuntu) — full parity across all four supported package-format families. The openssh-server / openssh-clients naming differs per distro; package-existence tests use package_map: to resolve (see below).
Cross-distro package-test pattern: the sshd layer's
openssh-server-package check uses package_map: to resolve the right
name per distro — openssh-server on Fedora/Debian, openssh on Arch.
This is the canonical worked example for the package_map feature; see
/ov:test "Cross-distro package names (package_map:)" for the
mechanics and the priority ordering (fedora:43 > fedora when both
match).
- id: openssh-server-package
package: openssh-server # default
package_map:
archlinux: openssh
fedora: openssh-server
fedora:43: openssh-server
debian: openssh-server
ubuntu: openssh-server
installed: true
getent passwd 1000The sudoers drop-in at /etc/sudoers.d/ov-user targets the actual uid-1000 account, whatever it happens to be named on the running base image. The layer no longer hardcodes a literal user — instead it discovers the account name at build time via getent passwd 1000:
tasks:
- cmd: |
account=$(getent passwd 1000 | cut -d: -f1)
if [ -z "$account" ]; then
echo "sshd layer: no uid-1000 account found — refusing to write sudoers" >&2
exit 1
fi
printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$account" > /etc/sudoers.d/ov-user
chmod 0440 /etc/sudoers.d/ov-user
user: root
This works uniformly across both user-policy modes:
| Image | Resolved account | Sudoers content |
|---|---|---|
| fedora-coder, arch-coder, debian-coder | user (create mode — /ov:image "user_policy") | user ALL=(ALL) NOPASSWD: ALL |
| ubuntu-coder | ubuntu (adopt mode — /ov-images:ubuntu base_user:) | ubuntu ALL=(ALL) NOPASSWD: ALL |
Why not ${USER} substitution? The generator substitutes ${USER} in task fields (paths, URLs, etc.) but not inside cmd: command text — cmd: is passed verbatim to bash, and bash at RUN time doesn't have $USER exported. getent is the robust, fully-generic alternative. See /ov:layer "${VAR} substitution scope" and /ov:image "user_policy" for the full architectural context.
# image.yml -- typically used via bootc-base composition
my-image:
layers:
- sshd
bootc-base composition layer (used in bootc images)/ov-images:aurora (disabled)/ov-images:selkies-desktop-bootc (via bootc-base) — worked example exercising the dual-mode sudo test below/etc/sudoers.d/ov-user (the NOPASSWD rule written by this layer) is
root:root 0750 — the non-root test user (uid 1000 in containers)
cannot traverse /etc/sudoers.d/. A file: /etc/sudoers.d/ov-user; exists: true
test reports "missing" even when the file is present. Use
command: sudo -n -l; stdout: [{contains: NOPASSWD}] to verify the
semantic instead. See /ov:test Authoring Gotcha #10.127.0.0.1:${HOST_PORT:2222}, not
${CONTAINER_IP}:${HOST_PORT:2222}. See /ov:test Gotcha #1.runuser -u user -- wrapperov image test runs with USER=1000 on container images but USER=0 on bootc images (bootc intentionally keeps USER=root because systemd manages user sessions via login). A naïve sudo -n -l; contains: NOPASSWD check fails on bootc — running as root prints root's Defaults block, which doesn't contain the literal string NOPASSWD. The layer's current test drops to user explicitly when running as root:
- id: sudoers-ov-user
command: |
if [ "$(id -u)" = "0" ]; then
runuser -u user -- sudo -n -l
else
sudo -n -l
fi
exit_status: 0
stdout:
- contains: "NOPASSWD"
Portability note: use runuser -u user -- <cmd>, not
runuser -l user -s /bin/bash -c '<cmd>'. On Arch util-linux (2.42+),
the -l … -c form swallows the wrapped command's stdout — reproduced
cleanly: runuser -l user -s /bin/bash -c 'sudo -n -l' prints nothing
and exits 0, while runuser -u user -- sudo -n -l prints the full
NOPASSWD listing. The layer was fixed to -u … -- after this was
caught during arch-ov bring-up. See /ov:test Authoring Gotcha #11.
/ov-layers:bootc-base -- composition that includes this layer/ov-layers:bootc-config -- the bootc boot wiring (tty1 autologin + systemd-user supervisord) that typically runs alongside this layer/ov-layers:cloud-init -- depends on sshd for VM provisioning/ov-images:selkies-desktop-bootc -- canonical bootc worked example that exercises the dual-mode sudo test/ov-images:ubuntu-coder -- canonical adopt-mode example; sudoers correctly targets ubuntu via getent/ov-images:debian-coder -- canonical create-mode deb-family example; sudoers targets user/ov-images:ubuntu -- declares the base_user: block that makes ubuntu-coder run as ubuntu/ov:test -- declarative testing framework (gotchas #10 and #11, package_map:, exclude_distros:)/ov:image -- user_policy: field (create / adopt / auto) that drives which account this layer's sudoers targets/ov:layer -- layer authoring (${VAR} substitution scope, cmd: vs write:)Use when the user asks about:
tools
Use when authoring or modifying a charly PLUGIN — a candy with a `plugin:` block that contributes Providers (verbs/kinds/deploy-targets/steps/builders/commands), its own CUE schema, builtin (compiled-in) or external (out-of-tree git repo). Covers the unified Provider model, the per-plugin CUE-schema contract (single source → Go params for dev + schema-over-Describe RPC for runtime), the SDK, and the loader.
tools
The CUE data-validation / configuration CLI (cue), pinned to v0.16.1. Use when working with the cue candy, installing the cue binary into a box or onto a target:local dev host, or running the offline schema-vendoring pipeline that feeds charly's egress validation.
tools
CUE EGRESS validation — validating (and, where it adds value, generating) the config files charly WRITES to a system BEFORE the bytes hit disk. MUST be invoked before working on charly/egress.go, the vendored schemas under candy/plugin-egress/egress-schemas/vendor/, the ValidateEgress / registerVendoredEgressKind path, the offline `task cue:vendor` pipeline, or adding an egress schema for any written artifact (cloud-init, k8s manifests, traefik routes, runtime config, install ledger, systemd/quadlet units, ssh_config, libvirt XML).
tools
Kubernetes cluster-probe declarative check verb — the `kube:` check verb (nodes, pods, ingress, storage class, addon health, apply/delete, and arbitrary resource GETs) served out-of-process by the candy/plugin-kube plugin (vendored client-go; no external kubectl required).