From 349b114752c3d23e549257cfea20d85814974d69 Mon Sep 17 00:00:00 2001 From: John-Mark Gurney Date: Thu, 22 Oct 2020 19:54:01 +0000 Subject: [PATCH] add support for ro/wo and tests to verify this funcionality.. Also did some line wrapping.. --- ggatessh/ggatessh.c | 173 ++++++++++++++++++++++++++++------------- tests/ggatessh_test.sh | 128 +++++++++++++++++++++++++++--- 2 files changed, 238 insertions(+), 63 deletions(-) diff --git a/ggatessh/ggatessh.c b/ggatessh/ggatessh.c index f8a2337..dedeab8 100644 --- a/ggatessh/ggatessh.c +++ b/ggatessh/ggatessh.c @@ -104,8 +104,10 @@ struct ggs_req { TAILQ_ENTRY(ggs_req) r_next; }; -static TAILQ_HEAD(ggs_reqqueue, ggs_req) procqueue = TAILQ_HEAD_INITIALIZER(procqueue); -static TAILQ_HEAD(ggs_sessqueue, ggs_sess_cache) session_cache = TAILQ_HEAD_INITIALIZER(session_cache); +static TAILQ_HEAD(ggs_reqqueue, ggs_req) procqueue = + TAILQ_HEAD_INITIALIZER(procqueue); +static TAILQ_HEAD(ggs_sessqueue, ggs_sess_cache) session_cache = + TAILQ_HEAD_INITIALIZER(session_cache); static sem_t nconn_sem; static pthread_mutex_t procqueue_mtx; @@ -196,6 +198,22 @@ tcp_connect(const char *host, const char *service, int af) return sd; } +static int +get_open_flags() +{ + + switch (flags) { + case G_GATE_FLAG_READONLY: + return LIBSSH2_FXF_READ; + + case G_GATE_FLAG_WRITEONLY: + return LIBSSH2_FXF_WRITE; + + default: + return LIBSSH2_FXF_READ|LIBSSH2_FXF_WRITE; + } +} + static struct ggs_connection make_connection(void) { @@ -209,7 +227,8 @@ make_connection(void) sockfd = tcp_connect(hostname, sshport, 0); if (sockfd == -1) { if (errno == ENOENT) - g_gate_xlog("tcp_connect: failed to lookup %s", hostname); + g_gate_xlog("tcp_connect: failed to lookup %s", + hostname); g_gate_xlog("tcp_connect: %s.", strerror(errno)); } @@ -218,11 +237,10 @@ make_connection(void) if (session == NULL) libssh2_errorx(session, "libssh2_session_init"); - if (g_gate_verbose) { - //libssh2_trace(session, LIBSSH2_TRACE_SOCKET|LIBSSH2_TRACE_TRANS|LIBSSH2_TRACE_KEX|LIBSSH2_TRACE_AUTH|LIBSSH2_TRACE_CONN|LIBSSH2_TRACE_SFTP|LIBSSH2_TRACE_ERROR|LIBSSH2_TRACE_PUBLICKEY); - libssh2_trace(session, LIBSSH2_TRACE_SOCKET|LIBSSH2_TRACE_KEX|LIBSSH2_TRACE_AUTH|LIBSSH2_TRACE_CONN|LIBSSH2_TRACE_SFTP|LIBSSH2_TRACE_ERROR|LIBSSH2_TRACE_PUBLICKEY); - //libssh2_trace(session, LIBSSH2_TRACE_KEX|LIBSSH2_TRACE_AUTH|LIBSSH2_TRACE_CONN|LIBSSH2_TRACE_SFTP|LIBSSH2_TRACE_ERROR|LIBSSH2_TRACE_PUBLICKEY); - } + if (g_gate_verbose) + libssh2_trace(session, LIBSSH2_TRACE_SOCKET|LIBSSH2_TRACE_KEX| + LIBSSH2_TRACE_AUTH|LIBSSH2_TRACE_CONN|LIBSSH2_TRACE_SFTP| + LIBSSH2_TRACE_ERROR|LIBSSH2_TRACE_PUBLICKEY); /* XXX - libssh2_session_flag to enable compression */ @@ -247,7 +265,8 @@ make_connection(void) g_gate_log(LOG_DEBUG, "trying identity file: %s", identityfile); - rc = libssh2_userauth_publickey_fromfile(session, username, pubkeyfile, identityfile, NULL); + rc = libssh2_userauth_publickey_fromfile(session, username, pubkeyfile, + identityfile, NULL); //rc = libssh2_userauth_password(session, "freebsd", "freebsd"); if (rc) { g_gate_log(LOG_ERR, "identity file: %s", identityfile); @@ -260,7 +279,7 @@ make_connection(void) if (sftp_session == NULL) g_gate_xlog("libssh2_sftp_init"); - handle = libssh2_sftp_open(sftp_session, imgpath, LIBSSH2_FXF_READ|LIBSSH2_FXF_WRITE, 0); + handle = libssh2_sftp_open(sftp_session, imgpath, get_open_flags(), 0); if (handle == NULL) { g_gate_log(LOG_ERR, "image file: %s", imgpath); libssh2_errorx(session, "libssh2_sftp_open"); @@ -291,7 +310,8 @@ mediachg(void *arg __unused) .gctl_mediasize = mediasize, }; g_gate_ioctl(G_GATE_CMD_MODIFY, &ggiom); - g_gate_log(LOG_DEBUG, "updated ggate%d mediasize to %zd", unit, mediasize); + g_gate_log(LOG_DEBUG, "updated ggate%d mediasize to %zd", unit, + mediasize); return NULL; } @@ -407,7 +427,8 @@ req_thread(void *arg __unused) } static int -process_pending(struct ggs_reqqueue *req_pending, struct ggs_sessqueue *sessqueue) +process_pending(struct ggs_reqqueue *req_pending, + struct ggs_sessqueue *sessqueue) { struct ggs_req *greq, *greq2; char *errmsg; @@ -421,27 +442,40 @@ process_pending(struct ggs_reqqueue *req_pending, struct ggs_sessqueue *sessqueu again: switch (greq->r_ggio.gctl_cmd) { case BIO_READ: - g_gate_log(LOG_DEBUG, "sftp_read(%p): %d(%d), rem: %d", greq, greq->r_ggio.gctl_offset, greq->r_ggio.gctl_length, greq->r_ggio.gctl_length - greq->r_bufoff); + g_gate_log(LOG_DEBUG, "sftp_read(%p): %d(%d), rem: %d", + greq, greq->r_ggio.gctl_offset, + greq->r_ggio.gctl_length, + greq->r_ggio.gctl_length - greq->r_bufoff); if (greq->r_didseek == 0) { - libssh2_sftp_seek64(greq->r_handle, greq->r_ggio.gctl_offset); + libssh2_sftp_seek64(greq->r_handle, + greq->r_ggio.gctl_offset); greq->r_didseek = 1; } - rc = libssh2_sftp_read(greq->r_handle, (char *)greq->r_ggio.gctl_data + greq->r_bufoff, greq->r_ggio.gctl_length - greq->r_bufoff); + rc = libssh2_sftp_read(greq->r_handle, + (char *)greq->r_ggio.gctl_data + greq->r_bufoff, + greq->r_ggio.gctl_length - greq->r_bufoff); g_gate_log(LOG_DEBUG, "sftp_read ret: %d", rc); if (rc < 0 && rc != LIBSSH2_ERROR_EAGAIN) g_gate_log(LOG_ERR, "libssh2_sftp_read"); break; case BIO_WRITE: - g_gate_log(LOG_DEBUG, "sftp_write(%p): %d(%d), rem: %d", greq, greq->r_ggio.gctl_offset, greq->r_ggio.gctl_length, greq->r_ggio.gctl_length - greq->r_bufoff); + g_gate_log(LOG_DEBUG, "sftp_write(%p): %d(%d), rem: %d", + greq, greq->r_ggio.gctl_offset, + greq->r_ggio.gctl_length, + greq->r_ggio.gctl_length - greq->r_bufoff); if (greq->r_didseek == 0) { - libssh2_sftp_seek64(greq->r_handle, greq->r_ggio.gctl_offset); + libssh2_sftp_seek64(greq->r_handle, + greq->r_ggio.gctl_offset); greq->r_didseek = 1; } - rc = libssh2_sftp_write(greq->r_handle, (char *)greq->r_ggio.gctl_data + greq->r_bufoff, greq->r_ggio.gctl_length - greq->r_bufoff); + rc = libssh2_sftp_write(greq->r_handle, + (char *)greq->r_ggio.gctl_data + greq->r_bufoff, + greq->r_ggio.gctl_length - greq->r_bufoff); g_gate_log(LOG_DEBUG, "sftp_write ret: %d", rc); if (rc < 0 && rc != LIBSSH2_ERROR_EAGAIN) - libssh2_errorx(greq->r_ssh_session, "libssh2_sftp_write"); + libssh2_errorx(greq->r_ssh_session, + "libssh2_sftp_write"); break; case BIO_FLUSH: @@ -461,8 +495,10 @@ again: goto completeio; default: - libssh2_session_last_error(greq->r_ssh_session, &errmsg, NULL, 0); - g_gate_log(LOG_ERR, "sftp_flush(%p) ret %d: %s", greq, rc, errmsg); + libssh2_session_last_error(greq->r_ssh_session, + &errmsg, NULL, 0); + g_gate_log(LOG_ERR, "sftp_flush(%p) ret %d: %s", + greq, rc, errmsg); greq->r_ggio.gctl_error = EIO; goto completeio; } @@ -471,7 +507,8 @@ again: default: rc = 0; - g_gate_log(LOG_ERR, "unhandled op: %d", greq->r_ggio.gctl_cmd); + g_gate_log(LOG_ERR, "unhandled op: %d", + greq->r_ggio.gctl_cmd); continue; } @@ -479,7 +516,10 @@ again: didwork = 1; greq->r_bufoff += rc; - /* try again on partial read/write, might have more data pending */ + /* + * try again on partial read/write, + * might have more data pending + */ if ((off_t)greq->r_bufoff != greq->r_ggio.gctl_length) goto again; } @@ -487,10 +527,12 @@ again: if ((off_t)greq->r_bufoff == greq->r_ggio.gctl_length) { /* complete */ completeio: - g_gate_log(LOG_DEBUG, "cmd complete: seq: %d, cmd: %d", greq->r_ggio.gctl_seq, greq->r_ggio.gctl_cmd); + g_gate_log(LOG_DEBUG, "cmd complete: seq: %d, cmd: %d", + greq->r_ggio.gctl_seq, greq->r_ggio.gctl_cmd); g_gate_ioctl(G_GATE_CMD_DONE, &greq->r_ggio); TAILQ_REMOVE(req_pending, greq, r_next); - TAILQ_INSERT_HEAD(sessqueue, greq->r_sesscache, sc_next); + TAILQ_INSERT_HEAD(sessqueue, greq->r_sesscache, + sc_next); free(greq->r_ggio.gctl_data); free(greq); @@ -562,7 +604,8 @@ proc_thread(void *arg __unused) FD_ZERO(&fdexcep); dir = libssh2_session_block_directions(session); - if (dir & LIBSSH2_SESSION_BLOCK_INBOUND || gsc_pending != NULL) + if (dir & LIBSSH2_SESSION_BLOCK_INBOUND || + gsc_pending != NULL) FD_SET(sockfd, &fdread); if (dir & LIBSSH2_SESSION_BLOCK_OUTBOUND) FD_SET(sockfd, &fdwrite); @@ -571,24 +614,30 @@ proc_thread(void *arg __unused) FD_SET(popfd, &fdread); maxfd = MAX(popfd, sockfd); - #if 0 - /* we need to be kj */ - if (gsc_pending != NULL) - FD_SET(sockfd, &fdread); - #endif - - g_gate_log(LOG_DEBUG, "selecting: %s %s, read: sockfd: %d, popfd: %d, write: sockfd: %d", (dir & LIBSSH2_SESSION_BLOCK_INBOUND) ? "inbound" : "", (dir & LIBSSH2_SESSION_BLOCK_OUTBOUND) ? "outbound" : "", FD_ISSET(sockfd, &fdread), FD_ISSET(popfd, &fdread), FD_ISSET(sockfd, &fdwrite)); + g_gate_log(LOG_DEBUG, "selecting: %s %s, " \ + "read: sockfd: %d, popfd: %d, write: sockfd: %d", + (dir & LIBSSH2_SESSION_BLOCK_INBOUND) ? "inbound" : + "", (dir & LIBSSH2_SESSION_BLOCK_OUTBOUND) ? + "outbound" : "", FD_ISSET(sockfd, &fdread), + FD_ISSET(popfd, &fdread), + FD_ISSET(sockfd, &fdwrite)); to = (struct timeval){ .tv_sec = 1, .tv_usec = 1000 }; (void)to; - rc = select(maxfd + 1, &fdread, &fdwrite, &fdexcep, NULL); + rc = select(maxfd + 1, &fdread, &fdwrite, &fdexcep, + NULL); switch (rc) { case -1: - g_gate_log(LOG_ERR, "%s: select failed: %s", __func__, - strerror(errno)); + g_gate_log(LOG_ERR, "%s: select failed: %s", + __func__, strerror(errno)); break; case 0: default: - g_gate_log(LOG_DEBUG, "select: %d, read: sockfd: %d, popfd: %d, write: sockfd: %d", rc, FD_ISSET(sockfd, &fdread), FD_ISSET(popfd, &fdread), FD_ISSET(sockfd, &fdwrite)); + g_gate_log(LOG_DEBUG, "select: %d, " \ + "read: sockfd: %d, popfd: %d, " \ + "write: sockfd: %d", rc, + FD_ISSET(sockfd, &fdread), + FD_ISSET(popfd, &fdread), + FD_ISSET(sockfd, &fdwrite)); break; } } @@ -623,30 +672,38 @@ procreq: if (gsc == NULL) { if (gsc_pending == NULL) { /* need new session */ - g_gate_log(LOG_DEBUG, "need new session"); - gsc_pending = malloc(sizeof *gsc); - gsc_pending->sc_ssh_session = session; + g_gate_log(LOG_DEBUG, + "need new session"); + gsc_pending = + malloc(sizeof *gsc); + gsc_pending->sc_ssh_session = + session; gsc_pending->sc_session = NULL; gsc_pending->sc_handle = NULL; } /* put back request */ - error = pthread_mutex_lock(&procqueue_mtx); + error = + pthread_mutex_lock(&procqueue_mtx); assert(error == 0); - TAILQ_INSERT_HEAD(&procqueue, greq, r_next); - error = pthread_mutex_unlock(&procqueue_mtx); + TAILQ_INSERT_HEAD(&procqueue, greq, + r_next); + error = pthread_mutex_unlock( + &procqueue_mtx); assert(error == 0); break; } else { /* process request */ - TAILQ_REMOVE(&session_cache, gsc, sc_next); + TAILQ_REMOVE(&session_cache, gsc, + sc_next); greq->r_sesscache = gsc; gsc = NULL; greq->r_bufoff = 0; - TAILQ_INSERT_TAIL(&req_pending, greq, r_next); + TAILQ_INSERT_TAIL(&req_pending, greq, + r_next); greq = NULL; } @@ -657,20 +714,26 @@ procreq: /* we are creating a new session */ if (gsc_pending->sc_session == NULL) { didwork = 1; - gsc_pending->sc_session = libssh2_sftp_init(session); + gsc_pending->sc_session = + libssh2_sftp_init(session); } if (gsc_pending->sc_session != NULL) { didwork = 1; - gsc_pending->sc_handle = libssh2_sftp_open(gsc_pending->sc_session, "fstest/data.img", LIBSSH2_FXF_READ|LIBSSH2_FXF_WRITE, 0); + gsc_pending->sc_handle = libssh2_sftp_open( + gsc_pending->sc_session, "fstest/data.img", + get_open_flags(), 0); } - g_gate_log(LOG_DEBUG, "pending: session: %p, handle: %p", gsc_pending->sc_session, gsc_pending->sc_handle); + g_gate_log(LOG_DEBUG, + "pending: session: %p, handle: %p", + gsc_pending->sc_session, gsc_pending->sc_handle); /* we have a fully initalized entry, use it */ if (gsc_pending->sc_handle != NULL) { g_gate_log(LOG_DEBUG, "new session created"); - TAILQ_INSERT_HEAD(&session_cache, gsc_pending, sc_next); + TAILQ_INSERT_HEAD(&session_cache, gsc_pending, + sc_next); gsc_pending = NULL; didwork = 1; goto procreq; @@ -692,13 +755,15 @@ ggatessh_makepidfile(void) if (!g_gate_verbose) { if (ggatessh_pidfile == NULL) { - asprintf(&ggatessh_pidfile, _PATH_VARRUN "/ggatessh.ggate%d.pid", unit); + asprintf(&ggatessh_pidfile, + _PATH_VARRUN "/ggatessh.ggate%d.pid", unit); err(EXIT_FAILURE, "Cannot allocate memory for pidfile"); } pfh = pidfile_open(ggatessh_pidfile, 0600, &otherpid); if (pfh == NULL) { if (errno == EEXIST) { - errx(EXIT_FAILURE, "Daemon already running, pid: %jd.", + errx(EXIT_FAILURE, + "Daemon already running, pid: %jd.", (intmax_t)otherpid); } err(EXIT_FAILURE, "Cannot open/create pidfile"); @@ -824,7 +889,8 @@ g_gatessh_create(void) ggioc.gctl_maxcount = queue_size; ggioc.gctl_timeout = timeout; ggioc.gctl_unit = unit; - snprintf(ggioc.gctl_info, sizeof(ggioc.gctl_info), "%s@%s:%s", username, hostname, imgpath); + snprintf(ggioc.gctl_info, sizeof(ggioc.gctl_info), "%s@%s:%s", + username, hostname, imgpath); g_gate_ioctl(G_GATE_CMD_CREATE, &ggioc); if (unit == -1) { printf("%s%u\n", G_GATE_PROVIDER_NAME, ggioc.gctl_unit); @@ -865,7 +931,8 @@ g_gatessh_rescue(void) error = pthread_create(&mediatd, NULL, mediachg, NULL); if (error != 0) - g_gate_xlog("unable to create mediasize change thread", strerror(errno)); + g_gate_xlog("unable to create mediasize change thread", + strerror(errno)); g_gatessh_loop(); } diff --git a/tests/ggatessh_test.sh b/tests/ggatessh_test.sh index 75dcb9e..fb08324 100644 --- a/tests/ggatessh_test.sh +++ b/tests/ggatessh_test.sh @@ -12,7 +12,7 @@ ggatessh_head() atf_set "descr" "ggatessh can proxy to sftp" atf_set "require.progs" "ggatessh" atf_set "require.user" "root" - atf_set "timeout" 20 + atf_set "timeout" 10 } ggatessh_body() @@ -32,9 +32,12 @@ ggatessh_body() # sshd authenticates and switches to USER chown "$USER" "$TESTIMG" - echo 'WARNING: ggatessh error messages goes to syslog (aka /var/log/messages)' + echo 'WARNING: ggatessh error messages goes to syslog' \ + '(aka /var/log/messages)' - atf_check ggatessh create -i "$(pwd)/id_rsa" -p "$PORT" -F "$PIDFILE" -u $us -l "$USER" 127.0.0.1 "$(pwd)/$TESTIMG" + atf_check \ + ggatessh create -i "$(pwd)/id_rsa" -p "$PORT" -F "$PIDFILE" \ + -u $us -l "$USER" 127.0.0.1 "$(pwd)/$TESTIMG" ggate_dev=/dev/ggate${us} @@ -50,11 +53,13 @@ EOF # Test writing atf_check -e ignore -o ignore \ - dd if="$TEMPFILE" of=${ggate_dev} bs=1m count=$n1mchunks conv=notrunc + dd if="$TEMPFILE" of=${ggate_dev} bs=1m count=$n1mchunks \ + conv=notrunc # Test reading atf_check -e ignore -o ignore \ - dd of="$TEMPFILE"2 if=${ggate_dev} bs=1m count=$n1mchunks conv=notrunc + dd of="$TEMPFILE"2 if=${ggate_dev} bs=1m count=$n1mchunks \ + conv=notrunc # Verify that we read what we wrote atf_check cmp "$TEMPFILE" "$TEMPFILE"2 @@ -93,9 +98,12 @@ ggatessh_resize_body() # sshd authenticates and switches to USER chown "$USER" "$TESTIMG" - echo 'WARNING: ggatessh error messages goes to syslog (aka /var/log/messages)' + echo 'WARNING: ggatessh error messages goes to syslog' \ + '(aka /var/log/messages)' - atf_check ggatessh create -i "$(pwd)/id_rsa" -p "$PORT" -F "$PIDFILE" -u $us -l "$USER" 127.0.0.1 "$(pwd)/$TESTIMG" + atf_check \ + ggatessh create -i "$(pwd)/id_rsa" -p "$PORT" -F "$PIDFILE" \ + -u $us -l "$USER" 127.0.0.1 "$(pwd)/$TESTIMG" ggate_dev=/dev/ggate${us} @@ -122,7 +130,9 @@ EOF sleep 1 # restart ggate - atf_check ggatessh rescue -v -i "$(pwd)/id_rsa" -p "$PORT" -F "$PIDFILE" -u $us -l "$USER" 127.0.0.1 "$(pwd)/$TESTIMG" & + atf_check \ + ggatessh rescue -v -i "$(pwd)/id_rsa" -p "$PORT" -F "$PIDFILE" \ + -u $us -l "$USER" 127.0.0.1 "$(pwd)/$TESTIMG" & sleep 1 @@ -143,10 +153,104 @@ ggatessh_resize_cleanup() common_cleanup } +atf_test_case ggatessh_rowotest cleanup +ggatessh_rowotest_head() +{ + atf_set "descr" "ggatessh properly handles the -o flag" + atf_set "require.progs" "ggatessh" + atf_set "require.user" "root" + atf_set "timeout" 10 +} + +ggatessh_rowotest_body() +{ + + n1mchunks=4 + secsize=4096 + us=$(alloc_ggate_dev) + + startup_sshd + + truncate -s ${n1mchunks}m "$TESTIMG" + # sshd authenticates and switches to USER + chmod 444 "$TESTIMG" + + echo 'WARNING: ggatessh error messages goes to syslog' \ + '(aka /var/log/messages)' + + # make sure it fails in rw mode + atf_check -s not-exit:0 \ + ggatessh create -i "$(pwd)/id_rsa" -p "$PORT" -F "$PIDFILE" \ + -u $us -l "$USER" 127.0.0.1 "$(pwd)/$TESTIMG" + + # open it in read-only mode + atf_check \ + ggatessh create -o ro -i "$(pwd)/id_rsa" -p "$PORT" -F "$PIDFILE" \ + -u $us -l "$USER" 127.0.0.1 "$(pwd)/$TESTIMG" + + ggate_dev=/dev/ggate${us} + + wait_for_ggate_device ${ggate_dev} + + # make sure it has correct size and sector sizekj + read _dev _secsize _size _nsecs _stripesize _stripeoff </dev/null + + # test write-only + chmod 222 "$TESTIMG" + + # make sure it fails in rw mode + atf_check -s not-exit:0 \ + ggatessh create -i "$(pwd)/id_rsa" -p "$PORT" -F "$PIDFILE" \ + -u $us -l "$USER" 127.0.0.1 "$(pwd)/$TESTIMG" + + # open it in write-only mode + atf_check \ + ggatessh create -o wo -i "$(pwd)/id_rsa" -p "$PORT" \ + -F "$PIDFILE" -u $us -l "$USER" 127.0.0.1 "$(pwd)/$TESTIMG" + + ggate_dev=/dev/ggate${us} + + wait_for_ggate_device ${ggate_dev} + + # Note: diskinfo opens w/ read, can't verify device info + + # that we can not read a write-only ggate device + atf_check -e ignore -s not-exit:0 \ + dd if=${ggate_dev} of=/dev/null bs=1m count=$n1mchunks + + # that we can write a write-only ggate device + atf_check -e ignore \ + dd of=${ggate_dev} if=/dev/zero bs=1m count=1 +} + +ggatessh_rowotest_cleanup() +{ + + common_cleanup +} + atf_init_test_cases() { atf_add_test_case ggatessh atf_add_test_case ggatessh_resize + atf_add_test_case ggatessh_rowotest } alloc_ggate_dev() @@ -175,11 +279,15 @@ alloc_md() # https://serverfault.com/questions/344295/is-it-possible-to-run-sshd-as-a-normal-user startup_sshd() { + # =============================================================== + # Note: using shorter keys to speed up tests, these are insecure. + # =============================================================== + # Host keys - ssh-keygen -f ssh_host_rsa_key -N '' -t rsa > /dev/null + ssh-keygen -f ssh_host_rsa_key -b 1024 -N '' -t rsa > /dev/null # user key - ssh-keygen -f id_rsa -N '' -t rsa > /dev/null + ssh-keygen -f id_rsa -b 1024 -N '' -t rsa > /dev/null (echo -n 'command="/usr/libexec/sftp-server" '; cat id_rsa.pub) > authorized_keys