Viewing: sec_gssiam_srv.c

// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (c) 2026, Google
 *
 * GSSIAM Security Policy for Lustre server
 *
 */
#define DEBUG_SUBSYSTEM S_SEC

#include <linux/module.h>
#include <linux/string.h>
#include <linux/random.h>
#include <obd.h>
#include <obd_class.h>
#include <lustre_net.h>
#include <lustre_sec.h>
#include <lustre_nodemap.h>
#include <upcall_cache.h>
#include <lustre_gssiam.h>

#include "../ptlrpc_internal.h"
#include "gss_internal.h"
#include "gss_api.h"
#include "gss_err.h"

static void gssiam_delete_sec_context(void *internal_ctx_id)
{
	struct gss_svc_ctx *ctx = internal_ctx_id;
	struct lu_nodemap *nodemap;

	nodemap = xchg(&ctx->gsc_gssiam_nodemap, NULL);
	if (nodemap)
		nodemap_putref(nodemap);
}

static struct gss_api_ops gssiam_ops = {
	.gss_delete_sec_context = gssiam_delete_sec_context,
};

static struct gss_api_mech gssiam_mech = {
	.gm_owner = NULL,
	.gm_name  = "gssiam",
	.gm_ops   = &gssiam_ops,
};

static int gssiam_update_nodemap_identity(const char *nm_name,
				  const struct lustre_gssiam_info *gssiam_info)
{
	char *identity;
	int len;
	const char *principal = gssiam_info->lii_desc.lid_principal ?
				gssiam_info->lii_desc.lid_principal : "";
	const char *subdir = gssiam_info->lii_desc.lid_subdir ?
			     gssiam_info->lii_desc.lid_subdir : "";
	const char *auth_str = "deny";
	int rc;

	if (gssiam_info->lii_auth_permission & GSSIAM_AUTH_RW)
		auth_str = "rw";
	else if (gssiam_info->lii_auth_permission & GSSIAM_AUTH_RO)
		auth_str = "ro";
	else if (gssiam_info->lii_auth_permission & GSSIAM_AUTH_EXPIRED)
		auth_str = "expired";

	len = strlen(principal) + strlen(subdir) + strlen(auth_str) + 3;
	OBD_ALLOC(identity, len);
	if (!identity)
		return -ENOMEM;

	snprintf(identity, len, "%s:%s:%s", principal, subdir,
		 auth_str);
	rc = nodemap_set_identity(nm_name, identity);
	OBD_FREE(identity, len);
	return rc;
}

/**
 * gssiam_get_nodemap() - Find or create nodemap for a GSSIAM connection
 * @target:	OBD device.
 * @gssiam_info: GSSIAM info (Project ID and options).
 *
 * This function handles the lifecycle of GSSIAM-specific nodemaps.
 * It first ensures the "gssiam" parent nodemap exists (creating and
 * hardening it if necessary). It then looks up or creates a unique
 * nodemap for the specific IAM identity/token and updates its policy
 * (Project ID, RO/RW status, and Fileset) to match the IAM policy.
 *
 * Return: pointer to struct lu_nodemap on success,
 * or ERR_PTR(rc) on failure.
 */
