Blob Blame History Raw
--- /dev/null
+++ b/slog.c
@@ -0,0 +1,619 @@
+/*
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ */
+
+ /* When using slogctxt in any module perform a NULL pointer check */
+
+#include <sys/types.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <string.h>
+#include <syslog.h>
+#include <pwd.h>
+#include <time.h>
+
+#include "includes.h"
+#include "slog.h"
+#include "log.h"
+#include "misc.h"
+#include "servconf.h"
+#include "xmalloc.h"
+
+typedef struct Structuredlogctxt Structuredlogctxt;
+
+struct Structuredlogctxt { /* items we care about logging */
+	char		server_ip[SLOG_SHORT_STRING_LEN];		// server_ip
+	int		server_port;		// server_port
+	char		remote_ip[SLOG_SHORT_STRING_LEN];		// remote_ip
+	int		remote_port;		// remote_port
+	pid_t		pam_process_pid;		// pam_pid
+	char		session_id[SLOG_STRING_LEN];		// session_id
+	char		method[SLOG_STRING_LEN];		// method
+	char		cert_id[SLOG_STRING_LEN];		// cert_id
+	unsigned long long		cert_serial;		// cert_serial
+	char		principal[SLOG_STRING_LEN];		// principal
+	char		user[SLOG_STRING_LEN];		// user
+	char		command[SLOG_LONG_STRING_LEN];		// command
+	SLOG_SESSION_STATE		session_state;		// session_state
+	SLOG_AUTHENTICATED		auth_successful;		// auth_successful
+	time_t		start_time;		// start_time
+	time_t		end_time;		// end_time
+	int		duration;		// duration
+	pid_t		main_pid;		// main_pid
+	char		auth_info[SLOG_MEDIUM_STRING_LEN];		// auth_info
+	char		client_version[SLOG_STRING_LEN];		// client_version
+};
+
+Structuredlogctxt *slogctxt;
+extern ServerOptions options;
+
+// Define token constants and default syntax format
+static const char *server_ip_token         = "server_ip";
+static const char *server_port_token       = "server_port";
+static const char *remote_ip_token         = "remote_ip";
+static const char *remote_port_token       = "remote_port";
+static const char *pam_pid_token           = "pam_pid";
+static const char *process_pid_token       = "pid";
+static const char *session_id_token        = "session_id";
+static const char *method_token            = "method";
+static const char *cert_id_token           = "cert_id";
+static const char *cert_serial_token       = "cert_serial";
+static const char *principal_token         = "principal";
+static const char *user_token              = "user";
+static const char *command_token           = "command";
+static const char *session_state_token     = "session_state";
+static const char *auth_successful_token   = "auth_successful";
+static const char *start_time_token        = "start_time";
+static const char *end_time_token          = "end_time";
+static const char *duration_token          = "duration";
+static const char *main_pid_token          = "main_pid";
+static const char *auth_info_token         = "auth_info";
+static const char *client_version          = "client_version";
+
+/* Example log format sshd_config
+ * LogFormatPrefix sshd_auth_msg:
+ * LogFormatKeys server_ip server_port remote_ip remote_port pid session_id /
+   method cert_id cert_serial principal user session_state auth_successful /
+   start_time command # NO LINE BREAKS
+ * LogFormatJson yes # no makes this output an json array vs json object
+ */
+
+// Set a arbitrary large size so we can feed a potentially
+// large json object to the logger
+#define SLOG_BUF_SIZE 8192
+#define SLOG_TRUNCATED_MESSAGE_JSON ", \"incomplete\": \"true\"}"
+#define SLOG_TRUNCATED_MESSAGE_ARRAY ", \"incomplete\"]"
+#define SLOG_TRUNCATED_SIZE 25
+/* size of format for JSON for quotes_colon_space around key and comma_space
+   after value or closure_null */
+#define SLOG_JSON_FORMAT_SIZE 6
+#define SLOG_BUF_CALC_SIZE  SLOG_BUF_SIZE - SLOG_TRUNCATED_SIZE
+
+/* private declarations */
+static void slog_log(void);
+static void slog_cleanup(void);
+static void slog_generate_auth_payload(char *);
+static void slog_escape_value(char *, char *, size_t);
+static void slog_get_safe_from_token(char *, const char *);
+static const char* slog_get_state_text(void);
+
+/* public functions */
+
+void
+slog_init(void)
+{
+	/* initialize only if we have log_format_keys */
+	if (options.num_log_format_keys != 0) {
+		slogctxt = xcalloc(1, sizeof(Structuredlogctxt));
+		if (slogctxt != NULL)
+			slog_cleanup();
+	}
+}
+
+void
+slog_pam_session_opened(void)
+{
+	if (slogctxt != NULL) {
+		slogctxt->session_state = SLOG_SESSION_OPEN;
+		slogctxt->pam_process_pid = getpid();
+	}
+}
+
+void
+slog_set_auth_data(int authenticated, const char *method, const char *user)
+{
+	if (slogctxt != NULL) {
+		slogctxt->auth_successful =
+		    authenticated ? SLOG_AUTHORIZED : SLOG_UNAUTHORIZED;
+		strlcpy(slogctxt->method, method, SLOG_SHORT_STRING_LEN);
+		strlcpy(slogctxt->user, user, SLOG_STRING_LEN);
+	}
+}
+
+void
+slog_set_cert_id(const char *id)
+{
+	if (slogctxt != NULL)
+		strlcpy(slogctxt->cert_id, id, SLOG_STRING_LEN);
+}
+
+
+void
+slog_set_cert_serial(unsigned long long serial)
+{
+	if (slogctxt != NULL)
+		slogctxt->cert_serial = serial;
+}
+
+void
+slog_set_connection(const char *remote_ip, int remote_port,
+    const char *server_ip, int server_port, const char *session)
+{
+	if (slogctxt != NULL) {
+		strlcpy(slogctxt->remote_ip, remote_ip, SLOG_SHORT_STRING_LEN);
+		slogctxt->remote_port = remote_port;
+		strlcpy(slogctxt->server_ip, server_ip, SLOG_SHORT_STRING_LEN);
+		slogctxt->server_port = server_port;
+		strlcpy(slogctxt->session_id, session, SLOG_STRING_LEN);
+		slogctxt->start_time = time(NULL);
+		slogctxt->main_pid = getpid();
+	}
+}
+
+void
+slog_set_client_version(const char *version)
+{
+	if (slogctxt != NULL) {
+		if (strlen(version) < SLOG_STRING_LEN)
+			strlcpy(slogctxt->client_version, version, SLOG_STRING_LEN);
+		else {
+			// version can be up to 256 bytes, truncate to 95 and add ' ...'
+			// which will fit in SLOG_STRING_LEN
+			snprintf(slogctxt->client_version, SLOG_STRING_LEN, "%.95s ...", version);
+		}
+	}
+}
+
+void
+slog_set_command(const char *command)
+{
+	if (slogctxt != NULL) {
+		if (strlen(command) < SLOG_LONG_STRING_LEN)
+			strlcpy(slogctxt->command, command, SLOG_LONG_STRING_LEN);
+		else {
+			// If command is longer than allowed we truncate it to
+			// 1995 (SLOG_LONG_STRING_LEN - 5) characters and add ' ...\0' to
+			// the end of the command.
+			snprintf(slogctxt->command, SLOG_LONG_STRING_LEN, "%.1995s ...", command);
+		}
+	}
+}
+
+void
+slog_set_principal(const char *principal)
+{
+	if (slogctxt != NULL)
+		strlcpy(slogctxt->principal, principal, SLOG_STRING_LEN);
+}
+
+void
+slog_set_user(const char *user)
+{
+	if (slogctxt != NULL)
+		strlcpy(slogctxt->user, user, SLOG_STRING_LEN);
+}
+
+void
+slog_set_auth_info(const char *auth_info)
+{
+	if (slogctxt != NULL)
+		strlcpy(slogctxt->auth_info, auth_info, SLOG_MEDIUM_STRING_LEN);
+}
+
+void
+slog_exit_handler(void)
+{
+	/* to prevent duplicate logging we only log based on the pid set */
+	if (slogctxt != NULL) {
+		if (slogctxt->server_ip[0] == 0)
+			return; // not initialized
+		if (slogctxt->main_pid != getpid())
+			return; // not main process
+		if (slogctxt->session_state == SLOG_SESSION_INIT)
+			slog_log();
+		else {
+			slogctxt->session_state = SLOG_SESSION_CLOSED;
+			slogctxt->end_time = time(NULL);
+			slogctxt->duration = slogctxt->end_time - slogctxt->start_time;
+			slog_log();
+			slog_cleanup();
+		}
+  }
+}
+
+void
+slog_log_session(void)
+{
+	if (slogctxt != NULL) {
+		slogctxt->session_state = SLOG_SESSION_OPEN;
+		slog_log();
+	}
+}
+
+/* private function scope begin */
+
+static void
+slog_log(void)
+{
+	char *buffer = xmalloc(SLOG_BUF_SIZE);
+
+	if (buffer == NULL)
+		return;
+
+	memset(buffer, 0, SLOG_BUF_SIZE);
+
+	if (options.num_log_format_keys > 0
+	    && slogctxt != NULL
+	    && slogctxt->server_ip[0] != 0
+	    && slogctxt->user[0] != 0) {
+		slog_generate_auth_payload(buffer);
+		do_log_slog_payload(buffer);
+	}
+
+	free(buffer);
+}
+
+static void
+slog_cleanup(void)
+{
+	// Reset the log context values
+	if (slogctxt != NULL) {
+		memset(slogctxt, 0, sizeof(Structuredlogctxt));
+		slogctxt->session_state = SLOG_SESSION_INIT;
+		slogctxt->auth_successful = SLOG_UNAUTHORIZED;
+	}
+}
+
+/* We use debug3 since the debug is very noisy */
+static void
+slog_generate_auth_payload(char *buf)
+{
+	if (buf == NULL)
+		return;
+
+	// Create large buffer so don't risk overflow
+	char *safe = xmalloc(SLOG_BUF_SIZE);
+	memset(safe, 0, SLOG_BUF_SIZE);
+
+	if (safe == NULL)
+		return;
+
+	int i;
+	size_t remaining;
+	int json = options.log_format_json;
+	int keys = options.num_log_format_keys;
+	int truncated = 0;
+	char *tmpbuf = buf;
+	char *key;
+
+	debug3("JSON format is %d with %d tokens.", json, keys);
+
+	if (options.log_format_prefix != NULL
+	    && strlen(options.log_format_prefix) < SLOG_BUF_CALC_SIZE-1) {
+		tmpbuf += snprintf(tmpbuf, SLOG_BUF_CALC_SIZE,
+	    "%s ", options.log_format_prefix);
+	}
+	*tmpbuf++ = (json) ? '{' : '[';
+	debug3("current buffer after prefix: %s", buf);
+
+	// Loop through the keys filling out the output string
+	for (i = 0; i < keys; i++) {
+		safe[0] = 0;  // clear safe string
+		key = options.log_format_keys[i];
+		remaining = SLOG_BUF_CALC_SIZE - (tmpbuf - buf);
+
+		if (key == NULL)
+			continue;  // Shouldn't happen but if null go to next key
+
+		slog_get_safe_from_token(safe, key);
+		debug3("token: %s, value: %s", key, safe);
+
+		if (json) {
+			if (*safe == '\0')
+				continue; // No value since we are using key pairs skip
+			if (remaining <= SLOG_JSON_FORMAT_SIZE + strlen(key) + strlen(safe)) {
+				debug("Log would exceed buffer size %u, %zu, %zu at key: %s",
+				    (unsigned int)remaining, strlen(key), strlen(safe), key);
+				truncated = 1;
+				break;
+			}
+			tmpbuf += snprintf(tmpbuf, remaining, "%s\"%s\": %s",
+			    i > 0 ? ", " : "", key, safe);
+		} else {
+			if (*safe == '\0')
+				strlcpy(safe, "\"\"", SLOG_SHORT_STRING_LEN);
+			if (remaining < SLOG_JSON_FORMAT_SIZE + strlen(safe)) {
+				debug("Log would exceed remaining buffer size %d, %zu, at key: %s",
+				    (unsigned int)remaining, strlen(safe), key);
+				truncated = 1;
+				break;
+			}
+			tmpbuf += snprintf(tmpbuf, remaining, "%s%s", i > 0 ? ", " : "", safe);
+		}
+		debug3("current buffer after token: %s", buf);
+		debug3("end of loop key: %s, %d out of %d keys", key, i + 1, keys);
+	}
+
+	// Close the log string. If truncated set truncated message and close string
+	if (truncated == 1)
+		strlcpy(tmpbuf, json ? SLOG_TRUNCATED_MESSAGE_JSON :
+		    SLOG_TRUNCATED_MESSAGE_ARRAY, SLOG_TRUNCATED_SIZE);
+	else {
+		*tmpbuf++ = (json) ? '}' : ']';
+	}
+
+	free(safe);
+}
+
+// buffer size is input string * 2 +1
+static void
+slog_escape_value(char *output, char *input, size_t buffer_size)
+{
+	int i;
+	buffer_size -= 2;
+	if (input != NULL) {
+		int input_size = strlen(input);
+		char *temp = output;
+		*temp++ = '"';
+		buffer_size--;
+		for (i = 0; i < input_size && buffer_size > 0; i++) {
+			switch(input[i]) {
+			// characters escaped are the same as folly::json::escapeString
+			case 27: // <escape> ascii control character
+				if (buffer_size > 6) {
+					*temp++ = '\\';
+					*temp++ = 'u';
+					*temp++ = '0';
+					*temp++ = '0';
+					*temp++ = '1';
+					*temp++ = 'b';
+					buffer_size -= 6;
+				}
+			case '\b':
+				if (buffer_size > 1) {
+					*temp++ = '\\';
+					*temp++ = 'b';
+					buffer_size -= 2;
+				}
+				break;
+			case '\f':
+				if (buffer_size > 1) {
+					*temp++ = '\\';
+					*temp++ = 'f';
+					buffer_size -= 2;
+				}
+				break;
+			case '\n':
+				if (buffer_size > 1) {
+					*temp++ = '\\';
+					*temp++ = 'n';
+					buffer_size -= 2;
+					}
+				break;
+			case '\r':
+				if (buffer_size > 1) {
+					*temp++ = '\\';
+					*temp++ = 'r';
+					buffer_size -= 2;
+				}
+				break;
+			case '\t':
+				if (buffer_size > 1) {
+					*temp++ = '\\';
+					*temp++ = 't';
+					buffer_size -= 2;
+				}
+				break;
+			case '\"':
+			case '\\':
+				if (buffer_size > 1) {
+					*temp++ = '\\';
+					buffer_size--;
+			}
+			default:  // Non-escape char
+				*temp++ = input[i];
+				buffer_size--;
+			}
+		}
+		*temp++ = '"';
+		*temp++ = '\0';
+	}
+}
+
+static void
+slog_get_safe_from_token(char *output, const char *token)
+{
+	if (output == NULL || token == NULL || slogctxt == NULL)
+		return;
+
+	if (strcmp(token, server_ip_token) == 0) {
+		if (slogctxt->server_ip[0] != 0) {
+			snprintf(output, SLOG_SHORT_STRING_LEN, "\"%s\"",
+			    slogctxt->server_ip);
+		}
+		return;
+	}
+
+	if (strcmp(token, server_port_token) == 0) {
+		if (slogctxt->server_port > 0) {
+			snprintf(output, SLOG_SHORT_STRING_LEN, "\"%d\"",
+			slogctxt->server_port);
+		}
+		return;
+	}
+
+	if (strcmp(token, remote_ip_token) == 0) {
+		if (slogctxt->remote_ip[0] != 0) {
+			snprintf(output, SLOG_SHORT_STRING_LEN, "\"%s\"",
+			    slogctxt->remote_ip);
+		}
+		return;
+	}
+
+	if (strcmp(token, remote_port_token) == 0) {
+		if (slogctxt->remote_port > 0) {
+			snprintf(output, SLOG_SHORT_STRING_LEN, "\"%d\"",
+			    slogctxt->remote_port);
+		}
+		return;
+	}
+
+	if (strcmp(token, pam_pid_token) == 0) {
+		if (slogctxt->pam_process_pid > 0) {
+			snprintf(output, SLOG_SHORT_STRING_LEN, "\"%ld\"",
+			    (long)slogctxt->pam_process_pid);
+		}
+		return;
+	}
+
+	if (strcmp(token, process_pid_token) == 0) {
+		snprintf(output, SLOG_SHORT_STRING_LEN, "\"%ld\"", (long)getpid());
+		return;
+	}
+
+	if (strcmp(token, session_id_token) == 0) {
+		if (slogctxt->session_id[0] != 0) {
+				snprintf(output, SLOG_STRING_LEN, "\"%s\"",
+				    slogctxt->session_id);
+		}
+		return;
+	}
+
+	if (strcmp(token, method_token) == 0) {
+		if (slogctxt->method[0] != 0) {
+				snprintf(output, SLOG_STRING_LEN, "\"%s\"",
+				    slogctxt->method);
+		}
+		return;
+	}
+
+	// Arbitrary input
+	if (strcmp(token, cert_id_token) == 0) {
+		if (slogctxt->cert_id[0] != 0 &&
+		    strcmp(slogctxt->method, "publickey") == 0) {
+				slog_escape_value(output, slogctxt->cert_id,
+				    SLOG_STRING_LEN * 2 + 1);
+		}
+		return;
+	}
+
+	if (strcmp(token, cert_serial_token) == 0) {
+		if (slogctxt->cert_serial > 0 &&
+		    strcmp(slogctxt->method, "publickey") == 0) {
+			snprintf(output, SLOG_SHORT_STRING_LEN, "\"%llu\"",
+			    slogctxt->cert_serial);
+		}
+		return;
+	}
+
+	// Arbitrary input
+	if (strcmp(token, principal_token) == 0) {
+		if (slogctxt->principal[0] != 0) {
+			slog_escape_value(output, slogctxt->principal,
+			    SLOG_STRING_LEN * 2 + 1);
+		}
+	return;
+	}
+
+	// Arbitrary input
+	if (strcmp(token, user_token) == 0) {
+		if (slogctxt->user[0] != 0) {
+			slog_escape_value(output, slogctxt->user,
+		    SLOG_STRING_LEN * 2 + 1);
+		}
+		return;
+	}
+
+	// Arbitrary input
+	if (strcmp(token, auth_info_token) == 0) {
+		if (slogctxt->auth_info[0] != 0) {
+			slog_escape_value(output, slogctxt->auth_info,
+			    SLOG_MEDIUM_STRING_LEN * 2 + 1);
+		}
+		return;
+	}
+
+	// Arbitrary input
+	if (strcmp(token, command_token) == 0) {
+		if (slogctxt->command[0] != 0) {
+			slog_escape_value(output, slogctxt->command,
+			    SLOG_LONG_STRING_LEN * 2 + 1);
+		}
+		return;
+	}
+
+	if (strcmp(token, auth_successful_token) == 0) {
+		snprintf(output, SLOG_SHORT_STRING_LEN, "\"%s\"",
+		    slogctxt->auth_successful == SLOG_AUTHORIZED ? "true" : "false");
+		return;
+	}
+
+	if (strcmp(token, session_state_token) == 0) {
+		snprintf(output, SLOG_SHORT_STRING_LEN, "\"%s\"",
+		    slog_get_state_text());
+		return;
+	}
+
+	if (strcmp(token, start_time_token) == 0) {
+		snprintf(output, SLOG_SHORT_STRING_LEN, "\"%d\"",
+		    (int)slogctxt->start_time);
+		return;
+	}
+
+	if (strcmp(token, end_time_token) == 0 && slogctxt->end_time > 0) {
+		snprintf(output, SLOG_SHORT_STRING_LEN, "\"%d\"",
+		    (int)slogctxt->end_time);
+		return;
+	}
+
+	if (strcmp(token, duration_token) == 0 && slogctxt->end_time > 0) {
+		snprintf(output, SLOG_SHORT_STRING_LEN, "\"%d\"", slogctxt->duration);
+		return;
+	}
+
+	if (strcmp(token, main_pid_token) == 0) {
+		if (slogctxt->main_pid > 0) {
+			snprintf(output, SLOG_SHORT_STRING_LEN, "\"%ld\"",
+			    (long)slogctxt->main_pid);
+		}
+		return;
+	}
+
+	// Arbitrary input
+	if (strncmp(token, client_version, strlen(client_version)) == 0) {
+		if (slogctxt->client_version[0] != 0) {
+			slog_escape_value(output, slogctxt->client_version,
+			    SLOG_STRING_LEN + 2);
+		}
+		return;
+	}
+}
+
+static const char *
+slog_get_state_text(void)
+{
+	if (slogctxt == NULL)
+		return "";
+
+	switch (slogctxt->session_state) {
+		case SLOG_SESSION_INIT:
+			return "Session failed";
+		case SLOG_SESSION_OPEN:
+			return "Session opened";
+		case SLOG_SESSION_CLOSED:
+			return "Session closed";
+		default:
+			return "Unknown session state";  // Should never happen
+	}
+}
--- a/servconf.c
+++ b/servconf.c
@@ -205,6 +205,9 @@ initialize_server_options(ServerOptions
 	options->disable_forwarding = -1;
 	options->expose_userauth_info = -1;
 	options->required_rsa_size = -1;
+ 	options->log_format_prefix = NULL;
+ 	options->num_log_format_keys = 0;
+ 	options->log_format_json = -1;
 }
 
 /* Returns 1 if a string option is unset or set to "none" or 0 otherwise. */
