coder/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 | charly.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 candy 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 candy'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
/charly-check:check "Cross-distro package names (package_map:)" for the
mechanics and the priority ordering (fedora:43 > fedora when both
match).
# an id-named check step node under the sshd candy entity
openssh-server-package:
check: the openssh-server package is installed (name resolved per distro via package_map)
id: openssh-server-package
package: openssh-server # default
package_map:
arch: 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/charly-user targets the actual uid-1000 account, whatever it happens to be named on the running base image. The candy no longer hardcodes a literal user — instead it discovers the account name at build time via getent passwd 1000:
# a child step node under the sshd candy entity
sshd-write-sudoers:
run: write the NOPASSWD sudoers drop-in for the uid-1000 account
command: |
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/charly-user
chmod 0440 /etc/sudoers.d/charly-user
run_as: root
This works uniformly across both user-policy modes:
| Box | Resolved account | Sudoers content |
|---|---|---|
| fedora-coder, arch-coder, debian-coder | user (create mode — /charly-image:image "user_policy") | user ALL=(ALL) NOPASSWD: ALL |
| ubuntu-coder | ubuntu (adopt mode — /charly-distros:ubuntu base_user:) | ubuntu ALL=(ALL) NOPASSWD: ALL |
Why not ${USER} substitution? The generator substitutes ${USER} in plan-step fields (paths, URLs, etc.) but not inside command: command text — command: is passed verbatim to bash, and bash at RUN time doesn't have $USER exported. getent is the robust, fully-generic alternative. See /charly-image:layer "${VAR} substitution scope" and /charly-image:image "user_policy" for the full architectural context.
# charly.yml -- add the candy to any box that needs an in-container SSH server
# composition is a child node, not a top-level list
my-image:
candy:
base: fedora
my-image-candy:
candy:
- sshd
/charly-selkies:selkies-labwc), and applied to VM guests at deploy time/etc/sudoers.d/charly-user (the NOPASSWD rule written by this candy) is
root:root 0750 — the non-root test user (uid 1000 in containers)
cannot traverse /etc/sudoers.d/. A file: /etc/sudoers.d/charly-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 /charly-check:check Authoring Gotcha #10.127.0.0.1:${HOST_PORT:2222}, not
${CONTAINER_IP}:${HOST_PORT:2222}. See /charly-check:check Gotcha #1.runuser -u user -- wrappercharly check box 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 candy's current test drops to user explicitly when running as root:
# an id-named check step node under the sshd candy entity
sudoers-charly-user:
check: sudo -n -l lists the NOPASSWD rule (dropping to the uid-1000 user when run as root)
id: sudoers-charly-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 candy was fixed to -u … -- after this was
caught during charly-arch bring-up. See /charly-check:check Authoring Gotcha #11.
/charly-distros:cloud-init -- depends on sshd for VM provisioning/charly-coder:ubuntu-coder -- canonical adopt-mode example; sudoers correctly targets ubuntu via getent/charly-coder:debian-coder -- canonical create-mode deb-family example; sudoers targets user/charly-distros:ubuntu -- declares the base_user: block that makes ubuntu-coder run as ubuntu/charly-check:check -- declarative testing framework (gotchas #10 and #11, package_map:, exclude_distros:)/charly-image:image -- user_policy: field (create / adopt / auto) that drives which account this candy's sudoers targets/charly-image:layer -- candy authoring (${VAR} substitution scope, command: 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).