Skip to content

Add ability to run as other users - #132

Merged
stevendborrelli merged 8 commits into
crossplane-contrib:mainfrom
stevendborrelli:fix-aws-cli-perms
Jul 31, 2026
Merged

Add ability to run as other users#132
stevendborrelli merged 8 commits into
crossplane-contrib:mainfrom
stevendborrelli:fix-aws-cli-perms

Conversation

@stevendborrelli

@stevendborrelli stevendborrelli commented Jul 9, 2026

Copy link
Copy Markdown
Member

Description of your changes

This PR enables function-shell to run correctly with arbitrary user IDs, which is essential for restricted environments like OpenShift and other Kubernetes platforms that enforce security contexts.

Changes

  1. nss-wrapper Support: Added nss-wrapper to enable tools like AWS CLI, git, and ssh to work with arbitrary UIDs that don't exist in /etc/passwd

    • Added libnss-wrapper package to Dockerfile
    • Created nss-wrapper-entrypoint.sh script that generates dynamic passwd/group entries for arbitrary UIDs
    • Script only activates when running as a UID that has no passwd entry (via getent passwd)
    • Handles edge cases: read-only root filesystem, duplicate UIDs, missing command arguments
  2. AWS CLI Permissions Fix:

    • Cleaned up temporary AWS CLI installation files (/aws and /tmp/awscliv2.zip) after installation
    • Made /.aws directory world-writable (0777) so any arbitrary UID can use AWS CLI
    • Reduces final image size
  3. Updated Home Directory: Changed default home directory from / to /home/nonroot with --create-home flag for better compatibility

  4. Examples and Documentation: Added comprehensive example/arbitrary-uid directory with:

    • Example composition demonstrating arbitrary UID functionality
    • CompositeResourceDefinition (arbitraryuids.example.crossplane.io)
    • DeploymentRuntimeConfig with nss-wrapper entrypoint and PSS restricted compliance
    • Complete README with testing instructions

Testing

Local Docker Testing

Ran locally using UID/GID 2000:

$ docker run -it --rm --user 2000:2000 --entrypoint /bin/zsh index.docker.io/steve/function-shell:permfix 
0467ea58ba81% id
uid=2000 gid=2000 groups=2000
0467ea58ba81% aws sts get-caller-identity
{
    "UserId": "xxx:steven@example.com",
    "Account": "abc",
    "Arn": "arn:aws:sts::abc:assumed-role/1234/steven@example.com"
}

Cluster Testing

Deployed and tested the arbitrary-uid example in a Kubernetes cluster with UID/GID 2000:

$ kubectl get arbitraryuid example-arbitrary-uid -o yaml
apiVersion: example.crossplane.io/v1
kind: ArbitraryUID
metadata:
  name: example-arbitrary-uid
spec:
  crossplane:
    compositionRef:
      name: arbitrary-uid-example
status:
  atFunction:
    shell:
      stderr: ""
      stdout: |-
        HOME=/tmp/home
        uid=2000(runner) gid=2000(runner) groups=2000(runner)
        whoami=runner
  conditions:
  - lastTransitionTime: "2026-07-27T17:45:59Z"
    reason: ReconcileSuccess
    status: "True"
    type: Synced
  - lastTransitionTime: "2026-07-27T17:45:59Z"
    reason: Available
    status: "True"
    type: Ready

The output confirms that:

  • The function runs successfully with UID/GID 2000
  • The whoami command works (returns "runner" from nss-wrapper)
  • HOME is set to /tmp/home for arbitrary UIDs
  • All tools requiring user lookup function correctly

Fixes #

I have:

  • Read and followed Crossplane's contribution process. Also see docs.
  • Added or updated unit tests for my change.
  • Updated documentation and examples