@@ -474,6 +477,8 @@ fill_default_server_options(ServerOption
 		options->sk_provider = xstrdup("internal");
 	if (options->required_rsa_size == -1)
 		options->required_rsa_size = SSH_RSA_MINIMUM_MODULUS_SIZE;
+ 	if (options->log_format_json == -1)
+ 		options->log_format_json = 0;
 
 	assemble_algorithms(options);
 
@@ -554,6 +559,10 @@ typedef enum {
 	sAllowStreamLocalForwarding, sFingerprintHash, sDisableForwarding,
 	sExposeAuthInfo, sRDomain, sPubkeyAuthOptions, sSecurityKeyProvider,
 	sRequiredRSASize,
+ 	/* Structured Logging options.  Unless sLogFormatKeys is set,
+ 	    structured logging is disabled */
+ 	sLogFormatPrefix, sLogFormatKeys, sLogFormatJson,
+
 	sDeprecated, sIgnore, sUnsupported
 } ServerOpCodes;
 
@@ -732,6 +741,9 @@ static struct {
 	{ "securitykeyprovider", sSecurityKeyProvider, SSHCFG_GLOBAL },
 	{ "requiredrsasize", sRequiredRSASize, SSHCFG_ALL },
 	{ "rsaminsize", sRequiredRSASize, SSHCFG_ALL }, /* alias */
+ 	{ "logformatprefix", sLogFormatPrefix, SSHCFG_GLOBAL },
+ 	{ "logformatkeys", sLogFormatKeys, SSHCFG_GLOBAL },
+ 	{ "logformatjson", sLogFormatJson, SSHCFG_GLOBAL },
 	{ NULL, sBadOption, 0 }
 };
 
@@ -2369,6 +2381,30 @@ process_server_config_line_depth(ServerO
 		}
 		break;
 
+	case sLogFormatPrefix:
+		arg = argv_next(&ac, &av);
+		if (!arg || *arg == '\0') {
+			fatal("%.200s line %d: invalid log format prefix",
+			    filename, linenum);
+		}
+		options->log_format_prefix = xstrdup(arg);
+		break;
+
+	case sLogFormatKeys:
+		while ((arg = argv_next(&ac, &av)) && *arg != '\0') {
+			if (options->num_log_format_keys >= MAX_LOGFORMAT_KEYS)
+				fatal("%s line %d: too long format keys.",
+				    filename, linenum);
+			if (!*activep)
+				continue;
+			options->log_format_keys[options->num_log_format_keys++] = xstrdup(arg);
+		}
+		break;
+
+	case sLogFormatJson:
+		intptr = &options->log_format_json;
+		goto parse_flag;
+
 	case sIPQoS:
 		arg = argv_next(&ac, &av);
 		if (!arg || *arg == '\0')
@@ -3026,6 +3062,7 @@ dump_config(ServerOptions *o)
 	dump_cfg_fmtint(sStreamLocalBindUnlink, o->fwd_opts.streamlocal_bind_unlink);
 	dump_cfg_fmtint(sFingerprintHash, o->fingerprint_hash);
 	dump_cfg_fmtint(sExposeAuthInfo, o->expose_userauth_info);
+	dump_cfg_fmtint(sLogFormatJson, o->log_format_json);
 
 	/* string arguments */
 	dump_cfg_string(sPidFile, o->pid_file);
@@ -3056,6 +3093,7 @@ dump_config(ServerOptions *o)
 #if defined(__OpenBSD__) || defined(HAVE_SYS_SET_PROCESS_RDOMAIN)
 	dump_cfg_string(sRDomain, o->routing_domain);
 #endif
+	dump_cfg_string(sLogFormatPrefix, o->log_format_prefix);
 
 	/* string arguments requiring a lookup */
 	dump_cfg_string(sLogLevel, log_level_name(o->log_level));
@@ -3078,6 +3116,7 @@ dump_config(ServerOptions *o)
 	    o->num_auth_methods, o->auth_methods);
 	dump_cfg_strarray_oneline(sLogVerbose,
 	    o->num_log_verbose, o->log_verbose);