static struct lu_nodemap *gssiam_get_nodemap(struct obd_device *target,
				   const struct lustre_gssiam_info *gssiam_info)
{
	char nm_name[LUSTRE_NODEMAP_NAME_LENGTH + 1];
	char add_name[LUSTRE_NODEMAP_NAME_LENGTH + 16];
	const char *subdir = gssiam_info->lii_desc.lid_subdir;
	struct lu_nodemap *nodemap;
	int rc;

	/*
	 * If dynamically created, it inherits from 'default'. To prevent
	 * unauthenticated clients or GSSIAM clients from accidentally
	 * inheriting permissive 'default' access, we explicitly lock down
	 * the 'gssiam' parent nodemap upon creation.
	 */
	nodemap = nodemap_lookup_unlocked("gssiam");
	if (IS_ERR(nodemap)) {
		rc = nodemap_add("default/gssiam", true);
		if (rc == 0) {
			rc = nodemap_set_gssiam_managed("gssiam", true);
			if (rc)
				return ERR_PTR(rc);
			rc = nodemap_set_allow_root("gssiam", false);
			if (rc)
				return ERR_PTR(rc);
			rc = nodemap_set_trust_client_ids("gssiam", false);
			if (rc)
				return ERR_PTR(rc);
			/* Allow child dynamic nodemaps to raise admin privileges
			 * (allow_root) when authorized. By default, allow_root is
			 * false on the parent so rootsquash is enforced.
			 */
			rc = nodemap_set_raise_privs("gssiam",
						     NODEMAP_RAISE_PRIV_ADMIN,
						     NODEMAP_RBAC_NONE);
			if (rc)
				return ERR_PTR(rc);
		} else if (rc != -EEXIST) {
			return ERR_PTR(rc);
		}
	} else {
		nodemap_putref(nodemap);
	}

	gssiam_nodemap_name(nm_name, sizeof(nm_name),
			    gssiam_info->lii_desc.lid_token,
			    gssiam_info->lii_desc.lid_token_len, subdir,
			    gssiam_info->lii_desc.lid_projid,
			    gssiam_info->lii_auth_permission);

	nodemap = nodemap_lookup_unlocked(nm_name);
	if (IS_ERR(nodemap)) {
		/* Create as dynamic child */
		snprintf(add_name, sizeof(add_name), "gssiam/%s", nm_name);
		rc = nodemap_add(add_name, true);
		if (rc == 0 || rc == -EEXIST) {
			nodemap = nodemap_lookup_unlocked(nm_name);
			if (IS_ERR(nodemap)) {
				CERROR("%s: failed nodemap %s: rc = %ld\n",
				       target->obd_name, nm_name,
				       PTR_ERR(nodemap));
				return nodemap;
			}
		} else {
			CERROR("%s: add nodemap failed : rc = %d\n",
			       target->obd_name, rc);
			return ERR_PTR(rc);
		}
	}

	rc = gssiam_update_nodemap_identity(nm_name, gssiam_info);
	if (rc) {
		/* Let's ignore the identity update failure, though
		 * lctl get_param nodemap may not retrieve the real
		 * identity.
		 */
		CWARN("%s: failed to update identity on nodemap %s: rc = %d\n",
		      target->obd_name, nm_name, rc);
	}

	/*
	 * The gssiam_info contains the latest truth obtained from the
	 * GSSIAM authentication process (either from upcall or cache).
	 * Updating the nodemap here ensures that the connection's
	 * permissions (RO/RW) and resource attribution (ProjID) are
	 * perfectly synchronized with the central IAM policy.
	 */
	rc = nodemap_gssiam_attrs_update(nm_name,
					 gssiam_info->lii_desc.lid_projid,
					 (gssiam_info->lii_auth_permission &
					  GSSIAM_AUTH_RO) != 0, false);
	if (rc) {
		CERROR("%s: failed to update gssiam on nodemap %s: rc = %d\n",
		       target->obd_name, nm_name, rc);
		GOTO(out_put, rc);
	}

	/* Enforce the fileset specified by the IAM policy */
	if (subdir && strlen(subdir) > 0)
		rc = nodemap_fileset_add(nm_name, subdir, false,
					 false);
	else
		rc = nodemap_fileset_clear(nm_name, false);

	if (rc && rc != -EPERM && rc != -EEXIST) {
		CERROR("%s: failed to set fileset on nodemap %s: rc = %d\n",
		       target->obd_name, nm_name, rc);
		GOTO(out_put, rc);
	} else if (rc == -EPERM || rc == -EEXIST) {
		/* Fallback to the secure, inherited parent fileset if the
		 * client's requested fileset is broader (-EPERM) or different
		 * from the pre-existing inherited fileset (-EEXIST).
		 */
		rc = 0;
	}

	CDEBUG(D_SEC, "projid %u auth_perm 0x%x subdir %s\n",
	       gssiam_info->lii_desc.lid_projid,
	       gssiam_info->lii_auth_permission,
	       subdir ? subdir : "none");
out_put:
	if (rc) {
		nodemap_putref(nodemap);
		nodemap = ERR_PTR(rc);
	}

	return nodemap;
}

