Skip to content

Commit 1c76cfe

Browse files
committed
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>
1 parent 62cfbc9 commit 1c76cfe

2 files changed

Lines changed: 286 additions & 0 deletions

File tree

src/klish-plugin-infix/src/infix.c

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
#include <stdarg.h>
55
#include <stdlib.h>
66
#include <string.h>
7+
#include <fcntl.h>
8+
#include <sys/stat.h>
79
#include <sys/types.h>
810
#include <sys/wait.h>
911

@@ -79,6 +81,51 @@ static int run(char *const argv[])
7981
return -1;
8082
}
8183

84+
/*
85+
* Like run(), but drops privileges to the given user before exec.
86+
* Use for commands that must run as the CLI user, not root (klishd).
87+
*/
88+
static int run_as_user(const char *user, char *const argv[])
89+
{
90+
struct passwd *pw;
91+
pid_t pid;
92+
int rc;
93+
94+
pw = getpwnam(user);
95+
if (!pw) {
96+
fprintf(stderr, ERRMSG "unknown user: %s\n", user);
97+
return -1;
98+
}
99+
100+
pid = fork();
101+
if (pid == -1)
102+
return -1;
103+
104+
if (!pid) {
105+
if (setgid(pw->pw_gid) || setuid(pw->pw_uid)) {
106+
fprintf(stderr, "Aborting, failed dropping privileges to "
107+
"(UID:%d GID:%d): %s\n",
108+
pw->pw_uid, pw->pw_gid, strerror(errno));
109+
_exit(1);
110+
}
111+
execvp(argv[0], argv);
112+
_exit(127);
113+
}
114+
115+
while (waitpid(pid, &rc, 0) != pid)
116+
;
117+
118+
if (WIFEXITED(rc))
119+
return WEXITSTATUS(rc);
120+
121+
if (WIFSIGNALED(rc)) {
122+
errno = EINTR;
123+
return -1;
124+
}
125+
126+
return -1;
127+
}
128+
82129
/*
83130
* Shell command execution - only use with hardcoded commands or when
84131
* shell features (pipes, redirects) are needed. Never use with user input.
@@ -427,6 +474,192 @@ int infix_asym_keys(kcontext_t *ctx)
427474
"| jq -r '.\"ietf-keystore:keystore\".\"asymmetric-keys\".\"asymmetric-key\"[].name'");
428475
}
429476

477+
/*
478+
* Create ~/.ssh/known_hosts with correct ownership if it doesn't exist.
479+
* Must be called as root before dropping privileges, since ~/.ssh/ is
480+
* owned by root on Infix (the system manages authorized_keys via YANG).
481+
* Once the file exists with the user's ownership, ssh(1) can update it.
482+
*/
483+
static void ensure_known_hosts(const struct passwd *pw)
484+
{
485+
char path[512];
486+
int fd;
487+
488+
snprintf(path, sizeof(path), "%s/.ssh/known_hosts", pw->pw_dir);
489+
fd = open(path, O_CREAT | O_EXCL | O_WRONLY, 0600);
490+
if (fd < 0)
491+
return; /* Already exists, or unrecoverable error */
492+
493+
fchown(fd, pw->pw_uid, pw->pw_gid);
494+
close(fd);
495+
}
496+
497+
int infix_ssh_connect(kcontext_t *ctx)
498+
{
499+
kpargv_t *pargv = kcontext_pargv(ctx);
500+
const char *host, *ruser, *port, *user;
501+
struct passwd *pw;
502+
kparg_t *parg;
503+
char *argv[8];
504+
int i = 0;
505+
506+
host = kparg_value(kpargv_find(pargv, "host"));
507+
508+
parg = kpargv_find(pargv, "user");
509+
ruser = parg ? kparg_value(parg) : NULL;
510+
511+
parg = kpargv_find(pargv, "port");
512+
port = parg ? kparg_value(parg) : NULL;
513+
514+
if (!host) {
515+
fprintf(stderr, ERRMSG "missing host argument.\n");
516+
return -1;
517+
}
518+
519+
user = cd_home(ctx);
520+
pw = getpwnam(user);
521+
if (pw)
522+
ensure_known_hosts(pw);
523+
524+
argv[i++] = "ssh";
525+
if (ruser) { argv[i++] = "-l"; argv[i++] = (char *)ruser; }
526+
if (port) { argv[i++] = "-p"; argv[i++] = (char *)port; }
527+
argv[i++] = (char *)host;
528+
argv[i] = NULL;
529+
530+
return run_as_user(user, argv);
531+
}
532+
533+
/*
534+
* Completion: list hostnames from the current user's ~/.ssh/known_hosts.
535+
*/
536+
int infix_ssh_known_hosts(kcontext_t *ctx)
537+
{
538+
char path[512], line[4096];
539+
const char *user;
540+
struct passwd *pw;
541+
FILE *f;
542+
543+
user = cd_home(ctx);
544+
pw = getpwnam(user);
545+
if (!pw)
546+
return 0;
547+
548+
snprintf(path, sizeof(path), "%s/.ssh/known_hosts", pw->pw_dir);
549+
f = fopen(path, "r");
550+
if (!f)
551+
return 0;
552+
553+
while (fgets(line, sizeof(line), f)) {
554+
char *sp;
555+
556+
/* Skip comments, blank lines, and hashed entries */
557+
if (line[0] == '#' || line[0] == '\n' || line[0] == '|')
558+
continue;
559+
sp = strchr(line, ' ');
560+
if (!sp)
561+
continue;
562+
*sp = '\0';
563+
/* Entries may be comma-separated: "host,ip algo key" */
564+
for (char *tok = strtok(line, ","); tok; tok = strtok(NULL, ","))
565+
puts(tok);
566+
}
567+
568+
fclose(f);
569+
return 0;
570+
}
571+
572+
/*
573+
* Pre-enroll a host public key received out-of-band into ~/.ssh/known_hosts.
574+
* Runs as the CLI user to ensure correct file ownership.
575+
*/
576+
int infix_ssh_add_known_host(kcontext_t *ctx)
577+
{
578+
kpargv_t *pargv = kcontext_pargv(ctx);
579+
const char *host, *keytype, *pubkey, *user;
580+
char path[512];
581+
struct passwd *pw;
582+
pid_t pid;
583+
int rc;
584+
585+
host = kparg_value(kpargv_find(pargv, "host"));
586+
keytype = kparg_value(kpargv_find(pargv, "keytype"));
587+
pubkey = kparg_value(kpargv_find(pargv, "pubkey"));
588+
if (!host || !keytype || !pubkey) {
589+
fprintf(stderr, ERRMSG "missing arguments.\n");
590+
return -1;
591+
}
592+
593+
user = cd_home(ctx);
594+
pw = getpwnam(user);
595+
if (!pw) {
596+
fprintf(stderr, ERRMSG "unknown user: %s\n", user);
597+
return -1;
598+
}
599+
600+
snprintf(path, sizeof(path), "%s/.ssh/known_hosts", pw->pw_dir);
601+
602+
ensure_known_hosts(pw);
603+
604+
pid = fork();
605+
if (pid == -1)
606+
return -1;
607+
608+
if (!pid) {
609+
FILE *f;
610+
611+
if (setgid(pw->pw_gid) || setuid(pw->pw_uid)) {
612+
fprintf(stderr, "Aborting, failed dropping privileges: %s\n",
613+
strerror(errno));
614+
_exit(1);
615+
}
616+
617+
f = fopen(path, "a");
618+
if (!f) {
619+
fprintf(stderr, ERRMSG "cannot open %s: %s\n", path, strerror(errno));
620+
_exit(1);
621+
}
622+
623+
fprintf(f, "%s %s %s\n", host, keytype, pubkey);
624+
fclose(f);
625+
printf("Host %s added to %s\n", host, path);
626+
_exit(0);
627+
}
628+
629+
while (waitpid(pid, &rc, 0) != pid)
630+
;
631+
632+
if (WIFEXITED(rc))
633+
return WEXITSTATUS(rc);
634+
635+
if (WIFSIGNALED(rc)) {
636+
errno = EINTR;
637+
return -1;
638+
}
639+
640+
return -1;
641+
}
642+
643+
int infix_ssh_remove_known_host(kcontext_t *ctx)
644+
{
645+
kpargv_t *pargv = kcontext_pargv(ctx);
646+
const char *host;
647+
char *argv[4];
648+
649+
host = kparg_value(kpargv_find(pargv, "host"));
650+
if (!host) {
651+
fprintf(stderr, ERRMSG "missing host argument.\n");
652+
return -1;
653+
}
654+
655+
argv[0] = "ssh-keygen";
656+
argv[1] = "-R";
657+
argv[2] = (char *)host;
658+
argv[3] = NULL;
659+
660+
return run_as_user(cd_home(ctx), argv);
661+
}
662+
430663
int kplugin_infix_fini(kcontext_t *ctx)
431664
{
432665
(void)ctx;
@@ -453,6 +686,10 @@ int kplugin_infix_init(kcontext_t *ctx)
453686
kplugin_add_syms(plugin, ksym_new("firewall_services", infix_firewall_services));
454687
kplugin_add_syms(plugin, ksym_new("set_boot_order", infix_set_boot_order));
455688
kplugin_add_syms(plugin, ksym_new("shell", infix_shell));
689+
kplugin_add_syms(plugin, ksym_new("ssh_connect", infix_ssh_connect));
690+
kplugin_add_syms(plugin, ksym_new("ssh_known_hosts", infix_ssh_known_hosts));
691+
kplugin_add_syms(plugin, ksym_new("ssh_add_known_host", infix_ssh_add_known_host));
692+
kplugin_add_syms(plugin, ksym_new("ssh_remove_known_host", infix_ssh_remove_known_host));
456693

457694
return 0;
458695
}