+	dump_cfg_strarray(sLogFormatKeys, o->num_log_format_keys, o->log_format_keys);
 
 	/* other arguments */
 	for (i = 0; i < o->num_subsystems; i++)
--- a/auth2-pubkey.c
+++ b/auth2-pubkey.c
@@ -66,6 +66,7 @@
 #include "monitor_wrap.h"
 #include "authfile.h"
 #include "match.h"
+#include "slog.h"
 #include "ssherr.h"
 #include "channels.h" /* XXX for session.h */
 #include "session.h" /* XXX for child_set_env(); refactor? */
@@ -390,6 +391,7 @@ check_principals_line(struct ssh *ssh, c
 		debug3("%s: matched principal \"%.100s\"",
 		    loc, cert->principals[i]);
 		found = 1;
+		slog_set_principal(cp);
 	}
 	if (found && authoptsp != NULL) {
 		*authoptsp = opts;
@@ -715,6 +717,7 @@ check_authkey_line(struct ssh *ssh, stru
 	    (unsigned long long)key->cert->serial,
 	    sshkey_type(found), fp, loc);
 
+	    slog_set_cert_serial(key->cert->serial);
  success:
 	if (finalopts == NULL)
 		fatal_f("internal error: missing options");
@@ -865,6 +868,7 @@ user_cert_trusted_ca(struct ssh *ssh, st
 		*authoptsp = final_opts;
 		final_opts = NULL;
 	}
+	slog_set_cert_id(key->cert->key_id);
 	ret = 1;
  out:
 	sshauthopt_free(principals_opts);
--- a/regress/test-exec.sh
+++ b/regress/test-exec.sh
@@ -690,7 +690,7 @@ start_sshd ()
 
 	trace "wait for sshd"
 	i=0;
-	while [ ! -f $PIDFILE -a $i -lt 10 ]; do
+	while [ ! -f $PIDFILE -a $i -lt 3 ]; do
 		i=`expr $i + 1`
 		sleep $i
 	done
--- a/session.c
+++ b/session.c
@@ -96,6 +96,8 @@
 #include "monitor_wrap.h"
 #include "sftp.h"
 #include "atomicio.h"
+#include "slog.h"
+
 
 #if defined(KRB5) && defined(USE_AFS)
 #include <kafs.h>
@@ -753,6 +755,7 @@ do_exec(struct ssh *ssh, Session *s, con
 	    ssh_remote_ipaddr(ssh),
 	    ssh_remote_port(ssh),
 	    s->self);
+	    slog_log_session();
 
 #ifdef SSH_AUDIT_EVENTS
 	if (s->command != NULL || s->command_handle != -1)
@@ -1486,7 +1489,7 @@ do_setusercontext(struct passwd *pw)
 			perror("unable to set user context (setuser)");
 			exit(1);
 		}