static void gssiam_ctx_install_nodemap(struct gss_svc_reqctx *grctx,
				       struct obd_export *exp)
{
	struct lu_nodemap *nodemap;

	nodemap = xchg(&grctx->src_ctx->gsc_gssiam_nodemap, NULL);
	if (!nodemap)
		return;

	nodemap_export_refresh(nodemap, exp);

	/*
	 * The nodemap is looked up and referenced during the SEC_CTX_INIT
	 * RPC (gssiam_handle_init()) and temporarily stored in the server
	 * context (gsc_gssiam_nodemap).
	 *
	 * When the subsequent CONNECT RPC arrives, we attach this nodemap
	 * to the client's export here. Since the export now holds its own
	 * reference to the nodemap (via nodemap_export_refresh), we drop
	 * the temporary reference held by the security context.
	 */
	nodemap_putref(nodemap);
}

/* Install the security context to the export */
int gssiam_install_ctx(struct obd_export *exp, struct ptlrpc_request *req)
{
	struct gss_svc_reqctx *grctx;
	struct gss_rsc *rscp;

	ENTRY;

	/*
	 * In GSSIAM's Hybrid RPC Model, only handshake (SEC_CTX_INIT) and
	 * Connect RPCs are GSS-framed. All other RPCs (regular Data/Metadata
	 * RPCs) use Null framing for performance.
	 *
	 * Skip static Null-framed service contexts (&gssiam_svc_ctx). Note that
	 * "Null-framed" here refers to unframed GSSIAM Data/Metadata RPCs (i.e.
	 * RPCs other than SEC_CTX_INIT & Connect), not the Null security
	 * flavor.
	 *
	 * Calling gss_svc_ctx2reqctx() on the 24-byte shared gssiam_svc_ctx
	 * would calculate an invalid container_of() pointer and dereference
	 * random memory in .data when inspecting grctx->src_ctx.
	 */
	if (!req->rq_svc_ctx || gssiam_is_null_svc_ctx(req->rq_svc_ctx))
		RETURN(0);

	grctx = gss_svc_ctx2reqctx(req->rq_svc_ctx);
	if (!grctx->src_ctx)
		RETURN(0);

	if (!grctx->src_ctx->gsc_gssiam_nodemap)
		RETURN(0);

	gssiam_ctx_install_nodemap(grctx, exp);

	rscp = container_of(grctx->src_ctx, struct gss_rsc, sc_ctx);
	/*
	 * The GSSIAM gss_svc_ctx is highly ephemeral. Once the identity is
	 * pinned to the export (the connection) above, the handshake is
	 * complete. We immediately invalidate the context so the cache
	 * garbage collector can free it via our GSSIAM GSS mechanism.
	 */
	upcall_cache_update_entry(rsccache, rscp->sc_uc_entry, 0,
				  UC_CACHE_INVALID);
	RETURN(0);
}

static int gssiam_verify_target(struct obd_device *target,
			  struct lustre_gssiam_desc *lid,
			  struct lustre_gssiam_info **gssiam_info,
			  time64_t *gssiam_expire, __u32 *major)
{
	/* XXX following patches will reset other values */
	*major = GSS_S_FAILURE;
	return -EOPNOTSUPP;
}

