mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
cli: add 'ssh' commands to log onto other devices and manage keys
Add three SSH-related commands to the operational CLI:
ssh [user <name>] [port <num>] <host>
Connect to a remote device over SSH, running as the
CLI user (not root) by dropping privileges before exec.
set ssh known-hosts <host> <keytype> <pubkey>
Pre-enroll a host public key received out-of-band (e.g.
via email after a factory reset) into ~/.ssh/known_hosts,
avoiding a TOFU prompt on first connect.
no ssh known-hosts <host>
Remove a stale host key entry using ssh-keygen -R, e.g.
after a device factory reset causes a key mismatch.
Tab completion is provided for key types (ssh-ed25519,
ecdsa-sha2-nistp256, etc.) and for known host names/IPs.
A new run_as_user() helper is introduced alongside the existing
run(), factoring out the fork+setuid+execvp pattern used by
infix_shell() so it can be shared across the SSH functions.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
#include <errno.h>
|
||||
#include <grp.h>
|
||||
#include <pwd.h>
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
@@ -79,6 +82,53 @@ static int run(char *const argv[])
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Like run(), but drops privileges to the given user before exec.
|
||||
* Use for commands that must run as the CLI user, not root (klishd).
|
||||
*/
|
||||
static int run_as_user(const char *user, char *const argv[])
|
||||
{
|
||||
struct passwd *pw;
|
||||
pid_t pid;
|
||||
int rc;
|
||||
|
||||
pw = getpwnam(user);
|
||||
if (!pw) {
|
||||
fprintf(stderr, ERRMSG "unknown user: %s\n", user);
|
||||
return -1;
|
||||
}
|
||||
|
||||
pid = fork();
|
||||
if (pid == -1)
|
||||
return -1;
|
||||
|
||||
if (!pid) {
|
||||
if (initgroups(user, pw->pw_gid) || setgid(pw->pw_gid) || setuid(pw->pw_uid)) {
|
||||
fprintf(stderr, "Aborting, failed dropping privileges to "
|
||||
"(UID:%d GID:%d): %s\n",
|
||||
pw->pw_uid, pw->pw_gid, strerror(errno));
|
||||
_exit(1);
|
||||
}
|
||||
execvp(argv[0], argv);
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
while (waitpid(pid, &rc, 0) < 0) {
|
||||
if (errno != EINTR)
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (WIFEXITED(rc))
|
||||
return WEXITSTATUS(rc);
|
||||
|
||||
if (WIFSIGNALED(rc)) {
|
||||
errno = EINTR;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Shell command execution - only use with hardcoded commands or when
|
||||
* shell features (pipes, redirects) are needed. Never use with user input.
|
||||
@@ -304,7 +354,7 @@ int infix_shell(kcontext_t *ctx)
|
||||
};
|
||||
|
||||
pw = getpwnam(user);
|
||||
if (setgid(pw->pw_gid) || setuid(pw->pw_uid)) {
|
||||
if (initgroups(user, pw->pw_gid) || setgid(pw->pw_gid) || setuid(pw->pw_uid)) {
|
||||
fprintf(stderr, "Aborting, failed dropping privileges to (UID:%d GID:%d): %s\n",
|
||||
pw->pw_uid, pw->pw_gid, strerror(errno));
|
||||
_exit(1);
|
||||
@@ -313,8 +363,10 @@ int infix_shell(kcontext_t *ctx)
|
||||
_exit(execvp(args[0], args));
|
||||
}
|
||||
|
||||
while (waitpid(pid, &rc, 0) != pid)
|
||||
;
|
||||
while (waitpid(pid, &rc, 0) < 0) {
|
||||
if (errno != EINTR)
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (WIFEXITED(rc))
|
||||
rc = WEXITSTATUS(rc);
|
||||
@@ -427,6 +479,192 @@ int infix_asym_keys(kcontext_t *ctx)
|
||||
"| jq -r '.\"ietf-keystore:keystore\".\"asymmetric-keys\".\"asymmetric-key\"[].name'");
|
||||
}
|
||||
|
||||
/*
|
||||
* Create ~/.ssh/known_hosts with correct ownership if it doesn't exist.
|
||||
* Must be called as root before dropping privileges, since ~/.ssh/ is
|
||||
* owned by root on Infix (the system manages authorized_keys via YANG).
|
||||
* Once the file exists with the user's ownership, ssh(1) can update it.
|
||||
*/
|
||||
static void ensure_known_hosts(const struct passwd *pw)
|
||||
{
|
||||
char path[512];
|
||||
int fd;
|
||||
|
||||
snprintf(path, sizeof(path), "%s/.ssh/known_hosts", pw->pw_dir);
|
||||
fd = open(path, O_CREAT | O_EXCL | O_WRONLY, 0600);
|
||||
if (fd < 0)
|
||||
return; /* Already exists, or unrecoverable error */
|
||||
|
||||
fchown(fd, pw->pw_uid, pw->pw_gid);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
int infix_ssh_connect(kcontext_t *ctx)
|
||||
{
|
||||
kpargv_t *pargv = kcontext_pargv(ctx);
|
||||
const char *host, *ruser, *port, *user;
|
||||
struct passwd *pw;
|
||||
kparg_t *parg;
|
||||
char *argv[8];
|
||||
int i = 0;
|
||||
|
||||
host = kparg_value(kpargv_find(pargv, "host"));
|
||||
|
||||
parg = kpargv_find(pargv, "user");
|
||||
ruser = parg ? kparg_value(parg) : NULL;
|
||||
|
||||
parg = kpargv_find(pargv, "port");
|
||||
port = parg ? kparg_value(parg) : NULL;
|
||||
|
||||
if (!host) {
|
||||
fprintf(stderr, ERRMSG "missing host argument.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
user = cd_home(ctx);
|
||||
pw = getpwnam(user);
|
||||
if (pw)
|
||||
ensure_known_hosts(pw);
|
||||
|
||||
argv[i++] = "ssh";
|
||||
if (ruser) { argv[i++] = "-l"; argv[i++] = (char *)ruser; }
|
||||
if (port) { argv[i++] = "-p"; argv[i++] = (char *)port; }
|
||||
argv[i++] = (char *)host;
|
||||
argv[i] = NULL;
|
||||
|
||||
return run_as_user(user, argv);
|
||||
}
|
||||
|
||||
/*
|
||||
* Completion: list hostnames from the current user's ~/.ssh/known_hosts.
|
||||
*/
|
||||
int infix_ssh_known_hosts(kcontext_t *ctx)
|
||||
{
|
||||
char path[512], line[4096];
|
||||
const char *user;
|
||||
struct passwd *pw;
|
||||
FILE *f;
|
||||
|
||||
user = cd_home(ctx);
|
||||
pw = getpwnam(user);
|
||||
if (!pw)
|
||||
return 0;
|
||||
|
||||
snprintf(path, sizeof(path), "%s/.ssh/known_hosts", pw->pw_dir);
|
||||
f = fopen(path, "r");
|
||||
if (!f)
|
||||
return 0;
|
||||
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
char *sp;
|
||||
|
||||
/* Skip comments, blank lines, and hashed entries */
|
||||
if (line[0] == '#' || line[0] == '\n' || line[0] == '|')
|
||||
continue;
|
||||
sp = strchr(line, ' ');
|
||||
if (!sp)
|
||||
continue;
|
||||
*sp = '\0';
|
||||
/* Entries may be comma-separated: "host,ip algo key" */
|
||||
for (char *tok = strtok(line, ","); tok; tok = strtok(NULL, ","))
|
||||
puts(tok);
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Pre-enroll a host public key received out-of-band into ~/.ssh/known_hosts.
|
||||
* Runs as the CLI user to ensure correct file ownership.
|
||||
*/
|
||||
int infix_ssh_add_known_host(kcontext_t *ctx)
|
||||
{
|
||||
kpargv_t *pargv = kcontext_pargv(ctx);
|
||||
const char *host, *keytype, *pubkey, *user;
|
||||
char path[512];
|
||||
struct passwd *pw;
|
||||
pid_t pid;
|
||||
int rc;
|
||||
|
||||
host = kparg_value(kpargv_find(pargv, "host"));
|
||||
keytype = kparg_value(kpargv_find(pargv, "keytype"));
|
||||
pubkey = kparg_value(kpargv_find(pargv, "pubkey"));
|
||||
if (!host || !keytype || !pubkey) {
|
||||
fprintf(stderr, ERRMSG "missing arguments.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
user = cd_home(ctx);
|
||||
pw = getpwnam(user);
|
||||
if (!pw) {
|
||||
fprintf(stderr, ERRMSG "unknown user: %s\n", user);
|
||||
return -1;
|
||||
}
|
||||
|
||||
snprintf(path, sizeof(path), "%s/.ssh/known_hosts", pw->pw_dir);
|
||||
|
||||
ensure_known_hosts(pw);
|
||||
|
||||
pid = fork();
|
||||
if (pid == -1)
|
||||
return -1;
|
||||
|
||||
if (!pid) {
|
||||
FILE *f;
|
||||
|
||||
if (setgid(pw->pw_gid) || setuid(pw->pw_uid)) {
|
||||
fprintf(stderr, "Aborting, failed dropping privileges: %s\n",
|
||||
strerror(errno));
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
f = fopen(path, "a");
|
||||
if (!f) {
|
||||
fprintf(stderr, ERRMSG "cannot open %s: %s\n", path, strerror(errno));
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
fprintf(f, "%s %s %s\n", host, keytype, pubkey);
|
||||
fclose(f);
|
||||
printf("Host %s added to %s\n", host, path);
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
while (waitpid(pid, &rc, 0) != pid)
|
||||
;
|
||||
|
||||
if (WIFEXITED(rc))
|
||||
return WEXITSTATUS(rc);
|
||||
|
||||
if (WIFSIGNALED(rc)) {
|
||||
errno = EINTR;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int infix_ssh_remove_known_host(kcontext_t *ctx)
|
||||
{
|
||||
kpargv_t *pargv = kcontext_pargv(ctx);
|
||||
const char *host;
|
||||
char *argv[4];
|
||||
|
||||
host = kparg_value(kpargv_find(pargv, "host"));
|
||||
if (!host) {
|
||||
fprintf(stderr, ERRMSG "missing host argument.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
argv[0] = "ssh-keygen";
|
||||
argv[1] = "-R";
|
||||
argv[2] = (char *)host;
|
||||
argv[3] = NULL;
|
||||
|
||||
return run_as_user(cd_home(ctx), argv);
|
||||
}
|
||||
|
||||
int kplugin_infix_fini(kcontext_t *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
@@ -453,6 +691,10 @@ int kplugin_infix_init(kcontext_t *ctx)
|
||||
kplugin_add_syms(plugin, ksym_new("firewall_services", infix_firewall_services));
|
||||
kplugin_add_syms(plugin, ksym_new("set_boot_order", infix_set_boot_order));
|
||||
kplugin_add_syms(plugin, ksym_new("shell", infix_shell));
|
||||
kplugin_add_syms(plugin, ksym_new("ssh_connect", infix_ssh_connect));
|
||||
kplugin_add_syms(plugin, ksym_new("ssh_known_hosts", infix_ssh_known_hosts));
|
||||
kplugin_add_syms(plugin, ksym_new("ssh_add_known_host", infix_ssh_add_known_host));
|
||||
kplugin_add_syms(plugin, ksym_new("ssh_remove_known_host", infix_ssh_remove_known_host));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -169,6 +169,24 @@
|
||||
<ACTION sym="STRING"/>
|
||||
</PTYPE>
|
||||
|
||||
<PTYPE name="SSH_KNOWN_HOSTS">
|
||||
<COMPL>
|
||||
<ACTION sym="ssh_known_hosts@infix"/>
|
||||
</COMPL>
|
||||
<ACTION sym="STRING"/>
|
||||
</PTYPE>
|
||||
|
||||
<PTYPE name="SSH_KEY_TYPE">
|
||||
<COMPL>
|
||||
<ACTION sym="printl">ecdsa-sha2-nistp256</ACTION>
|
||||
<ACTION sym="printl">ecdsa-sha2-nistp384</ACTION>
|
||||
<ACTION sym="printl">ecdsa-sha2-nistp521</ACTION>
|
||||
<ACTION sym="printl">ssh-ed25519</ACTION>
|
||||
<ACTION sym="printl">ssh-rsa</ACTION>
|
||||
</COMPL>
|
||||
<ACTION sym="STRING"/>
|
||||
</PTYPE>
|
||||
|
||||
<VIEW name="main">
|
||||
<HOTKEY key="^D" cmd="exit"/>
|
||||
|
||||
@@ -300,6 +318,15 @@ echo "Public: $pub"
|
||||
<PARAM name="third" ptype="/BOOT_TARGET" help="Third boot target" min="0"/>
|
||||
<ACTION sym="set_boot_order@infix"/>
|
||||
</COMMAND>
|
||||
|
||||
<COMMAND name="ssh" help="SSH client settings" mode="switch">
|
||||
<COMMAND name="known-hosts" help="Pre-enroll a host public key received out-of-band">
|
||||
<PARAM name="host" ptype="/STRING" help="Hostname or IP address"/>
|
||||
<PARAM name="keytype" ptype="/SSH_KEY_TYPE" help="Public key algorithm"/>
|
||||
<PARAM name="pubkey" ptype="/STRING" help="Base64-encoded public key"/>
|
||||
<ACTION sym="ssh_add_known_host@infix"/>
|
||||
</COMMAND>
|
||||
</COMMAND>
|
||||
</COMMAND>
|
||||
|
||||
<COMMAND name="dhcp-server" help="DHCP server tools" mode="switch">
|
||||
@@ -875,6 +902,28 @@ echo "Public: $pub"
|
||||
</ACTION>
|
||||
</COMMAND>
|
||||
|
||||
<COMMAND name="ssh" help="Connect to a remote device over SSH">
|
||||
<SWITCH name="optional" min="0" max="2">
|
||||
<COMMAND name="user" help="Login with a different username">
|
||||
<PARAM name="user" ptype="/STRING" help="Remote username"/>
|
||||
</COMMAND>
|
||||
<COMMAND name="port" help="Connect to a non-standard port">
|
||||
<PARAM name="port" ptype="/UINT" help="TCP port number"/>
|
||||
</COMMAND>
|
||||
</SWITCH>
|
||||
<PARAM name="host" ptype="/STRING" help="Hostname or IP address"/>
|
||||
<ACTION sym="ssh_connect@infix" in="tty" out="tty" interrupt="true"/>
|
||||
</COMMAND>
|
||||
|
||||
<COMMAND name="no" help="Remove a setting or entry" mode="switch">
|
||||
<COMMAND name="ssh" help="SSH client settings" mode="switch">
|
||||
<COMMAND name="known-hosts" help="Remove a host from the SSH known_hosts file">
|
||||
<PARAM name="host" ptype="/SSH_KNOWN_HOSTS" help="Hostname or IP to remove"/>
|
||||
<ACTION sym="ssh_remove_known_host@infix"/>
|
||||
</COMMAND>
|
||||
</COMMAND>
|
||||
</COMMAND>
|
||||
|
||||
<COMMAND name="upgrade" help="Install a software update bundle from remote or local file">
|
||||
<PARAM name="URI" ptype="/STRING" help="[(ftp|tftp|http|https|sftp)://(dns.name | ip.address)/path/to/]upgrade-bundle.pkg"/>
|
||||
<SWITCH name="optional" min="0" max="2">
|
||||
|
||||
Reference in New Issue
Block a user