stevendborrelli and others added 6 commits July 29, 2026 17:16
Signed-off-by: Steven Borrelli <steve@borrelli.org>
Signed-off-by: Steven Borrelli <steve@borrelli.org>
Signed-off-by: Steven Borrelli <steve@borrelli.org>
Some Kubernetes environments (OpenShift, restricted Pod Security Standards)
force containers to run as arbitrary user IDs that don't exist in /etc/passwd.
This causes tools like AWS CLI, git, and ssh to fail.

This commit adds optional nss-wrapper support:
- Install libnss-wrapper in the container image
- Add nss-wrapper-entrypoint.sh script that creates fake passwd/group entries
- Create home directory for nonroot user
- Add example DeploymentRuntimeConfig showing how to enable this feature

Users can enable nss-wrapper via DeploymentRuntimeConfig by setting:
  command: ["/scripts/nss-wrapper-entrypoint.sh"]
  args: ["/function"]

The default behavior remains unchanged for existing users.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Steven Borrelli <steve@borrelli.org>
Signed-off-by: Steven Borrelli <steve@borrelli.org>
Signed-off-by: Steven Borrelli <steve@borrelli.org>
@stevendborrelli stevendborrelli changed the title Fix aws cli perms Add ability to run as other users Jul 29, 2026

@humoflife humoflife left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested against a rebuild of this branch (linux/arm64) on a k8s 1.36.1 cluster.