src/klish-plugin-infix/xml/infix.xml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,24 @@
169169
<ACTION sym="STRING"/>
170170
</PTYPE>
171171

172+
<PTYPE name="SSH_KNOWN_HOSTS">
173+
<COMPL>
174+
<ACTION sym="ssh_known_hosts@infix"/>
175+
</COMPL>
176+
<ACTION sym="STRING"/>
177+
</PTYPE>
178+
179+
<PTYPE name="SSH_KEY_TYPE">
180+
<COMPL>
181+
<ACTION sym="printl">ecdsa-sha2-nistp256</ACTION>
182+
<ACTION sym="printl">ecdsa-sha2-nistp384</ACTION>
183+
<ACTION sym="printl">ecdsa-sha2-nistp521</ACTION>
184+
<ACTION sym="printl">ssh-ed25519</ACTION>
185+
<ACTION sym="printl">ssh-rsa</ACTION>
186+
</COMPL>
187+
<ACTION sym="STRING"/>
188+
</PTYPE>
189+
172190
<VIEW name="main">
173191
<HOTKEY key="^D" cmd="exit"/>
174192

@@ -300,6 +318,15 @@ echo "Public: $pub"
300318
<PARAM name="third" ptype="/BOOT_TARGET" help="Third boot target" min="0"/>
301319
<ACTION sym="set_boot_order@infix"/>
302320
</COMMAND>
321+
322+
<COMMAND name="ssh" help="SSH client settings" mode="switch">
323+
<COMMAND name="known-hosts" help="Pre-enroll a host public key received out-of-band">
324+
<PARAM name="host" ptype="/STRING" help="Hostname or IP address"/>
325+
<PARAM name="keytype" ptype="/SSH_KEY_TYPE" help="Public key algorithm"/>
326+
<PARAM name="pubkey" ptype="/STRING" help="Base64-encoded public key"/>
327+
<ACTION sym="ssh_add_known_host@infix"/>
328+
</COMMAND>
329+
</COMMAND>
303330
</COMMAND>
304331

