diff --git a/auth.c b/auth.c
index 3b380d9bbd20..c6ca6dc8ef0d 100644
--- a/auth.c
+++ b/auth.c
@@ -381,7 +381,7 @@ auth_root_allowed(struct ssh *ssh, const char *method)
  * This returns a buffer allocated by xmalloc.
  */
 char *
-expand_authorized_keys(const char *filename, struct passwd *pw)
+expand_user_file(const char *filename, struct passwd *pw, const char *filetype)
 {
 	char *file, uidstr[32], ret[PATH_MAX];
 	int i;
@@ -400,7 +400,7 @@ expand_authorized_keys(const char *filename, struct passwd *pw)
 
 	i = snprintf(ret, sizeof(ret), "%s/%s", pw->pw_dir, file);
 	if (i < 0 || (size_t)i >= sizeof(ret))
-		fatal("expand_authorized_keys: path too long");
+		fatal("expand_user_file (%s): path too long", filetype);
 	free(file);
 	return (xstrdup(ret));
 }
@@ -410,7 +410,8 @@ authorized_principals_file(struct passwd *pw)
 {
 	if (options.authorized_principals_file == NULL)
 		return NULL;
-	return expand_authorized_keys(options.authorized_principals_file, pw);
+	return expand_user_file(options.authorized_principals_file, pw,
+	    "AuthorizedPrincipals");
 }
 
 /* return ok if key exists in sysfile or userfile */
diff --git a/auth.h b/auth.h
index 6d2d3976234e..2a6e43535b9b 100644
--- a/auth.h
+++ b/auth.h
@@ -190,7 +190,7 @@ int	bsdauth_respond(void *, u_int, char **);
 int	allowed_user(struct ssh *, struct passwd *);
 struct passwd * getpwnamallow(struct ssh *, const char *user);
 
-char	*expand_authorized_keys(const char *, struct passwd *pw);
+char	*expand_user_file(const char *, struct passwd *pw, const char *);
 char	*authorized_principals_file(struct passwd *);
 
 int	 auth_key_is_revoked(struct sshkey *);