int gssiam_handle_init(struct ptlrpc_request *req, struct gss_svc_reqctx *grctx,
		       struct obd_device *target, rawobj_t *in_token,
		       __u32 **secdata, __u32 *seclen)
{
	rawobj_t subdir_obj = RAWOBJ_EMPTY;
	rawobj_t options_obj = RAWOBJ_EMPTY;
	struct lustre_gssiam_desc *lid = NULL;
	struct lustre_gssiam_info *gssiam_info = NULL;
	struct gss_rsc rsc_key;
	__u64 key_hash;
	struct gss_rsc *rscp = NULL;
	int replen = sizeof(struct ptlrpc_body);
	struct ptlrpc_reply_state *rs;
	struct gss_rep_header *rephdr;
	__u32 major = GSS_S_FAILURE;
	time64_t gssiam_expire = ktime_get_seconds() +
				 rsccache->uc_entry_expire;
	struct lu_nodemap *nodemap;
	int rc;

	if (rawobj_extract(&subdir_obj, secdata, seclen)) {
		rc = -EINVAL;
		major = GSS_S_DEFECTIVE_TOKEN;
		CDEBUG(D_SEC, "%s: can't extract subdirectory: rc = %d\n",
		       target->obd_name, rc);
		GOTO(out, rc);
	}

	if (rawobj_extract(&options_obj, secdata, seclen)) {
		rc = -EINVAL;
		major = GSS_S_DEFECTIVE_TOKEN;
		CDEBUG(D_SEC, "%s: can't extract client options: rc = %d\n",
		       target->obd_name, rc);
		GOTO(out, rc);
	}

	if (options_obj.len != sizeof(__u32)) {
		rc = -EINVAL;
		major = GSS_S_DEFECTIVE_TOKEN;
		CDEBUG(D_SEC,
		       "%s: invalid client options: got %u expect %zu: rc = %d\n",
		       target->obd_name, options_obj.len, sizeof(__u32), rc);
		GOTO(out, rc);
	}

	if (in_token->len > GSSIAM_MAX_TOKEN_LEN) {
		rc = -EINVAL;
		major = GSS_S_DEFECTIVE_TOKEN;
		CDEBUG(D_SEC,
		       "%s: in_token length too long (%u > %u): rc = %d\n",
		       target->obd_name, in_token->len, GSSIAM_MAX_TOKEN_LEN, rc);
		GOTO(out, rc);
	}

	if (subdir_obj.len > PATH_MAX) {
		rc = -EINVAL;
		major = GSS_S_DEFECTIVE_TOKEN;
		CDEBUG(D_SEC,
		       "%s: subdirectory path too long (%u > %u): rc = %d\n",
		       target->obd_name, subdir_obj.len, PATH_MAX, rc);
		GOTO(out, rc);
	}

	OBD_ALLOC_PTR(lid);
	if (!lid)
		GOTO(out, rc = -ENOMEM);

	lid->lid_options = le32_to_cpu(*(__le32 *)options_obj.data);
	lid->lid_token_len = in_token->len;
	if (lid->lid_token_len > 0) {
		OBD_ALLOC(lid->lid_token, lid->lid_token_len);
		if (!lid->lid_token)
			GOTO(out, rc = -ENOMEM);
		memcpy(lid->lid_token, in_token->data, lid->lid_token_len);
	}

	OBD_ALLOC(lid->lid_subdir, subdir_obj.len + 1);
	if (!lid->lid_subdir)
		GOTO(out, rc = -ENOMEM);

	memcpy(lid->lid_subdir, subdir_obj.data, subdir_obj.len);

	rc = gssiam_verify_target(target, lid, &gssiam_info, &gssiam_expire,
				  &major);
	if (rc) {
		CERROR("%s: GSSIAM verify failed: rc = %d\n", target->obd_name,
		       rc);
		GOTO(out, rc);
	}

	/* Create ephemeral context */
	memset(&rsc_key, 0, sizeof(rsc_key));
#ifdef HAVE_GET_RANDOM_U32_AND_U64
	key_hash = get_random_u64();
#else
	key_hash = ((__u64)prandom_u32() << 32) | prandom_u32();
#endif
	/* Use hash as handle */
	if (rawobj_alloc(&rsc_key.sc_handle, (char *)&key_hash,
			 sizeof(key_hash)))
		GOTO(out, rc = -ENOMEM);

	rscp = rsc_entry_get(rsccache, &rsc_key);
	rawobj_free(&rsc_key.sc_handle);
	if (IS_ERR(rscp)) {
		rc = PTR_ERR(rscp);
		CERROR("%s: failed to get rsc for GSSIAM: rc = %d\n",
		       target->obd_name, rc);
		GOTO(out, rc);
	}

	if (!rscp->sc_ctx.gsc_mechctx) {
		struct gss_ctx *gctx;

		OBD_ALLOC_PTR(gctx);
		if (!gctx) {
			rc = -ENOMEM;
			CERROR("%s: failed to alloc gctx for GSSIAM: rc = %d\n",
			       target->obd_name, rc);
			GOTO(out, rc);
		}

		gctx->mech_type = lgss_mech_get(&gssiam_mech);
		gctx->internal_ctx_id = &rscp->sc_ctx;
		rscp->sc_ctx.gsc_mechctx = gctx;
	}

	/* Locate or Create Nodemap */
	nodemap = gssiam_get_nodemap(target, gssiam_info);
	if (IS_ERR(nodemap)) {
		rc = PTR_ERR(nodemap);
		CERROR("%s: failed to get/create nodemap for GSSIAM: rc = %d\n",
		       target->obd_name, rc);
		GOTO(out, rc);
	}

	if (rscp->sc_ctx.gsc_gssiam_nodemap)
		nodemap_putref(rscp->sc_ctx.gsc_gssiam_nodemap);
	rscp->sc_ctx.gsc_gssiam_nodemap = nodemap;

	if (rscp->sc_ctx.gsc_nm_name == NULL) {
		OBD_ALLOC(rscp->sc_ctx.gsc_nm_name,
			  LUSTRE_NODEMAP_NAME_LENGTH + 1);
		if (!rscp->sc_ctx.gsc_nm_name) {
			rc = -ENOMEM;
			CERROR("%s: failed to alloc nodemap name for GSSIAM: rc = %d\n",
			       target->obd_name, rc);
			GOTO(out, rc);
		}
		strscpy(rscp->sc_ctx.gsc_nm_name, nodemap->nm_name,
			LUSTRE_NODEMAP_NAME_LENGTH + 1);
	}

	upcall_cache_update_entry(rsccache, rscp->sc_uc_entry, gssiam_expire,
				  0);

	/* Attach to request context */
	upcall_cache_get_entry_raw(rscp->sc_uc_entry);
	grctx->src_ctx = &rscp->sc_ctx;
	grctx->src_init = 1;

	/* Pack reply */
	rc = lustre_pack_reply_v2(req, 1, &replen, NULL, 0);
	if (rc) {
		CERROR("%s: failed to pack GSSIAM reply: rc = %d\n",
		       target->obd_name, rc);
		GOTO(out, rc);
	}

	rs = req->rq_reply_state;
	rephdr = lustre_msg_buf(rs->rs_repbuf, 0, 0);
	rephdr->gh_version = PTLRPC_GSS_VERSION;
	rephdr->gh_flags = 0;
	rephdr->gh_proc = PTLRPC_GSS_PROC_INIT;
	rephdr->gh_major = GSS_S_COMPLETE;
	rephdr->gh_minor = 0;
	rephdr->gh_seqwin = GSS_SEQ_WIN;
	rephdr->gh_handle.len = sizeof(key_hash);
	memcpy(rephdr->gh_handle.data, &key_hash, sizeof(key_hash));

	rs->rs_repdata_len = lustre_packed_msg_size(rs->rs_repbuf);

	req->rq_reqmsg = lustre_msg_buf(req->rq_reqbuf, 1, 0);
	req->rq_reqlen = lustre_msg_buflen(req->rq_reqbuf, 1);

out:
	if (!IS_ERR_OR_NULL(rscp)) {
		if (rc)
			UC_CACHE_SET_INVALID(rscp->sc_uc_entry);
		rsc_entry_put(rsccache, rscp);
	}
	if (rc) {
		rc = gss_pack_err_notify(req, major, 0);
		if (!rc)
			rc = SECSVC_COMPLETE;
		else
			rc = SECSVC_DROP;
	} else {
		rc = SECSVC_OK;
	}

	if (lid) {
		OBD_FREE(lid->lid_token, lid->lid_token_len);
		OBD_FREE(lid->lid_subdir, subdir_obj.len + 1);
		OBD_FREE_PTR(lid);
	}
	if (gssiam_info) {
		OBD_FREE(gssiam_info->lii_desc.lid_token,
			 gssiam_info->lii_desc.lid_token_len);
		OBD_FREE_STR(gssiam_info->lii_desc.lid_subdir);
		OBD_FREE_STR(gssiam_info->lii_desc.lid_principal);
		OBD_FREE_PTR(gssiam_info);
	}
	return rc;
}