From 728f9be306e5e207abcef133b2231e366e19044d Mon Sep 17 00:00:00 2001 From: "Mark A. Grondona" Date: Sat, 7 Dec 2024 01:02:08 +0000 Subject: [PATCH 1/9] configure: require flux-core >= 0.64.0 Problem: The flux-pam module could be simplified using the 'rank' constraint feature provided by the job-list service, but this is only supported in flux-core v0.64.0 or later. Update configure check to require flux-core v0.64.0 at a minimum. --- config/ax_flux_core.m4 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/ax_flux_core.m4 b/config/ax_flux_core.m4 index 1a343f7..637abbe 100644 --- a/config/ax_flux_core.m4 +++ b/config/ax_flux_core.m4 @@ -49,7 +49,7 @@ AC_DEFUN([AX_FLUX_CORE], [ PKG_CONFIG_PATH=${prefix}/lib/pkgconfig:${PKG_CONFIG_PATH} export PKG_CONFIG_PATH - PKG_CHECK_MODULES([FLUX_CORE], [flux-core > 0.29.0], + PKG_CHECK_MODULES([FLUX_CORE], [flux-core > 0.64.0], [ FLUX_PREFIX=`pkg-config --variable=prefix flux-core` LIBFLUX_VERSION=`pkg-config --modversion flux-core` From 1f961b49ba3094576e7aea53b19bf46721b10009 Mon Sep 17 00:00:00 2001 From: "Mark A. Grondona" Date: Sat, 7 Dec 2024 01:05:10 +0000 Subject: [PATCH 2/9] pam_flux: simplify check for local jobs for a given userid Problem: The pam_flux module has to iterate all jobs for a user to determine if any active jobs are on the current rank, but the job-list service supports a rank constraint, so this could be simplified. Add a rank constraint to the job-list RPC. The user can now be permitted if any jobs are returned in the result. --- src/pam/pam_flux.c | 45 ++++++++++++++++++--------------------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/src/pam/pam_flux.c b/src/pam/pam_flux.c index ac0f810..44a4a42 100644 --- a/src/pam/pam_flux.c +++ b/src/pam/pam_flux.c @@ -32,7 +32,6 @@ #include #include -#include #define PAM_SM_ACCOUNT #include @@ -59,12 +58,10 @@ static void log_msg (int level, const char *format, ...) static int flux_check_user (uid_t uid) { int authorized = 0; - size_t index; - json_t *value; json_t *jobs = NULL; flux_t *h = NULL; unsigned int rank = -1; - flux_job_state_t state = FLUX_JOB_STATE_NEW; + char rankstr[16]; flux_future_t *f = NULL; if (!(h = flux_open (NULL, 0))) { @@ -75,45 +72,39 @@ static int flux_check_user (uid_t uid) log_msg (LOG_ERR, "Failed to get current broker rank: %m"); goto out; } + if (snprintf (rankstr, + sizeof (rankstr), + "%u", + rank) >= sizeof (rankstr)) { + log_msg (LOG_ERR, "Failed to encode broker rank as string: %m"); + goto out; + } + /* Query jobs in RUN state on current rank using RFC 43 constraint object + */ f = flux_rpc_pack (h, "job-list.list", 0, 0, - "{s:i s:[ss] s:{s:[{s:[i]} {s:[i]}]}}", + "{s:i s:[ss] s:{s:[{s:[i]} {s:[s]} {s:[i]}]}}", "max_entries", 0, "attrs", "ranks", "state", "constraint", "and", "userid", uid, - "states", FLUX_JOB_STATE_RUNNING); + "ranks", rankstr, + "states", FLUX_JOB_STATE_RUN); if (!f || flux_rpc_get_unpack (f, "{s:o}", "jobs", &jobs) < 0) { flux_future_destroy (f); log_msg (LOG_ERR, "flux_job_list: %m"); goto out; } - json_array_foreach (jobs, index, value) { - const char *ranks; - struct idset *ids; - - if (json_unpack (value, - "{s:s s:i}", - "ranks", &ranks, - "state", &state) < 0 - || !(ids = idset_decode (ranks))) { - log_msg (LOG_ERR, "Failed to unpack job response"); - goto out; - } - /* Job must have an R which includes this rank _and_ the job - * must be in FLUX_JOB_STATE_RUN (not CLEANUP) - */ - if (idset_test (ids, rank) && state == FLUX_JOB_STATE_RUN) - authorized = 1; - idset_destroy (ids); - if (authorized) - goto out; - } + /* If there was at least one job returned, then this user is allowed + */ + if (json_array_size (jobs) > 0) + authorized = 1; + out: flux_future_destroy (f); flux_close (h); From 26210d24b1fa37b1fec5a70ad9ece12401cb3d92 Mon Sep 17 00:00:00 2001 From: "Mark A. Grondona" Date: Sat, 7 Dec 2024 16:14:50 +0000 Subject: [PATCH 3/9] pam_flux: fix double free in error path Problem: If flux_rpc_get_unpack() fails, then the future is destroyed twice, once in the conditional and once at exit after the `out:` label. Remove the flux_future_destroy() in the conditional to avoid double-free. --- src/pam/pam_flux.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pam/pam_flux.c b/src/pam/pam_flux.c index 44a4a42..d100fc0 100644 --- a/src/pam/pam_flux.c +++ b/src/pam/pam_flux.c @@ -95,7 +95,6 @@ static int flux_check_user (uid_t uid) "ranks", rankstr, "states", FLUX_JOB_STATE_RUN); if (!f || flux_rpc_get_unpack (f, "{s:o}", "jobs", &jobs) < 0) { - flux_future_destroy (f); log_msg (LOG_ERR, "flux_job_list: %m"); goto out; } From 5baf8c60ad385d5ddce2587fc2af18989dc849dc Mon Sep 17 00:00:00 2001 From: "Mark A. Grondona" Date: Sun, 8 Dec 2024 19:22:56 +0000 Subject: [PATCH 4/9] pam_flux: remove use of deprecated _pam_drop_reply() Problem: The _pam_drop_reply() macro is deprecated in recent versions of Linux-PAM with the note: . Deprecated _pam_overwrite(), _pam_overwrite_n(), and _pam_drop_reply() macros provided by _pam_macros.h; the memory override performed by these macros can be optimized out by the compiler and therefore can no longer be relied upon. Replace use of _pam_drop_reply() with equivalent free(). --- src/pam/pam_flux.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pam/pam_flux.c b/src/pam/pam_flux.c index d100fc0..7732fdd 100644 --- a/src/pam/pam_flux.c +++ b/src/pam/pam_flux.c @@ -35,7 +35,6 @@ #define PAM_SM_ACCOUNT #include -#include /* * Write message described by the 'format' string to syslog. @@ -157,8 +156,13 @@ static void send_denial_msg (pam_handle_t *pamh, log_msg (LOG_ERR, "unable to converse with app: %s", pam_strerror (pamh, retval)); - if (prsp != NULL) - _pam_drop_reply (prsp, 1); + if (prsp != NULL) { + /* N.B. _pam_drop_reply() deprecated in recent versions + * of Linux-PAM. Free reply without use of macros: + */ + free (prsp[0].resp); + free (prsp); + } return; } From f85b6b6d30116f2346814ca6c41609c0d8c10608 Mon Sep 17 00:00:00 2001 From: "Mark A. Grondona" Date: Sun, 8 Dec 2024 00:26:31 +0000 Subject: [PATCH 5/9] pam_flux: support allow-guest-user option Problem: The pam_flux module only allows users onto a node if they currently have an active job assigned to that node. This simplistic access control is not sufficient if guest users need to access a multi-user instance of Flux running as a job in the system instance. Any attempt to use flux-proxy to interact with the subinstance will fail, because the ssh connector will be denied access by pam_flux.so. Add a new config parameter `allow-guest-user` to the flux_pam module. If this option is set, the module will add the current instance owner to the job-list query it uses to determine access. Access will additionally be granted if there is job for the instance owner and all of the following are true: - the current node is rank 0 of the job - the job is an instance of Flux - the instance is configured with access.allow-guest-user Of course, access is also granted if the user has an active job on the current node. Fixes #7 --- src/pam/pam_flux.c | 195 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 184 insertions(+), 11 deletions(-) diff --git a/src/pam/pam_flux.c b/src/pam/pam_flux.c index 7732fdd..093f59a 100644 --- a/src/pam/pam_flux.c +++ b/src/pam/pam_flux.c @@ -32,10 +32,21 @@ #include #include +#include #define PAM_SM_ACCOUNT #include +struct options { + /* If set, permit access to all users if the specified user has + * a job in RUN state on this host, this is rank 0 of that job, + * the job is an instance of Flux, and has access.allow-guest-user + * configured. + * (Allows guests to access multi-user instance jobs via ssh connector) + */ + bool allow_guest_user; +}; + /* * Write message described by the 'format' string to syslog. */ @@ -51,10 +62,139 @@ static void log_msg (int level, const char *format, ...) return; } -/* Return 1 if uid has the local rank currently allocated to an active - * Flux job. +static char *uri_to_local (const char *uri) +{ + char *local_uri = NULL; + char *p; + + /* Ensure this uri starts with `ssh://` + */ + if (!uri || strncmp (uri, "ssh://", 6) != 0) + return NULL; + + /* Skip to next '/' after ssh:// part + */ + if (!(p = strchr (uri+6, '/'))) + return NULL; + + /* Construct local uri from remainder (path) + */ + if (asprintf (&local_uri, "local:///%s", p) < 0) + return NULL; + return local_uri; +} + +/* Return 1 if local instance at uri has access.allow-guest-user=true. + * Return 0 otherwise. + */ +static int check_guest_allowed (const char *uri) +{ + int allowed = 0; + flux_t *h = NULL; + flux_future_t *f = NULL; + char *local_uri = NULL; + + if (!uri) + goto out; + + if (!(local_uri = uri_to_local (uri))) { + log_msg (LOG_ERR, "failed to transform %s into local uri", uri); + goto out; + } + if (!(h = flux_open (local_uri, 0))) { + log_msg (LOG_ERR, "flux_open (%s): %m", local_uri); + goto out; + } + if (!(f = flux_rpc (h, "config.get", NULL, FLUX_NODEID_ANY, 0)) + || flux_rpc_get_unpack (f, + "{s?{s?b}}", + "access", + "allow-guest-user", &allowed) < 0) { + log_msg (LOG_ERR, "failed to get config: %m"); + goto out; + } + if (!allowed) + log_msg (LOG_INFO, "access.allow-guest-user not enabled in child"); +out: + flux_close (h); + free (local_uri); + return allowed; +} + +/* Loop over jobs in json array 'jobs'. + * - If any job owner is uid, permit. + * - If any job owner is allow_if_user and rank == rank 0 of the job, + * permit if the job is an instance (has a uri) and acesss.allow-guest-user + * is true. + */ +static int check_jobs_array (json_t *jobs, + unsigned int rank, + uid_t uid, + uid_t allow_if_user) +{ + size_t index; + json_t *entry; + + json_array_foreach (jobs, index, entry) { + const char *job_ranks; + const char *uri = NULL; + int job_uid; + + if (json_unpack (entry, + "{s:i s:s s?{s?{s?s}}}", + "userid", &job_uid, + "ranks", &job_ranks, + "annotations", + "user", + "uri", &uri) < 0) { + log_msg (LOG_ERR, "failed to unpack userid, ranks for job"); + return 0; + } + if (job_uid == uid) + return 1; + else if (job_uid == allow_if_user) { + struct idset *ranks; + if ((ranks = idset_decode (job_ranks))) { + int allowed = 0; + /* Only if this rank is rank 0 of the job, check that + * access.allow-guest-user is enabled in the job instance: + */ + if (rank == idset_first (ranks)) + allowed = check_guest_allowed (uri); + idset_destroy (ranks); + if (allowed) + return 1; + } + } + } + return 0; +} + +/* Fetch an attribute and return its value as uid_t. */ -static int flux_check_user (uid_t uid) +static uid_t attr_get_uid (flux_t *h, const char *name) +{ + const char *s; + char *endptr; + long i; + + if (!(s = flux_attr_get (h, name))) { + log_msg (LOG_ERR, "flux_attr_get (%s): %m", name); + return (uid_t) -1; + } + errno = 0; + i = strtol (s, &endptr, 10); + if (errno != 0 || *endptr != '\0') { + log_msg (LOG_ERR, "error converting %s to uid: %m", name); + return (uid_t) -1; + } + return (uid_t) i; +} + + +/* get jobs in RUN state on this node for user(s) of interest: + */ +static int flux_check_user (struct options *opts, uid_t uid) { int authorized = 0; json_t *jobs = NULL; @@ -63,6 +203,13 @@ static int flux_check_user (uid_t uid) char rankstr[16]; flux_future_t *f = NULL; + /* allow_if_user MAY be set to the instance owner to allow guest + * access for uid to this node in the case of a multi-user subinstance. + * However, initialize it to uid so it can unconditionally be used below + * in the RPC to job-list, which greatly simplifies code. + */ + uid_t allow_if_user = uid; + if (!(h = flux_open (NULL, 0))) { log_msg (LOG_ERR, "Unable to connect to Flux: %m"); return 0; @@ -71,6 +218,14 @@ static int flux_check_user (uid_t uid) log_msg (LOG_ERR, "Failed to get current broker rank: %m"); goto out; } + if (opts->allow_guest_user) { + uid_t owner = attr_get_uid (h, "security.owner"); + if (owner != (uid_t) -1) + allow_if_user = owner; + else + log_msg (LOG_ERR, + "Failed to get security.owner, can't allow guest access"); + } if (snprintf (rankstr, sizeof (rankstr), "%u", @@ -85,12 +240,12 @@ static int flux_check_user (uid_t uid) "job-list.list", 0, 0, - "{s:i s:[ss] s:{s:[{s:[i]} {s:[s]} {s:[i]}]}}", + "{s:i s:[sss] s:{s:[{s:[ii]} {s:[s]} {s:[i]}]}}", "max_entries", 0, - "attrs", "ranks", "state", + "attrs", "userid", "ranks", "annotations", "constraint", "and", - "userid", uid, + "userid", uid, allow_if_user, "ranks", rankstr, "states", FLUX_JOB_STATE_RUN); if (!f || flux_rpc_get_unpack (f, "{s:o}", "jobs", &jobs) < 0) { @@ -98,10 +253,7 @@ static int flux_check_user (uid_t uid) goto out; } - /* If there was at least one job returned, then this user is allowed - */ - if (json_array_size (jobs) > 0) - authorized = 1; + authorized = check_jobs_array (jobs, rank, uid, allow_if_user); out: flux_future_destroy (f); @@ -167,6 +319,23 @@ static void send_denial_msg (pam_handle_t *pamh, return; } +static int parse_options (struct options *opts, int argc, const char **argv) +{ + for (int i = 0; i < argc; i++) { + if (strcmp ("allow-guest-user", argv[i]) == 0) { + opts->allow_guest_user = true; + } + else { + log_msg (LOG_ERR, + "unrecognized option: %s", + argv[i]); + return -1; + } + } + return 0; +} + + PAM_EXTERN int pam_sm_acct_mgmt (pam_handle_t *pamh, int flags, int argc, const char **argv) { @@ -175,6 +344,7 @@ pam_sm_acct_mgmt (pam_handle_t *pamh, int flags, int argc, const char **argv) struct passwd *pw; uid_t uid; int auth = PAM_PERM_DENIED; + struct options opts = { .allow_guest_user = false }; retval = pam_get_item (pamh, PAM_USER, (const void **) &user); if ((retval != PAM_SUCCESS) || (user == NULL) || (*user == '\0')) { @@ -189,7 +359,10 @@ pam_sm_acct_mgmt (pam_handle_t *pamh, int flags, int argc, const char **argv) } uid = pw->pw_uid; - if (flux_check_user (uid)) + if (parse_options (&opts, argc, argv) < 0) + return PAM_SYSTEM_ERR; + + if (flux_check_user (&opts, uid)) auth = PAM_SUCCESS; if (auth != PAM_SUCCESS) From 54368ee445e46f34e4ba91de465332d8ecee70ab Mon Sep 17 00:00:00 2001 From: "Mark A. Grondona" Date: Sun, 8 Dec 2024 04:16:06 +0000 Subject: [PATCH 6/9] testsuite: remove unnecessary config in pam_flux test Problem: A bug in flux-core <= v0.40.0 required useless use of a configured prolog in one of the test in t0001-pam_flux.t. Now that flux-pam requires v0.64.0 of flux-core, this is no longer necessary. Remove it. --- t/t0001-pam_flux.t | 6 ------ 1 file changed, 6 deletions(-) diff --git a/t/t0001-pam_flux.t b/t/t0001-pam_flux.t index 9259844..fd27f75 100755 --- a/t/t0001-pam_flux.t +++ b/t/t0001-pam_flux.t @@ -58,14 +58,8 @@ test_expect_success 'pam_flux: module denies access on flux_open() failure' ' ' test_expect_success 'pam_flux: module denies access during CLEANUP' ' cat <<-EOF >config/epilog.toml && - # Note: prolog only needs to be configured due to bug in - # flux-core <= v0.40.0 - [job-manager.prolog] - command = [ "/bin/true" ] - [job-manager.epilog] command = [ "flux", "event", "sub", "-c1", "pam-test-done" ] - EOF flux config reload && flux jobtap load perilog.so && From 673d8ad3e2796cd9ffc8987cbe6837b334430523 Mon Sep 17 00:00:00 2001 From: "Mark A. Grondona" Date: Sun, 8 Dec 2024 04:17:40 +0000 Subject: [PATCH 7/9] testsuite: cleanup instance config after test Problem: A test in t0001-pam_flux.t leaves the instance configured with an epilog. This could cause confusing failures in subsequent tests. Add test_when_finished to remove the added config and reconfigure the instance. --- t/t0001-pam_flux.t | 1 + 1 file changed, 1 insertion(+) diff --git a/t/t0001-pam_flux.t b/t/t0001-pam_flux.t index fd27f75..8a47b1d 100755 --- a/t/t0001-pam_flux.t +++ b/t/t0001-pam_flux.t @@ -62,6 +62,7 @@ test_expect_success 'pam_flux: module denies access during CLEANUP' ' command = [ "flux", "event", "sub", "-c1", "pam-test-done" ] EOF flux config reload && + test_when_finished "rm config/epilog.toml && flux config reload" && flux jobtap load perilog.so && jobid=$(flux submit --wait-event=epilog-start hostname) && test_must_fail pamtest -u ${USER} && From e9ac8d7e21a99e9f290571902517f6cf623e8093 Mon Sep 17 00:00:00 2001 From: "Mark A. Grondona" Date: Sun, 8 Dec 2024 15:48:49 +0000 Subject: [PATCH 8/9] testsuite: test allow-guest-user option of pam_flux.so Problem: No tests in the flux-pam testsuite ensure that the allow-guest-user option works. Expand the t0001-pam_flux.t tests to include some tests for allow-guest-user. --- t/t0001-pam_flux.t | 63 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/t/t0001-pam_flux.t b/t/t0001-pam_flux.t index 8a47b1d..5b5ab13 100755 --- a/t/t0001-pam_flux.t +++ b/t/t0001-pam_flux.t @@ -4,6 +4,7 @@ test_description='Basic plugin tests' . `dirname $0`/sharness.sh +export FLUX_URI_RESOLVE_LOCAL=t PAM_FLUX_PATH=${FLUX_BUILD_DIR}/src/pam/.libs/pam_flux.so PAMTEST=${FLUX_BUILD_DIR}/t/pamtest @@ -14,7 +15,7 @@ if ! test -x ${PAMTEST}; then fi mkdir config -test_under_flux 1 full -o,--config-path=$(pwd)/config +test_under_flux 2 full -o,--config-path=$(pwd)/config # Check for libpam_wrapper.so LD_PRELOAD=libpam_wrapper.so ${PAMTEST} -h >ld_preload.out 2>&1 @@ -30,6 +31,15 @@ pamtest() { ${PAMTEST} -v -s pam-test "$@" } +pamtest_on_rank() { + RANK=$1 + shift + LD_PRELOAD=libpam_wrapper.so \ + PAM_WRAPPER=1 \ + PAM_WRAPPER_SERVICE_DIR=$(pwd) \ + flux exec -r ${RANK} ${PAMTEST} -v -s pam-test "$@" +} + test_expect_success 'pam_flux: create pam-test PAM stack' ' cat <<-EOF >pam-test auth required pam_localuser.so @@ -70,4 +80,55 @@ test_expect_success 'pam_flux: module denies access during CLEANUP' ' flux event pub pam-test-done && flux job wait-event -vt 15 ${jobid} clean ' +test_expect_success 'add allow-guest-user to pam_flux module options' ' + cat <<-EOF >pam-test + auth required pam_localuser.so + account required ${PAM_FLUX_PATH} allow-guest-user + EOF +' +test_expect_success 'pam_flux: module denies access with allow-guest-access' ' + test_must_fail pamtest -u ${USER} 2>allow-guest.err && + test_debug "cat allow-guest.err" && + grep "Access denied: user ${USER}" allow-guest.err && + test_must_fail pamtest -u nobody 2>allow-guest1.err && + test_debug "cat allow-guest1.err" && + grep "Access denied: user nobody" allow-guest1.err +' +test_expect_success 'pam_flux: access denied if job not an instance' ' + id=$(flux submit -N1 --requires=rank:0 sleep inf) && + test_must_fail pamtest -u nobody 2>no-uri.err && + test_debug "cat no-uri.err" && + grep "Access denied: user nobody" no-uri.err && + flux cancel $id && + flux job wait-event -vt 15 $id clean +' +test_expect_success 'pam_flux: but job user still given access' ' + id=$(flux submit -N1 --requires=rank:0 sleep inf) && + pamtest -u ${USER} && # but job user is still allowed + flux cancel $id && + flux job wait-event -vt 15 $id clean +' +test_expect_success 'pam_flux: access denied if access.allow-guest-user=0' ' + id=$(flux alloc --bg -N1 --requires=rank:0) && + test_must_fail pamtest -u nobody 2>allow-guest2.err && + test_debug "cat allow-guest2.err" && + grep "Access denied: user nobody" allow-guest2.err && + flux cancel $id && + flux job wait-event -vt 15 $id clean +' +test_expect_success 'pam_flux: access allowed if access.allow-guest-user=1' ' + id=$(flux alloc --requires=rank:0 \ + --conf=access.allow-guest-user=true --bg -N1) && + pamtest -u nobody && + flux cancel $id && + flux job wait-event -vt 15 $id clean +' +test_expect_success 'pam_flux: access denied if not rank 0 of job' ' + id=$(flux alloc --conf=access.allow-guest-user=true --bg -N2) && + test_must_fail pamtest_on_rank 1 -u nobody 2>allow-guest3.err && + test_debug "cat allow-guest3.err" && + grep "Access denied: user nobody" allow-guest3.err && + flux cancel $id && + flux job wait-event -vt 15 $id clean +' test_done From 760cd61cbba6e394f334c8912b9f5914b7521d36 Mon Sep 17 00:00:00 2001 From: "Mark A. Grondona" Date: Sun, 8 Dec 2024 18:43:45 +0000 Subject: [PATCH 9/9] ci: replace fedora38 with fedora40 in github actions Problem: flux-core no longer builds images for fedora38. Update ci to use fedora40. --- .github/workflows/main.yml | 4 ++-- src/test/docker/{fedora38 => fedora40}/Dockerfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/test/docker/{fedora38 => fedora40}/Dockerfile (95%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6e369e9..42af966 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,8 +31,8 @@ jobs: image: "el8" env: chain_lint: t - - name: "fedora38" - image: "fedora38" + - name: "fedora40" + image: "fedora40" env: {} fail-fast: false name: ${{ matrix.name }} diff --git a/src/test/docker/fedora38/Dockerfile b/src/test/docker/fedora40/Dockerfile similarity index 95% rename from src/test/docker/fedora38/Dockerfile rename to src/test/docker/fedora40/Dockerfile index faf2f4b..39f6afd 100644 --- a/src/test/docker/fedora38/Dockerfile +++ b/src/test/docker/fedora40/Dockerfile @@ -1,4 +1,4 @@ -FROM fluxrm/flux-core:fedora38 +FROM fluxrm/flux-core:fedora40 ARG USER=fluxuser ARG UID=1000