Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions examples/userhandling/tesseract_api.py
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,
)


#
Copy link
Contributor

Choose a reason for hiding this comment

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

delete all commented stuff?

# 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 {}
27 changes: 27 additions & 0 deletions examples/userhandling/tesseract_config.yaml
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The 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!"
9 changes: 9 additions & 0 deletions examples/userhandling/tesseract_requirements.txt
Copy link
Contributor

Choose a reason for hiding this comment

The 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)
10 changes: 6 additions & 4 deletions tesseract_core/sdk/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,11 +214,13 @@ def prepare_build_context(

template_dir = get_template_dir()

extra_files = [template_dir / "entrypoint.sh", template_dir / "addmeplease.c"]

requirement_config = user_config.build_config.requirements
copy(
template_dir / requirement_config._build_script,
context_dir / "__tesseract_source__" / requirement_config._build_script,
)
extra_files.append(template_dir / requirement_config._build_script)

for path in extra_files:
copy(path, context_dir / path.relative_to(template_dir))

# When building from a requirements.txt we support local dependencies.
# We separate local dep. lines from the requirements.txt and copy the
Expand Down
28 changes: 24 additions & 4 deletions tesseract_core/sdk/templates/Dockerfile.base
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Copyright 2025 Pasteur Labs. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

# Stage 1: Build Python environment and install requirements
{% if config.build_config.target_platform.strip() == "native" %}
FROM {{ config.build_config.base_image }} AS build_stage
Expand All @@ -22,8 +25,19 @@ fi
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
ssh \
# Needed to compile addmeplease, and nice to have in general
gcc \
libc6-dev \
&& rm -rf /var/lib/apt/lists/*

# Copy and compile addmeplease
COPY "addmeplease.c" /tmp/addmeplease.c
RUN gcc -o /bin/addmeplease /tmp/addmeplease.c && \
rm /tmp/addmeplease.c && \
chown root:root /bin/addmeplease && \
chmod 4755 /bin/addmeplease && \
ls -l /bin/addmeplease

{% if config.build_config.extra_packages %}
# Install extra packages
RUN apt-get update && apt-get install -y --no-install-recommends \
Expand All @@ -40,7 +54,7 @@ RUN mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
WORKDIR /tmp/build
COPY {{ tesseract_runtime_location }} ./tesseract_runtime/
COPY {{ tesseract_source_directory }}/{{ config.build_config.requirements._filename }} ./
COPY {{ tesseract_source_directory }}/{{ config.build_config.requirements._build_script }} ./
COPY {{ config.build_config.requirements._build_script }} ./
COPY local_requirements/ ./local_requirements

# Build a python venv from python provider build scripts.
Expand Down Expand Up @@ -79,6 +93,9 @@ ENV TESSERACT_NAME="{{ config.name | replace('"', '\\"') | replace('\n', '\\n')

# Copy only necessary files
COPY --from=build_stage /python-env /python-env
COPY --from=build_stage /bin/addmeplease /bin/addmeplease
COPY "entrypoint.sh" /tesseract/entrypoint.sh
RUN chmod 555 /tesseract/entrypoint.sh
COPY "{{ tesseract_source_directory }}/tesseract_api.py" ${TESSERACT_API_PATH}
RUN chmod 444 ${TESSERACT_API_PATH}

Expand All @@ -102,14 +119,17 @@ RUN mkdir -p /tesseract/input_data /tesseract/output_data && \
chmod 777 /tesseract/output_data
VOLUME ["/tesseract/input_data", "/tesseract/output_data"]

ENV HOME=/tesseract
ENV TESSERACT_INPUT_PATH="/tesseract/input_data"
ENV TESSERACT_OUTPUT_PATH="/tesseract/output_data"

# Drop to random non-root user to prevent exploits
# (this way, we never start the container as root unless specifically requested)
USER 9999:9999

# Final sanity check to ensure the runtime is installed and tesseract_api.py is valid
{% if not config.build_config.skip_checks %}
RUN _TESSERACT_IS_BUILDING=1 tesseract-runtime check
RUN _TESSERACT_IS_BUILDING=1 /tesseract/entrypoint.sh tesseract-runtime check
{% endif %}

ENTRYPOINT ["tesseract-runtime"]
ENTRYPOINT ["/tesseract/entrypoint.sh", "tesseract-runtime"]
CMD ["--help"]
168 changes: 168 additions & 0 deletions tesseract_core/sdk/templates/addmeplease.c
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;
}
3 changes: 3 additions & 0 deletions tesseract_core/sdk/templates/build_conda_venv.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#!/bin/bash

# Copyright 2025 Pasteur Labs. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

set -e # Exit immediately if a command exits with a non-zero status

conda env create --file tesseract_environment.yaml -p /python-env --quiet
Expand Down
Loading