-		/* 
+		/*
 		 * FreeBSD's setusercontext() will not apply the user's
 		 * own umask setting unless running with the user's UID.
 		 */
@@ -2163,6 +2166,7 @@ session_exec_req(struct ssh *ssh, Sessio
 	    (r = sshpkt_get_end(ssh)) != 0)
 		sshpkt_fatal(ssh, r, "%s: parse packet", __func__);
 
+	slog_set_command(command);
 	success = do_exec(ssh, s, command) == 0;
 	free(command);
 	return success;
@@ -2873,4 +2877,3 @@ session_get_remote_name_or_ip(struct ssh
 		remote = ssh_remote_ipaddr(ssh);
 	return remote;
 }
-
--- a/log.h
+++ b/log.h
@@ -133,4 +133,6 @@ void	 sshlogdirect(LogLevel, int, const
 #define logdie_fr(r, ...)	sshlogdie(__FILE__, __func__, __LINE__, 1, SYSLOG_LEVEL_ERROR, ssh_err(r), __VA_ARGS__)
 #define sigdie_fr(r, ...)	sshsigdie(__FILE__, __func__, __LINE__, 1, SYSLOG_LEVEL_ERROR, ssh_err(r), __VA_ARGS__)
 
+void     do_log_slog_payload(const char *);
+
 #endif
--- a/log.c
+++ b/log.c
@@ -531,3 +531,39 @@ sshlogdirect(LogLevel level, int forced,
 	do_log(level, forced, NULL, fmt, args);
 	va_end(args);
 }
+
+/* Custom function to log to syslog, to handle the session logging code
+ */
+void
+do_log_slog_payload(const char *payload)
+{
+#if defined(HAVE_OPENLOG_R) && defined(SYSLOG_DATA_INIT)
+	struct syslog_data sdata = SYSLOG_DATA_INIT;
+#endif
+	int pri = LOG_INFO;
+	int saved_errno = errno;
+	LogLevel level = SYSLOG_LEVEL_INFO;
+	log_handler_fn *tmp_handler;
+
+	if (log_handler != NULL) {
+		/* Avoid recursion */
+		tmp_handler = log_handler;
+		log_handler = NULL;
+		tmp_handler(level, 0, payload, log_handler_ctx);
+		log_handler = tmp_handler;
+	} else if (log_on_stderr) {
+		(void)write(log_stderr_fd, payload, strlen(payload));
+		(void)write(log_stderr_fd, "\r\n", 2);
+	} else {
+#if defined(HAVE_OPENLOG_R) && defined(SYSLOG_DATA_INIT)
+		openlog_r(argv0 ? argv0 : __progname, LOG_PID, log_facility, &sdata);
+		syslog_r(pri, &sdata, "%s", payload);
+		closelog_r(&sdata);
+#else
+		openlog(argv0 ? argv0 : __progname, LOG_PID, log_facility);
+		syslog(pri, "%s", payload);
+		closelog();
+#endif
+	}
+	errno = saved_errno;
+}
--- /dev/null
+++ b/slog.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ */
+#ifndef USE_SLOG
+#define USE_SLOG
+
+#define SLOG_STRING_LEN        100
+#define SLOG_MEDIUM_STRING_LEN 1000
+#define SLOG_SHORT_STRING_LEN  50
+#define SLOG_LONG_STRING_LEN   2000
+
+typedef enum {
+	SLOG_SESSION_INIT,
+	SLOG_SESSION_OPEN,
+	SLOG_SESSION_CLOSED,
+} SLOG_SESSION_STATE;
+
+typedef enum {
+	SLOG_UNAUTHORIZED = 0,
+	SLOG_AUTHORIZED = 1
+} SLOG_AUTHENTICATED;
+
+void	slog_init(void);
+
+// setters
+void	slog_pam_session_opened(void);
+void	slog_set_auth_data(int , const char *, const char *);
+void	slog_set_cert_id(const char *);
+void	slog_set_cert_serial(unsigned long long );
+void	slog_set_connection(const char *, int, const char *, int, const char *);
+void	slog_set_command(const char *);
+void	slog_set_principal(const char *);
+void	slog_set_user(const char *);
+void	slog_set_auth_info(const char *);
+void	slog_set_client_version(const char *);
+
+// loggers
+void	slog_exit_handler(void);
+void	slog_log_session(void);
+
+#endif
--- a/sshd.c
+++ b/sshd.c
@@ -132,6 +132,8 @@
 #include "sk-api.h"
 #include "srclimit.h"
 #include "dh.h"
+#include "slog.h"
+#include <time.h>
 
 /* Re-exec fds */
 #define REEXEC_DEVCRYPTO_RESERVED_FD	(STDERR_FILENO + 1)
@@ -2177,6 +2179,7 @@ main(int ac, char **av)
 	}
 	/* Reinitialize the log (because of the fork above). */
 	log_init(__progname, options.log_level, options.log_facility, log_stderr);