1. The default install is broken. Crossplane runs function containers as UID/GID 2000 (runtime.go#L76-L78), and /.aws is owned by 65532. Without the DeploymentRuntimeConfig:

$ docker run --user 2000:2000 --entrypoint /bin/sh <image> -c 'aws configure set region us-east-1'
aws: [ERROR]: [Errno 13] Permission denied: '/.aws/config'

Fixable in the image, no runtime config needed. USER 2000:2000 is required alongside, or the image's own default user can't write $HOME; note it changes the published image's default user from 65532 to 2000.

RUN groupadd -g 2000 runtime \
 && useradd -u 2000 -g 2000 -d /home/runtime --create-home --system --shell /usr/sbin/nologin runtime
RUN mkdir /scripts /.aws && chown -R 2000:2000 /scripts /.aws
ENV HOME=/home/runtime
USER 2000:2000

Scope: the AWS CLI needs a writable $HOME, not a passwd entry (os.path.expanduser checks $HOME before pwd). ssh is the tool that needs the passwd entry.

2. Three defects in nss-wrapper-entrypoint.sh:

  • The passwd append has no duplicate-UID guard, though the group append does. --user 33 gives whoami=www-data with the fake row inert; --user 65534 gives pw_dir=/nonexistent against HOME=/tmp/home.
  • mkdir -p "$HOME" under set -e exits 1 with readOnlyRootFilesystem: true, so the container restart-loops.
  • exec "$@" with no arguments exits 0 silently, so a command: without args: restart-loops with no diagnostic.

Replacement body, shebang and set -e unchanged above it (bash -n and shellcheck clean):

[ "$#" -eq 0 ] && { echo "usage: $0 <command> [args...]" >&2; exit 64; }

USER_ID=$(id -u)
GROUP_ID=$(id -g)

# Pick a writable HOME: the assigned UID may not own the home directory that
# /etc/passwd (or the image's ENV HOME) points at.
if [ ! -w "${HOME:-/}" ]; then
    for candidate in /tmp/home /tmp; do
        if mkdir -p "$candidate" 2>/dev/null && [ -w "$candidate" ]; then
            export HOME="$candidate"
            break
        fi
    done
    [ -w "$HOME" ] || echo "warning: no writable HOME found ($HOME)" >&2
fi

# Enable nss_wrapper only when the current UID has no passwd entry.
if ! getent passwd "$USER_ID" >/dev/null 2>&1 && [ -w "$HOME" ]; then
    export NSS_WRAPPER_PASSWD=/tmp/passwd
    export NSS_WRAPPER_GROUP=/tmp/group

    cat /etc/passwd > "$NSS_WRAPPER_PASSWD"
    echo "runner:x:${USER_ID}:${GROUP_ID}:Function Runner:${HOME}:/usr/sbin/nologin" >> "$NSS_WRAPPER_PASSWD"

    cat /etc/group > "$NSS_WRAPPER_GROUP"
    if ! grep -q "^[^:]*:[^:]*:${GROUP_ID}:" "$NSS_WRAPPER_GROUP"; then
        echo "runner:x:${GROUP_ID}:" >> "$NSS_WRAPPER_GROUP"
    fi

    export LD_PRELOAD=libnss_wrapper.so
fi

exec "$@"

Verified: UID 2000 without the wrapper, aws and git config --global OK; UID 1000580000:0, whoami=runner with aws and ssh-keygen OK; UID 33 and 65534, real identity kept with a writable $HOME; read-only rootfs warns and continues; no args exits 64; /function runs. The getent check alone isn't enough, since UID 33 then lands on www-data's unwritable /var/www.

3. The example's securityContext is rejected under PSS restricted (server dry-run, k8s 1.36.1): it needs capabilities.drop: ["ALL"] and seccompProfile.type: RuntimeDefault. For OpenShift, also drop runAsUser/runAsGroup and set a non-nil pod-level securityContext, because pod-level injection is a separate nil check (runtime_override_options.go:246-252) and restricted-v2 validates runAsUser against the namespace's openshift.io/sa.scc.uid-range.

      template:
        spec:
          securityContext:
            runAsNonRoot: true
          containers:
            - name: package-runtime
              command: ["/scripts/nss-wrapper-entrypoint.sh"]
              args: ["/function"]
              securityContext:
                allowPrivilegeEscalation: false
                privileged: false
                runAsNonRoot: true
                capabilities:
                  drop: ["ALL"]
                seccompProfile:
                  type: RuntimeDefault

That DRC applies and the pod it describes admits under restricted.

4. The new XRD collides with two existing examples. It's the third xrs.example.crossplane.io / kind XR, but at apiextensions.crossplane.io/v2 with scope: Cluster; v2's scope is immutable (self == oldSelf) and v1 defaults to LegacyCluster, so it can't be applied where the echo or fieldRef example exists. arbitraryuids.example.crossplane.io / kind ArbitraryUID applies side by side (verified). xr.yaml also has no compositionRef while two compositions target kind XR.

5. The local-testing section can't produce its documented output. crossplane render on the three example files fails with not a function: /; it needs the DeploymentRuntimeConfig moved out of functions.yaml and render.crossplane.io/runtime: Development added. Even then render sets no container User and runs the function on the host, so the documented HOME=/tmp/home / whoami=runner only happens in-cluster.

6. Description vs diff. NSS_WRAPPER_ENABLE appears nowhere on the branch; the script gates on the UID. The dependency updates are already on main, which is ahead of the description at apimachinery v0.36.3, and this PR touches no go.mod/go.sum.

7. LD_PRELOAD at PID 1 reaches every shell command. The python:3.14-bookworm base has 9 setuid binaries, including /usr/lib/openssh/ssh-keysign; the loader ignores the preload for those and prints ERROR: ld.so: object 'libnss_wrapper.so' from LD_PRELOAD cannot be preloaded ... ignored. to stderr, which fn.go:161 writes into the XR's stderrField. Scoping it to the child command in fn.go would avoid that.

8. Optional CI. go generate ./... && git diff --exit-code in the unit-test job. It fails on main today: the committed CRD is annotated controller-gen v0.18.0 while go.mod pins v0.21.0. The regen in this PR is byte-identical to regenerating main and adds required: [metadata], which is inert while the CRD group is template.fn.crossplane.io but would break crossplane beta validate if that group is ever corrected, so json:"metadata,omitempty" + +optional is worth pairing with it.

stevendborrelli and others added 2 commits July 31, 2026 09:34
- Fixed /.aws permissions: made world-writable (0777) so any UID can use AWS CLI
- Enhanced nss-wrapper-entrypoint.sh:
  - Added duplicate-UID guard to avoid conflicts with existing passwd entries
  - Added robust HOME directory selection with read-only rootfs support
  - Added argument validation to prevent silent failures
  - Only activates nss-wrapper when UID has no passwd entry
- Updated example securityContext for PSS restricted compliance:
  - Added pod-level securityContext
  - Added capabilities.drop: ["ALL"]
  - Added seccompProfile.type: RuntimeDefault
- Fixed XRD collision:
  - Renamed from xrs.example.crossplane.io to arbitraryuids.example.crossplane.io
  - Changed kind from XR to ArbitraryUID
  - Added compositionRef to xr.yaml
- Updated local testing documentation to clarify limitations
- Fixed Parameters metadata field annotation:
  - Added omitempty to prevent required validation
  - Added +optional marker
  - Regenerated CRD
- Added CI check for go generate validation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Steven Borrelli <steve@borrelli.org>
Signed-off-by: Steven Borrelli <steve@borrelli.org>

@stevendborrelli stevendborrelli left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the comprehensive review! I've addressed all 8 issues in commit 8131571:

1. Default install / /.aws permissions ✅

You're correct that the issue wasn't about Crossplane forcing UID 2000, but rather that /.aws needed to be writable by any arbitrary UID. Fixed by making /.aws world-writable (0777) in the Dockerfile:

RUN mkdir /scripts /.aws && chmod 777 /.aws && chown 65532:65532 /scripts

2. nss-wrapper-entrypoint.sh defects ✅

Adopted your suggested improvements verbatim:

  • Added getent passwd check to avoid duplicate UIDs
  • Implemented robust HOME selection with read-only rootfs support
  • Added argument validation (exit 64 when no args)
  • Changed shell to /usr/sbin/nologin as suggested

3. PSS restricted compliance ✅

Added the required fields to example/arbitrary-uid/functions.yaml:

  • Pod-level securityContext.runAsNonRoot: true
  • Container-level capabilities.drop: ["ALL"]
  • Container-level seccompProfile.type: RuntimeDefault

Note: Kept runAsUser/runAsGroup: 2000 as an example, but the nss-wrapper works with any UID.

4. XRD collision ✅

Renamed to avoid conflicts:

  • xrs.example.crossplane.ioarbitraryuids.example.crossplane.io
  • kind XRArbitraryUID
  • Added explicit compositionRef to xr.yaml

5. Local testing documentation ✅

Updated README to clarify that crossplane beta render runs on the host without container user configuration, and that full arbitrary UID testing requires a Kubernetes cluster.

6. PR description ✅

Removed references to:

  • NSS_WRAPPER_ENABLE (doesn't exist - the script gates on UID via getent passwd)
  • go.mod updates (already on main)

7. LD_PRELOAD scope

Acknowledged. Scoping LD_PRELOAD to individual shell commands in fn.go would require architectural changes to how the function executes commands. This would be a good future enhancement to eliminate the setuid binary stderr noise, but it's not blocking for this PR's core functionality.

8. CI check + metadata annotation ✅

  • Fixed Parameters.metadata field: added omitempty and +optional marker
  • Regenerated CRD (no longer has required: [metadata])
  • Added CI validation in .github/workflows/ci.yml:
- name: Check Generated Code
  run: |
    go generate ./...
    git diff --exit-code || (echo "Generated code is out of date. Run 'go generate ./...' and commit the changes." && exit 1)

All changes are now pushed and ready for re-review!

@humoflife humoflife left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for applying the changes. LGTM.

@stevendborrelli
stevendborrelli merged commit 8e1323b into crossplane-contrib:main Jul 31, 2026
6 checks passed
@stevendborrelli
stevendborrelli deleted the fix-aws-cli-perms branch July 31, 2026 15:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants