-
Notifications
You must be signed in to change notification settings - Fork 4
fix: Ensure container users always exist #427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| # Copyright 2025 Pasteur Labs. All Rights Reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| # Tesseract API module for userhandler | ||
| # Generated by tesseract 1.2.1.dev14+g667850bb1.d20251217 on 2025-12-18T14:34:34.613891 | ||
|
|
||
|
|
||
| import os | ||
| import pwd | ||
|
|
||
| from pydantic import BaseModel | ||
|
|
||
| # | ||
| # Schemas | ||
| # | ||
|
|
||
|
|
||
| class InputSchema(BaseModel): | ||
| pass | ||
|
|
||
|
|
||
| class OutputSchema(BaseModel): | ||
| uid: int | ||
| gid: int | ||
| username: str | ||
| home: str | ||
| shell: str | ||
|
|
||
|
|
||
| # | ||
| # Required endpoints | ||
| # | ||
|
|
||
|
|
||
| def apply(inputs: InputSchema) -> OutputSchema: | ||
| """A simple Tesseract that fails if the current OS user doesn't have a valid /etc/passwd entry.""" | ||
| # Get current user ID | ||
| uid = os.getuid() | ||
| gid = os.getgid() | ||
|
|
||
| # Try to get the passwd entry for the current user | ||
| try: | ||
| pw_entry = pwd.getpwuid(uid) | ||
| except KeyError as exc: | ||
| raise RuntimeError( | ||
| f"Current user (UID: {uid}) does not have an entry in /etc/passwd. " | ||
| "This container should have run addmeplease in the entrypoint." | ||
| ) from exc | ||
|
|
||
| if pw_entry.pw_gid != gid: | ||
| raise RuntimeError( | ||
| f"Current group (GID: {gid}) does not match expected group from /etc/passwd ({pw_entry.pw_gid})." | ||
| ) | ||
|
|
||
| # Return the user information | ||
| return OutputSchema( | ||
| uid=pw_entry.pw_uid, | ||
| gid=pw_entry.pw_gid, | ||
| username=pw_entry.pw_name, | ||
| home=pw_entry.pw_dir, | ||
| shell=pw_entry.pw_shell, | ||
| ) | ||
|
|
||
|
|
||
| # | ||
| # Optional endpoints | ||
| # | ||
|
|
||
| # import numpy as np | ||
|
|
||
| # def jacobian(inputs: InputSchema, jac_inputs: set[str], jac_outputs: set[str]): | ||
| # return {} | ||
|
|
||
| # def jacobian_vector_product( | ||
| # inputs: InputSchema, | ||
| # jvp_inputs: set[str], | ||
| # jvp_outputs: set[str], | ||
| # tangent_vector: dict[str, np.typing.ArrayLike] | ||
| # ) -> dict[str, np.typing.ArrayLike]: | ||
| # return {} | ||
|
|
||
| # def vector_jacobian_product( | ||
| # inputs: InputSchema, | ||
| # vjp_inputs: set[str], | ||
| # vjp_outputs: set[str], | ||
| # cotangent_vector: dict[str, np.typing.ArrayLike] | ||
| # ) -> dict[str, np.typing.ArrayLike]: | ||
| # return {} | ||
|
|
||
| # def abstract_eval(abstract_inputs): | ||
| # return {} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Somewhere, maybe here in the description, it would be nice to have an explanation of what this example demonstrates |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| # Tesseract configuration file | ||
| # Generated by tesseract 1.2.1.dev14+g667850bb1.d20251217 on 2025-12-18T14:34:34.613891 | ||
|
|
||
| name: "userhandler" | ||
| version: "unknown" | ||
| description: "" | ||
|
|
||
| build_config: | ||
| # Base image to use for the container, must be Ubuntu or Debian-based | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and here? |
||
| # base_image: "debian:bookworm-slim" | ||
|
|
||
| # Platform to build the container for. In general, images can only be executed | ||
| # on the platform they were built for. | ||
| target_platform: "native" | ||
|
|
||
| # Additional packages to install in the container (via apt-get) | ||
| # extra_packages: | ||
| # - package_name | ||
|
|
||
| # Data to copy into the container, relative to the project root | ||
| # package_data: | ||
| # - [path/to/source, path/to/destination] | ||
|
|
||
| # Additional Dockerfile commands to run during the build process | ||
| # custom_build_steps: | ||
| # - | | ||
| # RUN echo "Hello, World!" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and this file? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| # Tesseract requirements file | ||
| # Generated by tesseract 1.2.1.dev14+g667850bb1.d20251217 on 2025-12-18T14:34:34.613891 | ||
|
|
||
| # Add Python requirements like this: | ||
| # numpy==1.18.1 | ||
|
|
||
| # This may contain private dependencies via SSH URLs: | ||
| # git+ssh://git@github.com/username/repo.git@branch | ||
| # (use `tesseract build --forward-ssh-agent` to grant the builder access to your SSH keys) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| // Copyright 2025 Pasteur Labs. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| /* | ||
| * addmeplease - Create user/group entries for non-privileged container users | ||
| * | ||
| * This program allows a non-privileged user to create their own entry in | ||
| * /etc/passwd and /etc/group. This is necessary because: | ||
| * | ||
| * 1. Containers run as non-root users (e.g., USER 501:501) for security | ||
| * 2. Some applications require an entry in /etc/passwd to function properly | ||
| * 3. The container entrypoint runs as the non-privileged user and cannot | ||
| * directly modify /etc/passwd without elevated privileges | ||
| * | ||
| * This binary is compiled and installed with setuid root (chmod 4755), which | ||
| * allows it to temporarily elevate privileges to modify /etc/passwd and | ||
| * /etc/group, then immediately drop privileges back to the calling user. | ||
| * | ||
| * Note: setuid only works with compiled binaries, not shell scripts. This is | ||
| * why this must be implemented in C rather than as a bash script. | ||
| */ | ||
|
|
||
| #define _GNU_SOURCE | ||
| #include <stdio.h> | ||
| #include <stdlib.h> | ||
| #include <string.h> | ||
| #include <unistd.h> | ||
| #include <sys/types.h> | ||
| #include <pwd.h> | ||
| #include <grp.h> | ||
| #include <errno.h> | ||
| #include <fcntl.h> | ||
| #include <sys/stat.h> | ||
|
|
||
| #define PASSWD_FILE "/etc/passwd" | ||
| #define GROUP_FILE "/etc/group" | ||
| #define SHADOW_FILE "/etc/shadow" | ||
| #define GSHADOW_FILE "/etc/gshadow" | ||
|
|
||
| int group_exists(gid_t gid) { | ||
| struct group *grp = getgrgid(gid); | ||
| return (grp != NULL); | ||
| } | ||
|
|
||
| int user_exists(uid_t uid) { | ||
| struct passwd *pwd = getpwuid(uid); | ||
| return (pwd != NULL); | ||
| } | ||
|
|
||
| int append_to_file(const char *filepath, const char *line) { | ||
| int fd = open(filepath, O_WRONLY | O_APPEND | O_CREAT, 0644); | ||
| if (fd < 0) { | ||
| return -1; | ||
| } | ||
|
|
||
| size_t len = strlen(line); | ||
| ssize_t written = write(fd, line, len); | ||
| close(fd); | ||
|
|
||
| return (written == (ssize_t)len) ? 0 : -1; | ||
| } | ||
|
|
||
| int create_group(gid_t gid, const char *groupname) { | ||
| if (group_exists(gid)) { | ||
| return 0; | ||
| } | ||
|
|
||
| // Format: groupname:x:gid: | ||
| char group_line[256]; | ||
| snprintf(group_line, sizeof(group_line), "%s:x:%d:\n", groupname, gid); | ||
|
|
||
| if (append_to_file(GROUP_FILE, group_line) != 0) { | ||
| fprintf(stderr, "addmeplease: Failed to add group to %s: %s\n", | ||
| GROUP_FILE, strerror(errno)); | ||
| return -1; | ||
| } | ||
|
|
||
| // Add to gshadow if it exists | ||
| char gshadow_line[256]; | ||
| snprintf(gshadow_line, sizeof(gshadow_line), "%s:!::\n", groupname); | ||
|
|
||
| struct stat st; | ||
| if (stat(GSHADOW_FILE, &st) == 0) { | ||
| append_to_file(GSHADOW_FILE, gshadow_line); | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| int create_user(uid_t uid, gid_t gid, const char *username) { | ||
| if (user_exists(uid)) { | ||
| return 0; | ||
| } | ||
|
|
||
| // Format: username:x:uid:gid:comment:home:shell | ||
| char passwd_line[512]; | ||
| snprintf(passwd_line, sizeof(passwd_line), | ||
| "%s:x:%d:%d:Tesseract User:/tesseract:/bin/bash\n", | ||
| username, uid, gid); | ||
|
|
||
| if (append_to_file(PASSWD_FILE, passwd_line) != 0) { | ||
| fprintf(stderr, "addmeplease: Failed to add user to %s: %s\n", | ||
| PASSWD_FILE, strerror(errno)); | ||
| return -1; | ||
| } | ||
|
|
||
| // Add to shadow file with locked password | ||
| // Format: username:!:lastchanged:min:max:warn:inactive:expire: | ||
| // We use ! for locked password and 0 for lastchanged (epoch) | ||
| char shadow_line[256]; | ||
| snprintf(shadow_line, sizeof(shadow_line), "%s:!:0:0:99999:7:::\n", username); | ||
|
|
||
| struct stat st; | ||
| if (stat(SHADOW_FILE, &st) == 0) { | ||
| // Shadow file typically has restricted permissions | ||
| int fd = open(SHADOW_FILE, O_WRONLY | O_APPEND); | ||
| if (fd >= 0) { | ||
| size_t len = strlen(shadow_line); | ||
| write(fd, shadow_line, len); | ||
| close(fd); | ||
| } | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| int main(void) { | ||
| uid_t real_uid = getuid(); | ||
| gid_t real_gid = getgid(); | ||
|
|
||
| // If running as root, nothing to do | ||
| if (real_uid == 0) { | ||
| return 0; | ||
| } | ||
|
|
||
| // Check if user already exists | ||
| if (user_exists(real_uid)) { | ||
| return 0; | ||
| } | ||
|
|
||
| // We need root privileges to modify /etc/passwd and /etc/group | ||
| // This is why the binary needs setuid root | ||
| if (seteuid(0) != 0) { | ||
| fprintf(stderr, "addmeplease: Failed to elevate privileges: %s\n", strerror(errno)); | ||
| fprintf(stderr, "addmeplease: This binary must be setuid root (chmod 4755)\n"); | ||
| return 1; | ||
| } | ||
|
|
||
| // Create group if it doesn't exist | ||
| if (create_group(real_gid, "tesseract-group") != 0) { | ||
| fprintf(stderr, "addmeplease: Warning: Failed to create group\n"); | ||
| // Continue anyway, the GID might exist with a different name | ||
| } | ||
|
|
||
| // Create user with current UID/GID | ||
| if (create_user(real_uid, real_gid, "tesseract-user") != 0) { | ||
| fprintf(stderr, "addmeplease: Warning: Failed to create user\n"); | ||
| // Continue anyway | ||
| } | ||
|
|
||
| // Drop privileges back to the original user | ||
| if (seteuid(real_uid) != 0) { | ||
| fprintf(stderr, "addmeplease: Failed to drop privileges: %s\n", strerror(errno)); | ||
| return 1; | ||
| } | ||
|
|
||
| return 0; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
delete all commented stuff?