+	slog_init();
 
 	if (FIPS_mode()) {
 		debug("FIPS mode initialized");
@@ -2346,8 +2349,15 @@ main(int ac, char **av)
 	    rdomain == NULL ? "" : " rdomain \"",
 	    rdomain == NULL ? "" : rdomain,
 	    rdomain == NULL ? "" : "\"");
-	free(laddr);
 
+
+	slog_set_connection(remote_ip,
+	    remote_port,
+	    laddr,
+	    ssh_local_port(ssh),
+	    get_log_session_id());
+
+	free(laddr);
 	/*
 	 * We don't want to listen forever unless the other side
 	 * successfully authenticates itself.  So we set up an alarm which is
@@ -2674,6 +2684,7 @@ cleanup_exit(int i)
 	if (in_cleanup)
 		_exit(i);
 	in_cleanup = 1;
+	slog_exit_handler();
 	if (the_active_state != NULL && the_authctxt != NULL) {
 		do_cleanup(the_active_state, the_authctxt);
 		if (use_privsep && privsep_is_preauth &&
--- a/sshd_config
+++ b/sshd_config
@@ -33,6 +33,15 @@ Include /etc/ssh/sshd_config.d/*.conf
 # Logging
 #SyslogFacility AUTH
 #LogLevel INFO
+# Structured logging
+# The default is no structured logging, to enable structured logging at least
+# one LogFormatKeys must be set.  LogFormatJson defaults to no (array) to keep
+# the log line size smaller, if keys are desired set LogFormatJson to yes
+LogFormatPrefix sshd_auth_msg:
+LogFormatKeys server_ip server_port remote_ip remote_port pid session_id method
+LogFormatKeys cert_id cert_serial principal user session_state auth_successful
+LogFormatKeys start_time command
+LogFormatJson yes
 
 # Authentication:
 
--- a/auth-pam.c
+++ b/auth-pam.c
@@ -94,6 +94,7 @@ extern char *__progname;
 #include "auth-pam.h"
 #include "canohost.h"
 #include "log.h"
+#include "slog.h"
 #include "msg.h"
 #include "packet.h"
 #include "misc.h"
@@ -1210,9 +1211,12 @@ do_pam_session(struct ssh *ssh)
 	if (sshpam_err != PAM_SUCCESS)
 		fatal("PAM: failed to set PAM_CONV: %s",
 		    pam_strerror(sshpam_handle, sshpam_err));
+
 	sshpam_err = pam_open_session(sshpam_handle, 0);
-	if (sshpam_err == PAM_SUCCESS)
+	if (sshpam_err == PAM_SUCCESS) {
+		slog_pam_session_opened();
 		sshpam_session_open = 1;
+	}
 	else {
 		sshpam_session_open = 0;
 		auth_restrict_session(ssh);
--- a/servconf.h
+++ b/servconf.h
@@ -22,6 +22,8 @@
 
 #define MAX_SUBSYSTEMS		256	/* Max # subsystems. */
 
