Steffen Nurpmeso wrote in <20240704180538.iV4uex29@steffen%sdaoden.eu>: |Simon Josefsson wrote in | <87jzi1fg24.fsf@xxxxxxxxxxx>: ||Jochen Bern <Jochen.Bern@xxxxxxxxx> writes: ||> (And since you mention "port knocking", I'd like to repeat how fond I ||> am of upgrading that original concept to a single-packet ||> crypto-armored implementation like fwknop.) || ||I am reluctantly considering to use some kind of port knocking mechanism ||on some machines, however I really don't want to carry around shared ||symmetric keys or setup yet another public/private key infrastructure ||for that purpose. I already have a working infrastructure for SSH ||authentication. || ||Does anyone know of any implementation that allows me to configure a ||PGP/SSH/FIDO/TPM/whatever public key on the server side, and it then ||only listens to signed port knocks from the corresponding private keys? ... |No, but for many years i do have a super simple port-knock server |to do the I/O plus sh(1)ell based client which can do .. whatever. ... |With the possibilities that ssh-keygen -Y sign|verify have added, |one could easily adapt the server and client to send "user-name |MSG", so that the server could look into authorized_keys of |user-name and verify MSG, whatever that is. Hey! That vision of yours, in conjunction with that -Y possibility of ssh-keygen thrilled me so much i wrote a draft. It uses TLS over TCP to secure the packet. (Not UDP based, hm.) It is not yet fully worked out, but that draft i like, i will change to use that approach next week for sure -- no more becoming root locally in order to port knock, only need loaded ssh-agent! echo >&2 'SYNOPSIS: '$0' create-server-cert email-address filename' self-signed port-knock server cert creation. clients need to have the cert for TLS verification. echo >&2 'SYNOPSIS: '$0' create-ssh-key email-address filename' create a ssh key for port knock purposes. Users then knock via echo >&2 'SYNOPSIS: '$0' knock path-to-ssh-pubkey path-to-port-knock-bin host port server-cert' a little bit complicated yet. The C binary needs to be compiled via gcc -W -Wall -pedantic -o /tmp/zt port-knock-bin.c -lssl -lcrypto and then run via cd /tmp/ ./zt -v server ./.Z.key ./.Z.pub 10000 /tmp/port-knock.sh /tmp/.ZX.ALLO The client then does ./port-knock.sh knock .ZX.pub /tmp/zt localhost 10000 /tmp/.Z.pub Of course it is a play thing, but for you all it is sunday and maybe you like it. 'Will review and polish it on Monday. TLS client certificates and things like capsicum or pledge/unveil or missing for, also after Monday. --steffen | |Der Kragenbaer, The moon bear, |der holt sich munter he cheerfully and one by one |einen nach dem anderen runter wa.ks himself off |(By Robert Gernhardt)
#!/bin/sh - #@ port-knock.sh: port knock user interface (for port-knock-bin.c server). #@ The binary requires the *SSL library, the script openssl and ssh-keygen with -Y support. syno() { echo >&2 'TODO not yet SYNOPSIS: '$0' create-client-cert email-address filename' echo >&2 'SYNOPSIS: '$0' create-server-cert email-address filename' echo >&2 'SYNOPSIS: '$0' create-ssh-key email-address filename' echo >&2 'SYNOPSIS: '$0' knock path-to-ssh-pubkey path-to-port-knock-bin host port server-cert' echo >&2 '[SYNOPSIS: '$0' . sig-pool-file ip-address [sig-to-verify (else block)]' exit 64 # EX_USAGE } # # FIXME # Termination via SIGTERM; Usually started with a driver like start-stop-daemon, note --no-close and --output (to be # used with -v, otherwise freopen("/dev/null", "w", stderr) is used): # # ssd=start-stop-daemon # port_knock__init() { # name=port_knock # pid=/run/${name}.pid # prog=/root/port-knock-server # } # port_knock_start() { # port_knock__init # # --no-close --output /tmp/.port_knock.log # eval ${ssd} --start --background --make-pidfile --pidfile ${pid} --exec ${prog} -- ${PORT_KNOCK_ARGS} # } # port_knock_stop() { daemon__genstop port_knock; } # port_knock_status() { port_knock__init; daemons__stat_and_dog n; } # port_knock_watchdog() { port_knock__init; daemons__stat_and_dog y; } # # # 2020 - 2024 Steffen Nurpmeso <steffen@xxxxxxxxxx> # Public Domain blacklist() { logger -t /root/bin/port-knock-client.sh "black listing $1" /root/bin/net-qos.sh add alien_super "$1" /root/bin/net-qos.sh del port_knock "$1" } whitelist() { logger -t /root/bin/port-knock-client.sh "white listing $1 for 30 seconds" /root/bin/net-qos.sh del alien_super "$1" /root/bin/net-qos.sh del sshorvpn "$1" /root/bin/net-qos.sh del port_knock "$1" /root/bin/net-qos.sh eval fw_rules_ins 1 i^a^-src "$1" # /root/bin/net-qos.sh start-daemon sshd ( sleep 30 /root/bin/net-qos.sh eval eval fw_rules_del i^a^-src "$1" ) </dev/null >/dev/null 2>&1 & } case "$1" in create-client-cert) [ $# -ne 3 ] && syno echo >&2 'TODO note client certificates are not yet supported' exec openssl req -new -newkey $OPENSSL_KEY_ARGS -x509 -nodes -days 3333 \ -subj "/emailAddress=$2" \ -addext extendedKeyUsage=clientAuth -addext nsCertType=client \ -out "$3.pub" -keyout "$3.key" ;; create-server-cert) [ $# -ne 3 ] && syno exec openssl req -new -newkey $OPENSSL_KEY_ARGS -x509 -nodes -days 3333 \ -subj "/emailAddress=$2" \ -addext extendedKeyUsage=serverAuth -addext nsCertType=server \ -out "$3.pub" -keyout "$3.key" ;; create-ssh-key) [ $# -ne 3 ] && syno exec ssh-keygen -t ed25519 -C port-knock/"$2" -N '' -f "$3" ;; knock) [ $# -ne 6 ] && syno data=$(</dev/null ssh-keygen -Y sign -n port-knock -f "$2" | sed -Ee '/^-+BEGIN SSH/d;/^-+END SSH/d;s/$/ /' | tr -d '\012 ') "$3" -v client $6 $5 $4 "$data" ;; .) # PWD=/tmp/! ok= if [ $# -eq 4 ]; then # FIXME TMP FILE { echo '-----BEGIN SSH SIGNATURE-----' echo "$4" echo '-----END SSH SIGNATURE-----' } > /tmp/.TMP if ssh-keygen -Y find-principals -f "$2" -s /tmp/.TMP >/dev/null 2>&1; then ok=y; fi fi if [ -n "$ok" ]; then echo whitelist "$3" >> /tmp/.ZZZZ else echo blacklist "$3" >> /tmp/.ZZZZ fi ;; *) echo >&2 'Invalid command: '$1 syno ;; esac exit 0 # s-sht-mode
/*@ port-knock-bin.c: C backend for port-knock.sh; please see there. *@ TODO - client certificates (DB file? Or, empty file, name "is fingerprint") *@ TODO - capsicum / pledge/unveil (fork from server a forker process that forks+execv the command) * * Copyright (c) 2020 - 2024 Steffen Nurpmeso <steffen@xxxxxxxxxx>. * SPDX-License-Identifier: ISC * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #define _GNU_SOURCE #include <sys/select.h> #include <sys/socket.h> #include <sys/types.h> #include <netdb.h> #include <arpa/inet.h> #include <netinet/in.h> #include <openssl/err.h> #include <openssl/opensslv.h> #include <openssl/ssl.h> #include <ctype.h> #include <errno.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> /* For SO_{SND,RCV}TIMEO and SO_LINGER */ #define a_TIMEOUT_SECS 42 /* Bytes we should reserve for the largest possible SSH signature (ED25519=295, RSA=1560) + 1 */ #define a_SSH_SIG_LEN 2048 /* */ #define a_BIN_SH "/bin/sh" /* >8 -- 8< */ #if defined OPENSSL_VERSION_NUMBER && !defined LIBRESSL_VERSION_NUMBER &&\ OPENSSL_VERSION_NUMBER + 0 >= 0x30000000L # define a_SSL_CTX_load_verify_file(CTXP,FILE) SSL_CTX_load_verify_file(CTXP, FILE) #else # define a_SSL_CTX_load_verify_file(CTXP,FILE) SSL_CTX_load_verify_locations(CTXP, FILE, NULL) #endif enum{ a_EX_OK, a_EX_ERR, a_EX_USAGE = 64, a_EX_NOHOST = 68, a_EX_UNAVAILABLE = 69, a_EX_OSERR = 71, a_EX_IOERR = 74 }; static int a_verbose; static int volatile a_sig_seen; static int a_server(SSL_CTX *ctxp, int port, char const *cmd_path, char const *sigpool); static int a_client(SSL_CTX *ctxp, char const *host, char const *port, char const *data); /* Logs and closes sofd on error */ static SSL *a_sock_init(SSL_CTX *ctxp, int sofd); static void a_sig_hdl(int sig); int main(int argc, char **argv){ char const *prog, *host, *port; int es, srv, portno; SSL_CTX *ctxp; ctxp = NULL; es = 1; prog = (argc == 0) ? "port-knock" : argv[0]; port = host = NULL; /* xxx UNINIT(..,NULL); */ if(argc < 6) goto jesyn; fclose(stdin); fclose(stdout); if(!strcmp(argv[1], "-v")){ a_verbose = 1; --argc, ++argv; }else freopen("/dev/null", "w", stderr); if(!strcmp(argv[1], "server")){ srv = 1; if(argc != 7) goto jesyn; }else if(!strcmp(argv[1], "client")){ srv = 0; if(argc < 6 || argc > 7) goto jesyn; }else goto jesyn; ++es; ctxp = SSL_CTX_new(srv ? TLS_server_method() : TLS_client_method()); if(ctxp == NULL){ fprintf(stderr, "Cannot create TLS context: %s\n", ERR_error_string(ERR_get_error(), NULL)); goto jleave; } SSL_CTX_set_mode(ctxp, SSL_MODE_AUTO_RETRY); ++es; if(srv){ if(SSL_CTX_use_PrivateKey_file(ctxp, argv[2], SSL_FILETYPE_PEM) != 1){ fprintf(stderr, "Cannot use serv-privkey-file: %s: %s\n", argv[2], ERR_error_string(ERR_get_error(), NULL)); goto jesyn; } ++es; } if(srv || argc > 6){ if(SSL_CTX_use_certificate_chain_file(ctxp, argv[2 + srv]) != 1){ fprintf(stderr, "Cannot use (serv-pubkey|cli-cert)-file: %s: %s\n", argv[2 + srv], ERR_error_string(ERR_get_error(), NULL)); goto jesyn; } if(!srv) fprintf(stderr, "TODO Client certificates are not yet supported (ignored)\n"); --argc, ++argv; ++es; } if(!srv){ SSL_CTX_set_verify(ctxp, SSL_VERIFY_PEER, NULL); if(a_SSL_CTX_load_verify_file(ctxp, argv[2]) != 1){ fprintf(stderr, "Client cannot load CA serv-pubkey-file: %s: %s\n", argv[2], ERR_error_string(ERR_get_error(), NULL)); goto jesyn; } ++es; } portno = atoi(port = argv[3]); if(portno <= 0 || portno > 65535){ fprintf(stderr, "Bad port (must be >0 and <65536): %s -> %d\n", argv[3], portno); goto jesyn; } ++es; if(!srv){ host = argv[4]; if(*host == '\0'){ fprintf(stderr, "Empty hostname given\n"); goto jesyn; } --argc, ++argv; } ++es; /* TODO [chroot], pledge/unveil */ if(chdir("/") == -1){ fprintf(stderr, "Cannot chdir(2) to /\n"); goto jleave; } ++es; es = srv ? a_server(ctxp, portno, argv[4], argv[5]) : a_client(ctxp, host, port, argv[4]); jleave: if(ctxp != NULL) SSL_CTX_free(ctxp); return es; jesyn: fprintf(stderr, "Synopsis: %s [-v] server serv-privkey-file serv-pubkey-file port cmd-path sigpool-path\n" "Synopsis: %s [-v] client [cli-cert-file] serv-pubkey-file port host data\n", prog, prog); goto jleave; } static int a_server(SSL_CTX *ctxp, int portno, char const *cmd, char const *sigpool){ char dbuf[a_SSH_SIG_LEN], nbuf[INET6_ADDRSTRLEN +1]; char const *argv[7]; struct sigaction siac; struct sockaddr_storage soa_buf; union {struct sockaddr_storage *ps; struct sockaddr *p4; struct sockaddr_in6 *p6;} soa; socklen_t soal; int clifd, sofd, es; SSL *sslp; sslp = NULL; clifd = -1; while((sofd = socket(AF_INET6, SOCK_STREAM, 0)) == -1){ es = errno; if(es != EINTR){ fprintf(stderr, "Server socket creation failed: %s\n", strerror(es)); goto jleave; } } soa.ps = &soa_buf; memset(soa.ps, 0, sizeof(soa_buf)); soa.p6->sin6_family = AF_INET6; soa.p6->sin6_port = htons(portno); memcpy(&soa.p6->sin6_addr, &in6addr_any, sizeof soa.p6->sin6_addr); while(bind(sofd, (struct sockaddr const*)soa.p6, sizeof(*soa.p6)) == -1){ es = errno; if(es != EINTR){ fprintf(stderr, "Server socket cannot bind: %s\n", strerror(es)); goto jleave; } } while(listen(sofd, 1) == -1){ es = errno; if(es != EINTR){ fprintf(stderr, "Server socket cannot bind: %s\n", strerror(es)); goto jleave; } } memset(&siac, 0, sizeof siac); siac.sa_handler = &a_sig_hdl; sigemptyset(&siac.sa_mask); sigaction(SIGTERM, &siac, NULL); /*if(a_verbose)*/ sigaction(SIGINT, &siac, NULL); siac.sa_handler = SIG_IGN; sigaction(SIGPIPE, &siac, NULL); sigaction(SIGCHLD, &siac, NULL); argv[0] = "sh"; argv[1] = cmd; argv[2] = "."; argv[3] = sigpool; /* [4] = IP, [[5] = SIG] (else block IP); ie, [5] is our trigger */ argv[4] = nbuf; argv[6] = NULL; while(!a_sig_seen){ soal = sizeof(soa_buf); clifd = accept(sofd, (struct sockaddr*)soa.ps, &soal); if(clifd == -1){ es = errno; if(es == EINTR) continue; fprintf(stderr, "Cannot accept new client: %s\n", strerror(es)); es = a_EX_OSERR; goto jleave; } sslp = a_sock_init(ctxp, clifd); if(sslp == NULL){ clifd = -1; es = a_EX_UNAVAILABLE; goto jleave; } if(inet_ntop(soa.ps->ss_family, soa.ps, nbuf, sizeof(nbuf)) == NULL){ fprintf(stderr, "IMPL ERROR: cannot inet_ntop() accept(2)ed client\n"); goto jclose; } if(a_verbose) fprintf(stderr, "Accepted client: %s\n", nbuf); argv[5] = NULL; if(SSL_accept(sslp) != 1){ fprintf(stderr, "Cannot SSL accept client: %s\n", ERR_error_string(ERR_get_error(), NULL)); goto jfork; } /* C99 */{ int yet, still; yet = 0; still = sizeof(dbuf) -1; for(;;){ es = SSL_read(sslp, &dbuf[(unsigned)yet], still); if(es <= 0){ int e; es = SSL_get_error(sslp, es); if(es == SSL_ERROR_NONE || es == SSL_ERROR_ZERO_RETURN) break; e = errno; if(es == SSL_ERROR_WANT_WRITE || es == SSL_ERROR_WANT_READ || (es == SSL_ERROR_SYSCALL && e == EINTR)) continue; fprintf(stderr, "Data receive failed after %d bytes: %s: %s\n", yet, strerror(e), ERR_error_string(es, NULL)); /* TODO too dangerous to blacklist: could be attack! */ goto jfork; } /* Buffer spaced so excess is error */ still -= es; if(still == 0) goto jfork; while(es-- > 0){ char c; c = dbuf[(unsigned)yet++]; if(((unsigned)c & 0x80) || iscntrl(c)) goto jfork; } } dbuf[(unsigned)yet] = '\0'; } for(;;){ es = SSL_shutdown(sslp); if(es == 1) break; if(es != 0){ int e; e = errno; es = SSL_get_error(sslp, es); if(es == SSL_ERROR_WANT_WRITE || es == SSL_ERROR_WANT_READ || (es == SSL_ERROR_SYSCALL && e == EINTR)) continue; /* XXX select(2) / nanosleep(2)? */ fprintf(stderr, "SSL shutdown failed: %s: %s\n", strerror(e), ERR_error_string(es, NULL)); /* TODO too dangerous to blacklist: could be attack! */ goto jfork; } } /* argv[5] tells it like it is */ if(dbuf[0] != '\0') argv[5] = dbuf; jfork: if(a_verbose) fprintf(stderr, "execv: " a_BIN_SH " %s %s %s %s %s\n", argv[1], argv[2], argv[3], argv[4], (argv[5] != NULL ? argv[5] : "")); switch(fork()){ case -1: es = errno; fprintf(stderr, "Fork failed: %s\n", strerror(es)); es = a_EX_OSERR; goto jleave; default: break; case 0: execv(a_BIN_SH, (char*const*)argv); for(;;) _exit(a_EX_OSERR); } jclose: /* C99 */{ union {SSL *sslp; int fd;} t; t.sslp = sslp; sslp = NULL; SSL_free(t.sslp); t.fd = clifd; clifd = -1; close(t.fd); } } es = a_EX_OK; jleave: if(sslp != NULL) SSL_free(sslp); if(clifd != -1) close(clifd); if(sofd != -1) close(sofd); return es; } static int a_client(SSL_CTX *ctxp, char const *host, char const *port, char const *data){ struct addrinfo hints, *res0, *res; SSL *sslp; int sofd, es; memset(&hints, 0, sizeof hints); hints.ai_flags = AI_ADDRCONFIG | AI_V4MAPPED | AI_NUMERICSERV; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; sofd = -1; sslp = NULL; res0 = NULL; es = getaddrinfo(host, port, &hints, &res0); if(es != 0){ fprintf(stderr, "DNS lookup failed for: %s:%s %s\n", host, port, gai_strerror(es)); es = a_EX_NOHOST; goto jleave; } for(res = res0; res != NULL; res = res->ai_next){ for(;;){ sofd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if(sofd != -1) goto jso; es = errno; if(es != EINTR) break; } fprintf(stderr, "Socket creation failed: %s\n", strerror(es)); } es = a_EX_UNAVAILABLE; goto jleave; jso: es = connect(sofd, res->ai_addr, res->ai_addrlen); if(es == -1){ es = errno; if(es == EINTR) goto jso; fprintf(stderr, "Cannot connect to %s:%s: %s\n", host, port, strerror(es)); es = a_EX_UNAVAILABLE; goto jleave; } sslp = a_sock_init(ctxp, sofd); if(sslp == NULL){ sofd = -1; es = a_EX_UNAVAILABLE; goto jleave; } SSL_set_tlsext_host_name(sslp, host); if(SSL_connect(sslp) != 1){ fprintf(stderr, "Cannot SSL connect: %s\n", ERR_error_string(ERR_get_error(), NULL)); es = a_EX_IOERR; goto jleave; } /* C99 */{ size_t l, yet; l = strlen(data); yet = 0; while(l > 0){ es = SSL_write(sslp, &data[yet], l); if(es <= 0){ es = SSL_get_error(sslp, es); if(es != SSL_ERROR_NONE){ int e; e = errno; if(es == SSL_ERROR_WANT_WRITE || es == SSL_ERROR_WANT_READ || (es == SSL_ERROR_SYSCALL && e == EINTR)) continue; fprintf(stderr, "Data transmit failed: %s: %s\n", strerror(e), ERR_error_string(es, NULL)); es = a_EX_IOERR; goto jleave; } } yet += es; l -= es; } } for(;;){ es = SSL_shutdown(sslp); if(es == 1) break; if(es != 0){ int e; es = SSL_get_error(sslp, es); e = errno; if(es == SSL_ERROR_WANT_WRITE || es == SSL_ERROR_WANT_READ || (es == SSL_ERROR_SYSCALL && e == EINTR)) continue; /* XXX select(2) / nanosleep(2)? */ fprintf(stderr, "SSL shutdown failed: %s: %s\n", strerror(e), ERR_error_string(es, NULL)); es = a_EX_IOERR; goto jleave; } } es = a_EX_OK; jleave: if(sslp != NULL) SSL_free(sslp); if(sofd != -1) close(sofd); return es; } static SSL * a_sock_init(SSL_CTX *ctxp, int sofd){ struct timeval tv; struct linger li; SSL *sslp; tv.tv_sec = a_TIMEOUT_SECS; tv.tv_usec = 0; (void)setsockopt(sofd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); (void)setsockopt(sofd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); li.l_onoff = 1; li.l_linger = a_TIMEOUT_SECS; (void)setsockopt(sofd, SOL_SOCKET, SO_LINGER, &li, sizeof li); sslp = SSL_new(ctxp); if(sslp == NULL){ fprintf(stderr, "Cannot create SSL connection object: %s\n", ERR_error_string(ERR_get_error(), NULL)); goto jleave; } if(!SSL_set_fd(sslp, sofd)){ fprintf(stderr, "Cannot correlate file descriptor with SSL: %s\n", ERR_error_string(ERR_get_error(), NULL)); goto jleave; } jleave: if(sslp == NULL) close(sofd); return sslp; } static void a_sig_hdl(int sig){ (void)sig; a_sig_seen = 1; } /* s-itt-mode */
_______________________________________________ openssh-unix-dev mailing list openssh-unix-dev@xxxxxxxxxxx https://lists.mindrot.org/mailman/listinfo/openssh-unix-dev