305332
<COMMAND name="dhcp-server" help="DHCP server tools" mode="switch">
@@ -875,6 +902,28 @@ echo "Public: $pub"
875902
</ACTION>
876903
</COMMAND>
877904

905+
<COMMAND name="ssh" help="Connect to a remote device over SSH">
906+
<SWITCH name="optional" min="0" max="2">
907+
<COMMAND name="user" help="Login with a different username">
908+
<PARAM name="user" ptype="/STRING" help="Remote username"/>
909+
</COMMAND>
910+
<COMMAND name="port" help="Connect to a non-standard port">
911+
<PARAM name="port" ptype="/UINT" help="TCP port number"/>
912+
</COMMAND>
913+
</SWITCH>
914+
<PARAM name="host" ptype="/STRING" help="Hostname or IP address"/>
915+
<ACTION sym="ssh_connect@infix" in="tty" out="tty" interrupt="true"/>
916+
</COMMAND>
917+
918+
<COMMAND name="no" help="Remove a setting or entry" mode="switch">
919+
<COMMAND name="ssh" help="SSH client settings" mode="switch">
920+
<COMMAND name="known-hosts" help="Remove a host from the SSH known_hosts file">
921+
<PARAM name="host" ptype="/SSH_KNOWN_HOSTS" help="Hostname or IP to remove"/>
922+
<ACTION sym="ssh_remove_known_host@infix"/>
923+
</COMMAND>
924+
</COMMAND>
925+
</COMMAND>
926+
878927
<COMMAND name="upgrade" help="Install a software update bundle from remote or local file">
879928
<PARAM name="URI" ptype="/STRING" help="[(ftp|tftp|http|https|sftp)://(dns.name | ip.address)/path/to/]upgrade-bundle.pkg"/>
880929
<SWITCH name="optional" min="0" max="2">

0 commit comments

Comments
 (0)