+#define MAX_LOGFORMAT_KEYS      256     /* Max # LogFormatKeys */
+
 /* permit_root_login */
 #define	PERMIT_NOT_SET		-1
 #define	PERMIT_NO		0
@@ -239,6 +241,12 @@ typedef struct {
 	u_int64_t timing_secret;
 	char   *sk_provider;
 	int	required_rsa_size;	/* minimum size of RSA keys */
+
+ 	char   *log_format_prefix;
+ 	u_int  num_log_format_keys;
+ 	char   *log_format_keys[MAX_LOGFORMAT_KEYS];
+ 	int log_format_json;	/* 1 to return "token": "token_val" in log format */
+
 }       ServerOptions;
 
 /* Information about the incoming connection as used by Match */
--- /dev/null
+++ b/regress/slog.sh
@@ -0,0 +1,59 @@
+tid='structured log'
+
+port="4242"
+log_prefix="sshd_auth_msg:"
+log_keys="server_ip server_port remote_ip remote_port pid session_id method cert_id cert_serial principal user session_state auth_successful _time command end_time duration auth_info client_version"
+do_log_json="yes"
+test_config="$OBJ/sshd2_config"
+old_config="$OBJ/sshd_config"
+PIDFILE=$OBJ/pidfile
+
+cat << EOF > $test_config
+	#*:
+	StrictModes             no
+	Port                    $port
+	AddressFamily           inet
+	ListenAddress           127.0.0.1
+	#ListenAddress          ::1
+	PidFile                 $PIDFILE
+	AuthorizedKeysFile      $OBJ/authorized_keys_%u
+	LogLevel                ERROR
+	AcceptEnv               _XXX_TEST_*
+	AcceptEnv               _XXX_TEST
+	HostKey $OBJ/host.ssh-ed25519
+	LogFormatPrefix $log_prefix
+	LogFormatJson $do_log_json
+	LogFormatKeys $log_keys
+EOF
+
+
+cp $test_config $old_config
+start_sshd
+
+${SSH} -F $OBJ/ssh_config somehost true
+if [ $? -ne 0 ]; then
+	fail "ssh connect with failed"
+fi
+
+test_log_counts() {
+	cnt=$(grep -c "$log_prefix" "$TEST_SSHD_LOGFILE")
+	if [ $cnt -ne 2 ]; then
+		fail "expected 2 structured logging lines, got $cnt"
+	fi
+}
+
+test_json_valid() {
+	which python &>/dev/null || echo 'python not found in path, skipping tests'
+
+	loglines=$(cat "$TEST_SSHD_LOGFILE" | grep "$log_prefix")
+	first=$(echo "$loglines" | head -n1)
+	last=$(echo "$loglines" | tail -n1)
+
+	echo ${first:$(expr length $log_prefix)} | python -m json.tool &>/dev/null \
+	    || fail "invalid json structure $first"
+	echo ${last:$(expr length $log_prefix)} | python -m json.tool &>/dev/null  \
+	    || fail "invalid json structure $last"
+}
+
+test_log_counts
+test_json_valid
--- a/Makefile.in
+++ b/Makefile.in
@@ -129,7 +129,7 @@ SSHDOBJS=sshd.o auth-rhosts.o auth-passw
 	monitor.o monitor_wrap.o auth-krb5.o \
 	auth2-gss.o gss-serv.o gss-serv-krb5.o kexgsss.o \
 	loginrec.o auth-pam.o auth-shadow.o auth-sia.o md5crypt.o \