diff --git a/auth2-pubkey.c b/auth2-pubkey.c
index 3f49e1df3b7d..94b3556ce7ca 100644
--- a/auth2-pubkey.c
+++ b/auth2-pubkey.c
@@ -777,8 +777,8 @@ user_key_allowed(struct ssh *ssh, struct passwd *pw, struct sshkey *key,
 	for (i = 0; !success && i < options.num_authkeys_files; i++) {
 		if (strcasecmp(options.authorized_keys_files[i], "none") == 0)
 			continue;
-		file = expand_authorized_keys(
-		    options.authorized_keys_files[i], pw);
+		file = expand_user_file(options.authorized_keys_files[i], pw,
+		    "AuthorizedKeys");
 		success = user_key_allowed2(pw, key, file,
 		    remote_ip, remote_host, &opts);
 		free(file);
diff --git a/servconf.c b/servconf.c
index 4b434909ab2f..5093e2273526 100644
--- a/servconf.c
+++ b/servconf.c
@@ -115,6 +115,7 @@ initialize_server_options(ServerOptions *options)
 	options->x11_use_localhost = -1;
 	options->permit_tty = -1;
 	options->permit_user_rc = -1;
+	options->num_user_rc_files = 0;
 	options->xauth_location = NULL;
 	options->strict_modes = -1;
 	options->tcp_keep_alive = -1;
@@ -141,6 +142,7 @@ initialize_server_options(ServerOptions *options)
 	options->permit_empty_passwd = -1;
 	options->permit_user_env = -1;
 	options->permit_user_env_allowlist = NULL;
+	options->num_user_env_files = 0;
 	options->compression = -1;
 	options->rekey_limit = -1;
 	options->rekey_interval = -1;
@@ -332,6 +334,11 @@ fill_default_server_options(ServerOptions *options)
 		options->permit_tty = 1;
 	if (options->permit_user_rc == -1)
 		options->permit_user_rc = 1;
+	if (options->num_user_rc_files == 0)
+		opt_array_append("[default]", 0, "UserRCFiles",
+		    &options->user_rc_files,
+		    &options->num_user_rc_files,
+		    _PATH_SSH_USER_RC);
 	if (options->strict_modes == -1)
 		options->strict_modes = 1;
 	if (options->tcp_keep_alive == -1)
@@ -372,6 +379,11 @@ fill_default_server_options(ServerOptions *options)
 		options->permit_user_env = 0;
 		options->permit_user_env_allowlist = NULL;
 	}
+	if (options->num_user_env_files == 0)
+		opt_array_append("[default]", 0, "UserEnvFiles",
+		    &options->user_env_files,
+		    &options->num_user_env_files,
+		    _PATH_SSH_USER_DIR "/environment");
 	if (options->compression == -1)
 #ifdef WITH_ZLIB
 		options->compression = COMP_DELAYED;
@@ -508,7 +520,8 @@ typedef enum {
 	sPrintMotd, sPrintLastLog, sIgnoreRhosts,
 	sX11Forwarding, sX11DisplayOffset, sX11UseLocalhost,
 	sPermitTTY, sStrictModes, sEmptyPasswd, sTCPKeepAlive,
-	sPermitUserEnvironment, sAllowTcpForwarding, sCompression,
+	sPermitUserEnvironment, sUserEnvironmentFile,
+	sAllowTcpForwarding, sCompression,
 	sRekeyLimit, sAllowUsers, sDenyUsers, sAllowGroups, sDenyGroups,
 	sIgnoreUserKnownHosts, sCiphers, sMacs, sPidFile, sModuliFile,
 	sGatewayPorts, sPubkeyAuthentication, sPubkeyAcceptedAlgorithms,
@@ -526,7 +539,7 @@ typedef enum {
 	sAuthorizedPrincipalsCommand, sAuthorizedPrincipalsCommandUser,
 	sKexAlgorithms, sCASignatureAlgorithms, sIPQoS, sVersionAddendum,
 	sAuthorizedKeysCommand, sAuthorizedKeysCommandUser,
-	sAuthenticationMethods, sHostKeyAgent, sPermitUserRC,
+	sAuthenticationMethods, sHostKeyAgent, sPermitUserRC, sUserRCFile,
 	sStreamLocalBindMask, sStreamLocalBindUnlink,
 	sAllowStreamLocalForwarding, sFingerprintHash, sDisableForwarding,
 	sExposeAuthInfo, sRDomain, sPubkeyAuthOptions, sSecurityKeyProvider,
@@ -628,6 +641,7 @@ static struct {
 	{ "strictmodes", sStrictModes, SSHCFG_GLOBAL },
 	{ "permitemptypasswords", sEmptyPasswd, SSHCFG_ALL },
 	{ "permituserenvironment", sPermitUserEnvironment, SSHCFG_GLOBAL },
+	{ "userenvironmentfile", sUserEnvironmentFile, SSHCFG_GLOBAL },
 	{ "uselogin", sDeprecated, SSHCFG_GLOBAL },
 	{ "compression", sCompression, SSHCFG_GLOBAL },
 	{ "rekeylimit", sRekeyLimit, SSHCFG_ALL },
@@ -663,6 +677,7 @@ static struct {
 	{ "permittunnel", sPermitTunnel, SSHCFG_ALL },
 	{ "permittty", sPermitTTY, SSHCFG_ALL },
 	{ "permituserrc", sPermitUserRC, SSHCFG_ALL },
+	{ "userrcfile", sUserRCFile, SSHCFG_ALL },
 	{ "match", sMatch, SSHCFG_ALL },
 	{ "permitopen", sPermitOpen, SSHCFG_ALL },
 	{ "permitlisten", sPermitListen, SSHCFG_ALL },
@@ -1673,6 +1688,30 @@ process_server_config_line_depth(ServerOptions *options, char *line,
 		intptr = &options->permit_user_rc;
 		goto parse_flag;
 
+	/*
+	 * These options can contain %X options expanded at
+	 * connect time, so that you can specify paths like:
+	 *
+	 * UserRCFile	/etc/ssh_user_rc/%u
+	 */
+	case sUserRCFile:
+		value = options->num_user_rc_files;
+		while ((arg = argv_next(&ac, &av)) != NULL) {
+			if (*arg == '\0') {
+				error("%s line %d: keyword %s empty argument",
+				    filename, linenum, keyword);
+				goto out;
+			}
+			arg2 = tilde_expand_filename(arg, getuid());
+			if (*activep && value == 0) {
+				opt_array_append(filename, linenum, keyword,
+				    &options->user_rc_files,
+				    &options->num_user_rc_files, arg2);
+			}
+			free(arg2);
+		}
+		break;
+
 	case sStrictModes:
 		intptr = &options->strict_modes;
 		goto parse_flag;
@@ -1711,6 +1750,30 @@ process_server_config_line_depth(ServerOptions *options, char *line,
 		free(p);
 		break;
 
+	/*
+	 * These options can contain %X options expanded at
+	 * connect time, so that you can specify paths like:
+	 *
+	 * UserEnvironmentFile	/etc/ssh_user_env/%u
+	 */
+	case sUserEnvironmentFile:
+		value = options->num_user_env_files;
+		while ((arg = argv_next(&ac, &av)) != NULL) {
+			if (*arg == '\0') {
+				error("%s line %d: keyword %s empty argument",
+				    filename, linenum, keyword);
+				goto out;
+			}
+			arg2 = tilde_expand_filename(arg, getuid());
+			if (*activep && value == 0) {
+				opt_array_append(filename, linenum, keyword,
+				    &options->user_env_files,
+				    &options->num_user_env_files, arg2);
+			}
+			free(arg2);
+		}
+		break;
+
 	case sCompression:
 		intptr = &options->compression;
 		multistate_ptr = multistate_compression;
@@ -3175,6 +3238,10 @@ dump_config(ServerOptions *o)
 	/* string array arguments */
 	dump_cfg_strarray_oneline(sAuthorizedKeysFile, o->num_authkeys_files,
 	    o->authorized_keys_files);
+	dump_cfg_strarray_oneline(sUserRCFile, o->num_user_rc_files,
+	    o->user_rc_files);
+	dump_cfg_strarray_oneline(sUserEnvironmentFile, o->num_user_env_files,
+	    o->user_env_files);
 	dump_cfg_strarray(sHostKeyFile, o->num_host_key_files,
 	    o->host_key_files);
 	dump_cfg_strarray(sHostCertificate, o->num_host_cert_files,
diff --git a/servconf.h b/servconf.h
index ed7b72e8e0e3..ec1a4f9afdc3 100644
--- a/servconf.h
+++ b/servconf.h
@@ -233,6 +233,11 @@ typedef struct {
 	u_int	num_channel_timeouts;
 
 	int	unused_connection_timeout;
+
+	u_int   num_user_env_files;     /* alternative .ssh/environment files */
+	u_int   num_user_rc_files;      /* alternative .ssh/rc files */
+	char   **user_env_files;
+	char   **user_rc_files;
 }       ServerOptions;
 
 /* Information about the incoming connection as used by Match */
@@ -281,6 +286,8 @@ TAILQ_HEAD(include_list, include_item);
 		M_CP_STROPT(routing_domain); \
 		M_CP_STROPT(permit_user_env_allowlist); \
 		M_CP_STRARRAYOPT(authorized_keys_files, num_authkeys_files); \
+		M_CP_STRARRAYOPT(user_rc_files, num_user_rc_files); \
+		M_CP_STRARRAYOPT(user_env_files, num_user_env_files); \
 		M_CP_STRARRAYOPT(allow_users, num_allow_users); \
 		M_CP_STRARRAYOPT(deny_users, num_deny_users); \
 		M_CP_STRARRAYOPT(allow_groups, num_allow_groups); \
diff --git a/session.c b/session.c
index c821dcd4462d..3025762cc24f 100644
--- a/session.c
+++ b/session.c
@@ -832,6 +832,48 @@ check_quietlogin(Session *s, const char *command)
 	return 0;
 }
 
+/*
+ * Check that any custom user environment/rc file is "safe" to run, i.e.
+ * - If the file is located in the logged-in user's $HOME, it is owned by the
+ *   user and accessible *only* to the user
+ * - If the file is located outside the user's $HOME, it is owned by the super
+ *   user
+ */
+static int
+safe_user_file(const char *filename, struct passwd *pw)
+{
+	struct stat st;
+	char *path;
+	int safe = 0;
+
+	if (stat(filename, &st) == -1)
+		return safe;
+
+	path = realpath(filename, NULL);
+	if (strncmp(path, pw->pw_dir, strlen(pw->pw_dir)) == 0) {
+		if (st.st_uid != pw->pw_uid) {
+			fprintf(stderr, "Bad owner on %s, ignoring.\n", path);
+			goto out;
+		}
+		if ((st.st_mode & 077) != 0) {
+			fprintf(stderr,
+			    "Permissions too open (%3o) on %s, ignoring.\n",
+			    st.st_mode & 0777u, path);
+			goto out;
+		}
+		safe = 1;
+		goto out;
+	}
+	if (st.st_uid != 0 || st.st_gid != 0) {
+		fprintf(stderr, "%s not root-owned, ignoring.\n", path);
+		goto out;
+	}
+	safe = 1;
+out:
+	free(path);
+	return safe;
+}
+
 /*
  * Reads environment variables from the given file and adds/overrides them
  * into the environment.  If the file does not exist, this does nothing.
@@ -984,7 +1026,7 @@ do_setup_env(struct ssh *ssh, Session *s, const char *shell)
 	char buf[256];
 	size_t n;
 	u_int i, envsize;
-	char *ocp, *cp, *value, **env, *laddr;
+	char *ocp, *cp, *value, **env, *laddr, *userenv = NULL;
 	struct passwd *pw = s->pw;
 #if !defined (HAVE_LOGIN_CAP) && !defined (HAVE_CYGWIN)
 	char *path = NULL;
@@ -1116,12 +1158,16 @@ do_setup_env(struct ssh *ssh, Session *s, const char *shell)
 		}
 	}
 
-	/* read $HOME/.ssh/environment. */
+	/* read user environment files. */
 	if (options.permit_user_env) {
-		snprintf(buf, sizeof buf, "%.200s/%s/environment",
-		    pw->pw_dir, _PATH_SSH_USER_DIR);
-		read_environment_file(&env, &envsize, buf,
-		    options.permit_user_env_allowlist);
+		for (i = 0; i < options.num_user_env_files; i++) {
+			userenv = expand_user_file(options.user_env_files[i], pw,
+			    "UserEnvironment");
+			if (safe_user_file(userenv, pw))
+				read_environment_file(&env, &envsize, userenv,
+				    options.permit_user_env_allowlist);
+		}
+		free(userenv);
 	}
 
 #ifdef USE_PAM
@@ -1204,29 +1250,34 @@ do_rc_files(struct ssh *ssh, Session *s, const char *shell)
 	char *cmd = NULL, *user_rc = NULL;
 	int do_xauth;
 	struct stat st;
+	u_int i;
 
 	do_xauth =
 	    s->display != NULL && s->auth_proto != NULL && s->auth_data != NULL;
-	xasprintf(&user_rc, "%s/%s", s->pw->pw_dir, _PATH_SSH_USER_RC);
 
 	/* ignore _PATH_SSH_USER_RC for subsystems and admin forced commands */
 	if (!s->is_subsystem && options.adm_forced_command == NULL &&
-	    auth_opts->permit_user_rc && options.permit_user_rc &&
-	    stat(user_rc, &st) >= 0) {
-		if (xasprintf(&cmd, "%s -c '%s %s'", shell, _PATH_BSHELL,
-		    user_rc) == -1)
-			fatal_f("xasprintf: %s", strerror(errno));
-		if (debug_flag)
-			fprintf(stderr, "Running %s\n", cmd);
-		f = popen(cmd, "w");
-		if (f) {
-			if (do_xauth)
-				fprintf(f, "%s %s\n", s->auth_proto,
-				    s->auth_data);
-			pclose(f);
-		} else
-			fprintf(stderr, "Could not run %s\n",
-			    user_rc);
+	    auth_opts->permit_user_rc && options.permit_user_rc) {
+		for (i = 0; i < options.num_user_rc_files; i++) {
+			user_rc = expand_user_file(options.user_rc_files[i],
+			    s->pw, "UserRC");
+			if (safe_user_file(user_rc, s->pw)) {
+				if (xasprintf(&cmd, "%s -c '%s %s'", shell,
+				    _PATH_BSHELL, user_rc) == -1)
+					fatal_f("xasprintf: %s", strerror(errno));
+				if (debug_flag)
+					fprintf(stderr, "Running %s\n", cmd);
+				f = popen(cmd, "w");
+				if (f) {
+					if (do_xauth)
+						fprintf(f, "%s %s\n", s->auth_proto,
+						    s->auth_data);
+					pclose(f);
+				} else
+					fprintf(stderr, "Could not run %s\n",
+					    user_rc);
+			}
+		}
 	} else if (stat(_PATH_SSH_SYSTEM_RC, &st) >= 0) {
 		if (debug_flag)
 			fprintf(stderr, "Running %s %s\n", _PATH_BSHELL,