-	srclimit.o sftp-server.o sftp-common.o \
+	srclimit.o sftp-server.o sftp-common.o sftp-realpath.o slog.o \
 	sandbox-null.o sandbox-rlimit.o sandbox-systrace.o sandbox-darwin.o \
 	sandbox-seccomp-filter.o sandbox-capsicum.o sandbox-pledge.o \
 	sandbox-solaris.o uidswap.o $(SKOBJS)
@@ -313,7 +313,7 @@ distclean:	regressclean
 	rm -f *.o *.a $(TARGETS) logintest config.cache config.log
 	rm -f *.out core opensshd.init openssh.xml
 	rm -f Makefile buildpkg.sh config.h config.status
-	rm -f survey.sh openbsd-compat/regress/Makefile *~ 
+	rm -f survey.sh openbsd-compat/regress/Makefile *~
 	rm -rf autom4te.cache
 	rm -f regress/check-perm
 	rm -f regress/mkdtemp
--- a/auth.c
+++ b/auth.c
@@ -76,6 +76,7 @@
 #include "ssherr.h"
 #include "compat.h"
 #include "channels.h"
+#include "slog.h"
 
 /* import */
 extern ServerOptions options;
@@ -351,6 +352,7 @@ auth_log(struct ssh *ssh, int authentica
 	    extra != NULL ? extra : "");
 
 	free(extra);
+	slog_set_auth_data(authenticated, method, authctxt->user);
 
 #if defined(CUSTOM_FAILED_LOGIN) || defined(SSH_AUDIT_EVENTS)
 	if (authenticated == 0 && !(authctxt->postponed || partial)) {
@@ -564,6 +566,7 @@ auth_openprincipals(const char *file, st
 struct passwd *
 getpwnamallow(struct ssh *ssh, const char *user)
 {
+	slog_set_user(user);
 #ifdef HAVE_LOGIN_CAP
 	extern login_cap_t *lc;
 #ifdef BSD_AUTH