diff --git a/.gitignore b/.gitignore index 5281f6d80..5ce5127c8 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ __pycache__/ *.key *.pem TIKTOKEN_CACHE +PRISMA_CACHE # Ignore only top-level docs directory, not lib/docs /docs/ diff --git a/lib/api-base/fastApiContainer.ts b/lib/api-base/fastApiContainer.ts index cad18ed58..cf0dfa1da 100644 --- a/lib/api-base/fastApiContainer.ts +++ b/lib/api-base/fastApiContainer.ts @@ -140,6 +140,28 @@ export class FastApiContainer extends Construct { // Continue execution even if cache generation fails } } + + // When prepareDockerOffline is enabled, pre-generate the Prisma engine binary cache. + // This downloads Prisma engine binaries from binaries.prisma.sh for the target + // container platform and places them in PRISMA_CACHE/ so the Docker build can use + // them without internet access. Same pattern as the tiktoken cache generation above. + if (config.prepareDockerOffline && process.env.NODE_ENV !== 'test') { + const prismaCacheDir = path.join(REST_API_PATH, 'PRISMA_CACHE'); + console.log('prepareDockerOffline: Generating Prisma engine binary cache for offline builds...'); + try { + const scriptPath = path.join(ROOT_PATH, 'scripts', 'generate-prisma-cache.py'); + const platform = config.restApiConfig.buildConfig?.PRISMA_PLATFORM || 'debian-openssl-3.0.x'; + child_process.execSync(`python3 ${scriptPath} ${prismaCacheDir} --platform ${platform}`, { stdio: 'inherit' }); + } catch (error) { + console.error('prepareDockerOffline: Failed to generate Prisma cache:', error); + throw new Error( + 'Failed to generate Prisma engine binary cache for offline builds. ' + + 'Ensure internet access is available and prisma-client-py is installed (pip install prisma). ' + + 'Set prepareDockerOffline: false to skip this step.', + { cause: error } + ); + } + } } const restApiImage = config.restApiConfig.imageConfig || { diff --git a/lib/docs/admin/deploy.md b/lib/docs/admin/deploy.md index 5b9c1dac6..2905dc941 100644 --- a/lib/docs/admin/deploy.md +++ b/lib/docs/admin/deploy.md @@ -430,12 +430,15 @@ baseImage: /python:3.13-slim restApiConfig: buildConfig: PRISMA_CACHE_DIR: "./PRISMA_CACHE" # Path relative to lib/serve/rest-api/ + PRISMA_PLATFORM: "debian-openssl-3.0.x" # Must match container base image OS (default) # Configure offline build dependencies for MCP Workbench (S6 Overlay and rclone) mcpWorkbenchBuildConfig: S6_OVERLAY_NOARCH_SOURCE: "./s6-overlay-noarch.tar.xz" # Path relative to lib/serve/mcp-workbench/ S6_OVERLAY_ARCH_SOURCE: "./s6-overlay-x86_64.tar.xz" # Path relative to lib/serve/mcp-workbench/ RCLONE_SOURCE: "./rclone-linux-amd64.zip" # Path relative to lib/serve/mcp-workbench/ +# During synthesis, this will predownload tiktoken and prisma caches for rest-api. The folder will be added into the container, removing the need to download during the container build process +prepareDockerOffline: true ``` You'll also want any model hosting base containers available, e.g. vllm/vllm-openai:latest and ghcr.io/huggingface/text-embeddings-inference:latest @@ -446,31 +449,37 @@ For environments without internet access during Docker builds, you can pre-cache **REST API Prisma cache** (required by prisma-client-py): -The `prisma-client-py` package requires platform-specific binaries and a Node.js environment to function. When Prisma runs for the first time, it downloads these dependencies to `~/.cache/prisma/` and `~/.cache/prisma-python/`. For offline deployments, you need to pre-populate this cache. +The `prisma-client-py` package requires platform-specific engine binaries to function. For offline deployments, you need to pre-download these binaries so the Docker build doesn't need internet access. -Below is an example workflow using an Amazon Linux 2023 instance with Python 3.12: +LISA includes a helper script that downloads the correct engine binaries for the target container platform, regardless of what OS your build machine runs: ```bash -# Ensure Pip is up-to-date +# Ensure pip is up-to-date pip3 install --upgrade pip -# Install Prisma Python package +# Install Prisma Python package (needed to determine the engine version) pip3 install prisma -# Trigger Prisma to download all required binaries and create its Node.js environment -# This populates ~/.cache/prisma/ and ~/.cache/prisma-python/ -prisma version +# Download Prisma engine binaries for the target container platform +# Defaults to debian-openssl-3.0.x (matches python:3.13-slim) +python3 scripts/generate-prisma-cache.py lib/serve/rest-api/PRISMA_CACHE/ +``` + +The script downloads `query-engine` and `schema-engine` binaries directly from `binaries.prisma.sh` for the target platform. This is preferred over running `prisma version` (which downloads binaries for your *current* host OS) because the build machine often differs from the container OS — e.g. building on Amazon Linux or macOS while the container uses Debian-based `python:3.13-slim`. + +To target a different container base image, use the `--platform` flag: + +```bash +# For Amazon Linux 2023 / RHEL 9 based containers +python3 scripts/generate-prisma-cache.py lib/serve/rest-api/PRISMA_CACHE/ --platform rhel-openssl-3.0.x -# Copy the complete Prisma cache to your build context -# The wildcard captures both 'prisma' and 'prisma-python' directories -cp -r ~/.cache/prisma* lib/serve/rest-api/PRISMA_CACHE/ +# For Alpine based containers +python3 scripts/generate-prisma-cache.py lib/serve/rest-api/PRISMA_CACHE/ --platform linux-musl-openssl-3.0.x ``` -**Important Notes:** +You can also set `PRISMA_ENGINES_MIRROR` to use an internal mirror if `binaries.prisma.sh` is not accessible from your network. -* The cache is platform-specific. Generate it on a system matching your Docker base image (e.g., for `public.ecr.aws/docker/library/python:3.13-slim` which is Debian-based, so you may want to use a Debian-based system) -* The `prisma version` command downloads binaries for your current platform -* Both `prisma/` and `prisma-python/` directories are required for offline operation +When using `prepareDockerOffline: true`, the platform can be configured in `config-custom.yaml` via `restApiConfig.buildConfig.PRISMA_PLATFORM` (defaults to `debian-openssl-3.0.x`). **MCP Workbench dependencies** (S6 Overlay and rclone): diff --git a/lib/rag/ingestion/ingestion-image/Dockerfile b/lib/rag/ingestion/ingestion-image/Dockerfile index 9fad16343..6c8d6ad2c 100644 --- a/lib/rag/ingestion/ingestion-image/Dockerfile +++ b/lib/rag/ingestion/ingestion-image/Dockerfile @@ -1,21 +1,6 @@ ARG BASE_IMAGE=public.ecr.aws/lambda/python:3.13 FROM ${BASE_IMAGE} -# Apply SSH security hardening - disable weak ciphers (3DES-CBC, etc.) -RUN mkdir -p /etc/ssh && \ - echo "" >> /etc/ssh/ssh_config && \ - echo "# LISA Security Hardening - Disable weak ciphers" >> /etc/ssh/ssh_config && \ - echo "Host *" >> /etc/ssh/ssh_config && \ - echo " Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com" >> /etc/ssh/ssh_config && \ - echo " MACs hmac-sha2-256,hmac-sha2-512,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com" >> /etc/ssh/ssh_config && \ - echo " KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512" >> /etc/ssh/ssh_config && \ - if [ -f /etc/ssh/sshd_config ]; then \ - echo "" >> /etc/ssh/sshd_config && \ - echo "# LISA Security Hardening - Disable weak ciphers" >> /etc/ssh/sshd_config && \ - echo "Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com" >> /etc/ssh/sshd_config && \ - echo "MACs hmac-sha2-256,hmac-sha2-512,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com" >> /etc/ssh/sshd_config && \ - echo "KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512" >> /etc/ssh/sshd_config; \ - fi ARG BUILD_DIR=build diff --git a/lib/schema/configSchema.ts b/lib/schema/configSchema.ts index 47f965977..6b73c9fd6 100644 --- a/lib/schema/configSchema.ts +++ b/lib/schema/configSchema.ts @@ -58,12 +58,12 @@ export enum ModelType { * @property {string} pgVectorSecurityGroupId - Security Group ID. */ export const SecurityGroupConfigSchema = z.object({ - modelSecurityGroupId: z.string().startsWith('sg-'), - restAlbSecurityGroupId: z.string().startsWith('sg-'), - lambdaSecurityGroupId: z.string().startsWith('sg-'), - liteLlmDbSecurityGroupId: z.string().startsWith('sg-'), - openSearchSecurityGroupId: z.string().startsWith('sg-').optional(), - pgVectorSecurityGroupId: z.string().startsWith('sg-').optional(), + modelSecurityGroupId: z.string().regex(/^sg-/, 'Must start with sg-'), + restAlbSecurityGroupId: z.string().regex(/^sg-/, 'Must start with sg-'), + lambdaSecurityGroupId: z.string().regex(/^sg-/, 'Must start with sg-'), + liteLlmDbSecurityGroupId: z.string().regex(/^sg-/, 'Must start with sg-'), + openSearchSecurityGroupId: z.string().regex(/^sg-/, 'Must start with sg-').optional(), + pgVectorSecurityGroupId: z.string().regex(/^sg-/, 'Must start with sg-').optional(), }) .describe('Security Group Overrides used across stacks.'); @@ -728,7 +728,8 @@ const FastApiContainerConfigSchema = z.object({ sslCertIamArn: z.string().nullish().default(null).describe('ARN of the self-signed cert to be used throughout the system'), imageConfig: ImageAssetSchema.optional().describe('Override image configuration for ECS FastAPI Containers'), buildConfig: z.object({ - PRISMA_CACHE_DIR: z.string().optional().describe('Override with a path relative to the build directory for a pre-cached prisma directory. Defaults to PRISMA_CACHE.') + PRISMA_CACHE_DIR: z.string().optional().describe('Override with a path relative to the build directory for a pre-cached prisma directory. Defaults to PRISMA_CACHE.'), + PRISMA_PLATFORM: z.string().optional().describe('Prisma engine binary platform target for offline cache generation. Must match the container base image OS. Defaults to debian-openssl-3.0.x (matches python:3.13-slim). Common values: debian-openssl-3.0.x, rhel-openssl-3.0.x, linux-musl-openssl-3.0.x.') }).default({}), rdsConfig: RdsInstanceConfig .default({ @@ -895,7 +896,7 @@ export const RawConfigObject = z.object({ batchIngestionConfig: ImageAssetSchema.optional().describe('Image override for Batch Ingestion'), vpcId: z.string().optional().describe('VPC ID for the application. (e.g. vpc-0123456789abcdef)'), subnets: z.array(z.object({ - subnetId: z.string().startsWith('subnet-'), + subnetId: z.string().regex(/^subnet-/, 'Must start with subnet-'), ipv4CidrBlock: z.string(), availabilityZone: z.string().describe('Specify the availability zone for the subnet in the format {region}{letter} (e.g., us-east-1a).'), })).optional().describe('Array of subnet objects for the application. These contain a subnetId(e.g. [subnet-fedcba9876543210] and ipv4CidrBlock'), @@ -1025,6 +1026,8 @@ export const RawConfigObject = z.object({ convertInlinePoliciesToManaged: z.boolean().optional().default(false).describe('Convert inline policies to managed policies'), iamRdsAuth: z.boolean().optional().default(false) .describe('Enable IAM authentication for RDS. When true (default), IAM authentication is used and the bootstrap password is deleted after setup. When false, password-based authentication is used. WARNING: Switching from true to false after deployment is not supported - the master password is permanently deleted when IAM auth is enabled. This is a one-way migration.'), + prepareDockerOffline: z.boolean().default(false) + .describe('When true, pre-generates all Docker build dependencies (tiktoken cache, Prisma engine binaries) during CDK synth so that subsequent Docker builds can run in an airgapped environment without access to binaries.prisma.sh or other external hosts. Requires Docker and internet access on the synth machine.'), }); export const RawConfigSchema = RawConfigObject diff --git a/lib/serve/ecs-model/embedding/instructor/Dockerfile b/lib/serve/ecs-model/embedding/instructor/Dockerfile index 98f7332c9..9ff1bab85 100644 --- a/lib/serve/ecs-model/embedding/instructor/Dockerfile +++ b/lib/serve/ecs-model/embedding/instructor/Dockerfile @@ -4,20 +4,8 @@ FROM ${BASE_IMAGE} RUN apt-get update -y && apt-get upgrade -y && rm -rf /var/lib/apt/lists/* # Apply SSH security hardening - disable weak ciphers (3DES-CBC, etc.) -RUN mkdir -p /etc/ssh && \ - echo "" >> /etc/ssh/ssh_config && \ - echo "# LISA Security Hardening - Disable weak ciphers" >> /etc/ssh/ssh_config && \ - echo "Host *" >> /etc/ssh/ssh_config && \ - echo " Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com" >> /etc/ssh/ssh_config && \ - echo " MACs hmac-sha2-256,hmac-sha2-512,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com" >> /etc/ssh/ssh_config && \ - echo " KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512" >> /etc/ssh/ssh_config && \ - if [ -f /etc/ssh/sshd_config ]; then \ - echo "" >> /etc/ssh/sshd_config && \ - echo "# LISA Security Hardening - Disable weak ciphers" >> /etc/ssh/sshd_config && \ - echo "Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com" >> /etc/ssh/sshd_config && \ - echo "MACs hmac-sha2-256,hmac-sha2-512,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com" >> /etc/ssh/sshd_config && \ - echo "KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512" >> /etc/ssh/sshd_config; \ - fi +COPY ssh-hardening.sh /tmp/ssh-hardening.sh +RUN chmod +x /tmp/ssh-hardening.sh && /tmp/ssh-hardening.sh && rm /tmp/ssh-hardening.sh #### POINT TO NEW PYPI CONFIG ARG PYPI_INDEX_URL diff --git a/lib/serve/ecs-model/embedding/tei/Dockerfile b/lib/serve/ecs-model/embedding/tei/Dockerfile index b2bcfa2f1..d86c7be12 100644 --- a/lib/serve/ecs-model/embedding/tei/Dockerfile +++ b/lib/serve/ecs-model/embedding/tei/Dockerfile @@ -2,20 +2,8 @@ ARG BASE_IMAGE=ghcr.io/huggingface/text-embeddings-inference:latest FROM ${BASE_IMAGE} # Apply SSH security hardening - disable weak ciphers (3DES-CBC, etc.) -RUN mkdir -p /etc/ssh && \ - echo "" >> /etc/ssh/ssh_config && \ - echo "# LISA Security Hardening - Disable weak ciphers" >> /etc/ssh/ssh_config && \ - echo "Host *" >> /etc/ssh/ssh_config && \ - echo " Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com" >> /etc/ssh/ssh_config && \ - echo " MACs hmac-sha2-256,hmac-sha2-512,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com" >> /etc/ssh/ssh_config && \ - echo " KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512" >> /etc/ssh/ssh_config && \ - if [ -f /etc/ssh/sshd_config ]; then \ - echo "" >> /etc/ssh/sshd_config && \ - echo "# LISA Security Hardening - Disable weak ciphers" >> /etc/ssh/sshd_config && \ - echo "Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com" >> /etc/ssh/sshd_config && \ - echo "MACs hmac-sha2-256,hmac-sha2-512,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com" >> /etc/ssh/sshd_config && \ - echo "KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512" >> /etc/ssh/sshd_config; \ - fi +COPY ssh-hardening.sh /tmp/ssh-hardening.sh +RUN chmod +x /tmp/ssh-hardening.sh && /tmp/ssh-hardening.sh && rm /tmp/ssh-hardening.sh ##### Download S3 mountpoints and boto3 ARG MOUNTS3_DEB_URL diff --git a/lib/serve/ecs-model/ssh-hardening.sh b/lib/serve/ecs-model/ssh-hardening.sh new file mode 100644 index 000000000..e63f3c462 --- /dev/null +++ b/lib/serve/ecs-model/ssh-hardening.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# LISA Security Hardening - Disable weak SSH ciphers (3DES-CBC, etc.) +set -e + +mkdir -p /etc/ssh + +# Harden SSH client config +cat >> /etc/ssh/ssh_config <<'EOF' + +# LISA Security Hardening - Disable weak ciphers +Host * + Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com + MACs hmac-sha2-256,hmac-sha2-512,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com + KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512 +EOF + +# Harden SSH server config if present +if [ -f /etc/ssh/sshd_config ]; then + cat >> /etc/ssh/sshd_config <<'EOF' + +# LISA Security Hardening - Disable weak ciphers +Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com +MACs hmac-sha2-256,hmac-sha2-512,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com +KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512 +EOF +fi diff --git a/lib/serve/ecs-model/textgen/tgi/Dockerfile b/lib/serve/ecs-model/textgen/tgi/Dockerfile index bbabf84b3..9d72542ba 100644 --- a/lib/serve/ecs-model/textgen/tgi/Dockerfile +++ b/lib/serve/ecs-model/textgen/tgi/Dockerfile @@ -2,20 +2,8 @@ ARG BASE_IMAGE=ghcr.io/huggingface/text-generation-inference:latest FROM ${BASE_IMAGE} # Apply SSH security hardening - disable weak ciphers (3DES-CBC, etc.) -RUN mkdir -p /etc/ssh && \ - echo "" >> /etc/ssh/ssh_config && \ - echo "# LISA Security Hardening - Disable weak ciphers" >> /etc/ssh/ssh_config && \ - echo "Host *" >> /etc/ssh/ssh_config && \ - echo " Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com" >> /etc/ssh/ssh_config && \ - echo " MACs hmac-sha2-256,hmac-sha2-512,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com" >> /etc/ssh/ssh_config && \ - echo " KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512" >> /etc/ssh/ssh_config && \ - if [ -f /etc/ssh/sshd_config ]; then \ - echo "" >> /etc/ssh/sshd_config && \ - echo "# LISA Security Hardening - Disable weak ciphers" >> /etc/ssh/sshd_config && \ - echo "Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com" >> /etc/ssh/sshd_config && \ - echo "MACs hmac-sha2-256,hmac-sha2-512,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com" >> /etc/ssh/sshd_config && \ - echo "KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512" >> /etc/ssh/sshd_config; \ - fi +COPY ssh-hardening.sh /tmp/ssh-hardening.sh +RUN chmod +x /tmp/ssh-hardening.sh && /tmp/ssh-hardening.sh && rm /tmp/ssh-hardening.sh ##### Download S3 mountpoints and boto3 ARG MOUNTS3_DEB_URL diff --git a/lib/serve/ecs-model/vllm/Dockerfile b/lib/serve/ecs-model/vllm/Dockerfile index 7fc287846..e4e421f38 100644 --- a/lib/serve/ecs-model/vllm/Dockerfile +++ b/lib/serve/ecs-model/vllm/Dockerfile @@ -2,20 +2,8 @@ ARG BASE_IMAGE=public.ecr.aws/deep-learning-containers/vllm:0.17-gpu-py312-ec2 FROM ${BASE_IMAGE} # Apply SSH security hardening - disable weak ciphers (3DES-CBC, etc.) -RUN mkdir -p /etc/ssh && \ - echo "" >> /etc/ssh/ssh_config && \ - echo "# LISA Security Hardening - Disable weak ciphers" >> /etc/ssh/ssh_config && \ - echo "Host *" >> /etc/ssh/ssh_config && \ - echo " Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com" >> /etc/ssh/ssh_config && \ - echo " MACs hmac-sha2-256,hmac-sha2-512,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com" >> /etc/ssh/ssh_config && \ - echo " KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512" >> /etc/ssh/ssh_config && \ - if [ -f /etc/ssh/sshd_config ]; then \ - echo "" >> /etc/ssh/sshd_config && \ - echo "# LISA Security Hardening - Disable weak ciphers" >> /etc/ssh/sshd_config && \ - echo "Ciphers aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com" >> /etc/ssh/sshd_config && \ - echo "MACs hmac-sha2-256,hmac-sha2-512,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com" >> /etc/ssh/sshd_config && \ - echo "KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512" >> /etc/ssh/sshd_config; \ - fi +COPY ssh-hardening.sh /tmp/ssh-hardening.sh +RUN chmod +x /tmp/ssh-hardening.sh && /tmp/ssh-hardening.sh && rm /tmp/ssh-hardening.sh ##### DOWNLOAD MOUNTPOINTS S3 ARG MOUNTS3_DEB_URL diff --git a/lib/serve/rest-api/Dockerfile b/lib/serve/rest-api/Dockerfile index 4c18cda38..01fd65079 100644 --- a/lib/serve/rest-api/Dockerfile +++ b/lib/serve/rest-api/Dockerfile @@ -4,7 +4,20 @@ FROM ${BASE_IMAGE} ARG PRISMA_CACHE_DIR=PRISMA_CACHE ENV PRISMA_CACHE_DIR=$PRISMA_CACHE_DIR +# Install build dependencies for madoka package and Node.js for prisma-client-py. +# prisma-client-py requires a Node.js runtime to execute the Prisma CLI. Without a global +# node binary, it falls back to nodeenv which downloads Node.js from the internet — breaking +# airgapped builds. Installing nodejs here ensures prisma works offline. +RUN if command -v apt-get >/dev/null 2>&1; then \ + apt-get update -y && apt-get upgrade -y && apt-get install -y gcc g++ make procps nodejs libatomic1 && rm -rf /var/lib/apt/lists/*; \ + elif command -v yum >/dev/null 2>&1; then \ + yum install -y gcc gcc-c++ make procps-ng nodejs libatomic && yum clean all; \ + elif command -v apk >/dev/null 2>&1; then \ + apk add --no-cache gcc g++ make musl-dev procps nodejs libatomic; \ + fi + # Apply SSH security hardening - disable weak ciphers (3DES-CBC, etc.) +# This runs AFTER apt-get upgrade to avoid conffile conflicts with openssh-client upgrades. RUN mkdir -p /etc/ssh && \ echo "" >> /etc/ssh/ssh_config && \ echo "# LISA Security Hardening - Disable weak ciphers" >> /etc/ssh/ssh_config && \ @@ -20,15 +33,6 @@ RUN mkdir -p /etc/ssh && \ echo "KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512" >> /etc/ssh/sshd_config; \ fi -# Install build dependencies for madoka package -RUN if command -v apt-get >/dev/null 2>&1; then \ - apt-get update -y && apt-get upgrade -y && apt-get install -y gcc g++ make procps && rm -rf /var/lib/apt/lists/*; \ - elif command -v yum >/dev/null 2>&1; then \ - yum install -y gcc gcc-c++ make procps-ng && yum clean all; \ - elif command -v apk >/dev/null 2>&1; then \ - apk add --no-cache gcc g++ make musl-dev procps; \ - fi - # Copy LiteLLM config directly out of the LISA config.yaml file ARG LITELLM_CONFIG @@ -49,18 +53,35 @@ RUN pip install --no-cache-dir --upgrade -r requirements.txt # Copy prisma cache directory (always exists, may be empty or populated) COPY ${PRISMA_CACHE_DIR} /tmp/prisma-cache/ -# Pre-cache prisma for prisma-client-py -# If the copied directory has content, use it (for offline environments) -# Otherwise, download it during build (requires internet) -RUN mkdir -p /root/.cache && \ - if [ -d "/tmp/prisma-cache" ] && [ -n "$(ls /tmp/prisma-cache 2>/dev/null)" ]; then \ - echo "Using pre-cached Prisma dependencies from host" && \ - cp -r /tmp/prisma-cache/prisma* /root/.cache && \ - rm -rf /tmp/prisma-cache; \ - else \ - echo "Fetching Prisma Dependencies (requires internet)" && \ - prisma version; \ - fi +# Generate Prisma Python client at build time following LiteLLM's approach: +# 1. Copy LiteLLM's schema.prisma to the working directory +# 2. If PRISMA_CACHE has pre-downloaded engine binaries (for offline/airgap builds), +# set env vars to point directly at the binaries — this bypasses platform detection +# and prevents any download attempts from binaries.prisma.sh +# 3. Run prisma generate — this produces the Python client code +# This ensures prisma generate NEVER runs at runtime. +RUN SCHEMA_PATH=$(python3 -c "import litellm, os; print(os.path.join(os.path.dirname(litellm.__file__), 'proxy', 'schema.prisma'))") && \ + echo "LiteLLM schema: ${SCHEMA_PATH}" && \ + cp "${SCHEMA_PATH}" /app/schema.prisma && \ + mkdir -p /root/.cache && \ + if [ -d "/tmp/prisma-cache" ] && [ -n "$(ls -A /tmp/prisma-cache 2>/dev/null | grep -v '^\.gitkeep$')" ]; then \ + echo "Using pre-cached Prisma binaries from host" && \ + cp -r /tmp/prisma-cache/* /root/.cache/ && \ + rm -rf /tmp/prisma-cache && \ + QE=$(find /root/.cache -name 'query-engine-*' -not -name '*.sha256' -type f | head -1) && \ + SE=$(find /root/.cache -name 'schema-engine-*' -not -name '*.sha256' -type f | head -1) && \ + LQE=$(find /root/.cache -name 'libquery_engine-*' -o -name 'libquery-engine' -type f | head -1) && \ + echo " Query engine: ${QE}" && \ + echo " Schema engine: ${SE}" && \ + echo " Lib query engine: ${LQE}" && \ + export PRISMA_QUERY_ENGINE_BINARY="${QE}" && \ + export PRISMA_SCHEMA_ENGINE_BINARY="${SE}" && \ + export PRISMA_QUERY_ENGINE_LIBRARY="${LQE}" && \ + export PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING=1; \ + fi && \ + prisma generate --schema /app/schema.prisma && \ + echo "Prisma client generated successfully" && \ + python3 -c "from prisma import Prisma; print('Prisma client import verified')" # Copy the source code into the container COPY src/ ./src diff --git a/lib/serve/rest-api/PRISMA_CACHE/.gitkeep b/lib/serve/rest-api/PRISMA_CACHE/.gitkeep deleted file mode 100644 index ad102e091..000000000 --- a/lib/serve/rest-api/PRISMA_CACHE/.gitkeep +++ /dev/null @@ -1,5 +0,0 @@ -# Placeholder to ensure PRISMA_CACHE directory exists in build context -# For offline builds, populate this directory by copying the complete Prisma cache: -# 1. Install prisma: pip3 install prisma -# 2. Generate cache: prisma version -# 3. Copy cache: cp -r ~/.cache/prisma* lib/serve/rest-api/PRISMA_CACHE/ diff --git a/lib/serve/rest-api/src/api/endpoints/v2/litellm_passthrough.py b/lib/serve/rest-api/src/api/endpoints/v2/litellm_passthrough.py index 6251a67d7..93869eb60 100644 --- a/lib/serve/rest-api/src/api/endpoints/v2/litellm_passthrough.py +++ b/lib/serve/rest-api/src/api/endpoints/v2/litellm_passthrough.py @@ -515,6 +515,12 @@ async def litellm_passthrough(request: Request, api_path: str) -> Response: logger.error(f"Invalid JSON in request body: {e}") raise HTTPException(status_code=HTTP_400_BAD_REQUEST, detail="Invalid JSON in request body") + # Strip non-standard keys that clients (e.g. OpenAI SDK) may include in the request body. + # LiteLLM forwards unrecognized params to Bedrock's additionalModelRequestFields, and + # some Bedrock regions (e.g. GovCloud) strictly reject extraneous keys like "telemetry". + if isinstance(params, dict): + params.pop("telemetry", None) + # If the caller didn't already set OpenAI's "user" identifier, populate it # with the human-readable LISA username so LiteLLM can surface it in logs. if isinstance(params, dict) and lisa_username and (not params.get("user")): diff --git a/package-lock.json b/package-lock.json index 5a67606ac..289dd9f8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,7 +63,7 @@ "tsx": "^4.21.0", "typescript": "~5.9.3", "wait-on": "^9.0.4", - "zod2md": "^0.3.2" + "zod2md": "^0.3.3" }, "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "4.59.0", @@ -20145,9 +20145,9 @@ } }, "node_modules/zod2md": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/zod2md/-/zod2md-0.3.2.tgz", - "integrity": "sha512-40ItDcgzWJfaTMn1waWfamaGMxi2bHK21aTKykcjI5FOVupHuL0r/JBa1cp5371rkArld1+H1tGhb8nY3n3ErA==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/zod2md/-/zod2md-0.3.3.tgz", + "integrity": "sha512-d20k4HZxCNL9ZHDpWunLyI6HARL4Q4G6C15ItFgggMzYKomU+p3yOlrxbUMde4BCXpqHsOD3S12xqr3SXvmldg==", "dev": true, "license": "MIT", "dependencies": { @@ -20160,6 +20160,9 @@ "bin": { "zod2md": "dist/bin.js" }, + "engines": { + "node": ">=22.16.0" + }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } diff --git a/package.json b/package.json index 433f13b13..5ff5cdab3 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "tsx": "^4.21.0", "typescript": "~5.9.3", "wait-on": "^9.0.4", - "zod2md": "^0.3.2" + "zod2md": "^0.3.3" }, "dependencies": { "@aws-sdk/util-dynamodb": "^3.996.2", diff --git a/scripts/generate-prisma-cache.py b/scripts/generate-prisma-cache.py new file mode 100644 index 000000000..9f2bf5565 --- /dev/null +++ b/scripts/generate-prisma-cache.py @@ -0,0 +1,166 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Generate Prisma engine binary cache for offline/airgapped Docker builds. + +Downloads Prisma engine binaries from binaries.prisma.sh and places them in +the PRISMA_CACHE directory so the Dockerfile can use them without internet. + +Unlike running `prisma version` (which downloads binaries for the *current* +host platform), this script downloads binaries for a specified *target* +platform. This is critical when the build machine OS differs from the Docker +container OS — e.g. building on Amazon Linux or macOS while the container +uses python:3.13-slim (Debian). + +The Dockerfile sets PRISMA_QUERY_ENGINE_BINARY and PRISMA_SCHEMA_ENGINE_BINARY +env vars to point at these cached files, bypassing Prisma's platform detection +and download logic entirely. + +Usage: + python3 scripts/generate-prisma-cache.py [--platform ] + + platform defaults to 'debian-openssl-3.0.x' which matches python:3.13-slim. + Common platforms: + debian-openssl-3.0.x (Debian Bookworm/Trixie, python:3.13-slim) + debian-openssl-1.1.x (Debian Bullseye) + rhel-openssl-1.0.x (Amazon Linux 2, RHEL 7) + rhel-openssl-3.0.x (Amazon Linux 2023, RHEL 9) + linux-musl (Alpine) + linux-musl-openssl-3.0.x (Alpine with OpenSSL 3) +""" + +import gzip +import os +import shutil +import ssl +import stat +import sys +import urllib.request + +import certifi +from prisma import config + +# Engine binaries to download +ENGINES = ["query-engine", "schema-engine"] + +# Mirror URL (can be overridden with PRISMA_ENGINES_MIRROR env var) +DEFAULT_MIRROR = "https://binaries.prisma.sh" + + +def get_engine_version() -> str: + """Get the expected engine version from prisma-client-py.""" + try: + return str(config.expected_engine_version) + except ImportError: + raise SystemExit( + "prisma-client-py must be installed to determine the engine version. " "Install it with: pip install prisma" + ) + + +def download_engine(mirror: str, engine_hash: str, platform: str, engine_name: str, output_dir: str) -> str: + """Download a single engine binary and return the output path.""" + url = f"{mirror}/all_commits/{engine_hash}/{platform}/{engine_name}.gz" + output_path = os.path.join(output_dir, f"{engine_name}-{platform}") + + print(f" Downloading {engine_name} for {platform}...") + print(f" URL: {url}") + + try: + ctx = ssl.create_default_context() + # Honor REQUESTS_CA_BUNDLE / SSL_CERT_FILE / AWS_CA_BUNDLE env vars + ca_bundle = ( + os.environ.get("REQUESTS_CA_BUNDLE") or os.environ.get("SSL_CERT_FILE") or os.environ.get("AWS_CA_BUNDLE") + ) + if ca_bundle: + ctx.load_verify_locations(ca_bundle) + else: + # Try certifi if available (common in pip-managed environments) + try: + ctx.load_verify_locations(certifi.where()) + except ImportError: + pass + req = urllib.request.Request(url, headers={"User-Agent": "prisma-client-py"}) + with urllib.request.urlopen(req, context=ctx) as response: + compressed_data = response.read() + + # Decompress gzip + decompressed_data = gzip.decompress(compressed_data) + + with open(output_path, "wb") as f: + f.write(decompressed_data) + + # Make executable + os.chmod(output_path, os.stat(output_path).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH) + + size_mb = len(decompressed_data) / (1024 * 1024) + print(f" Saved: {output_path} ({size_mb:.1f} MB)") + return output_path + + except urllib.error.HTTPError as e: + print(f" Failed to download {url}: HTTP {e.code}") + raise + except Exception as e: + print(f" Failed to download {url}: {e}") + raise + + +def main() -> None: + if len(sys.argv) < 2: + print("Usage: python3 scripts/generate-prisma-cache.py [--platform ]") + print("\nDefaults to debian-openssl-3.0.x (matches python:3.13-slim)") + sys.exit(1) + + output_dir = sys.argv[1] + platform = "debian-openssl-3.0.x" + + # Parse --platform flag + for i, arg in enumerate(sys.argv): + if arg == "--platform" and i + 1 < len(sys.argv): + platform = sys.argv[i + 1] + + mirror = os.environ.get("PRISMA_ENGINES_MIRROR", DEFAULT_MIRROR) + engine_hash = get_engine_version() + + print("Generating Prisma engine cache:") + print(f" Engine hash: {engine_hash}") + print(f" Platform: {platform}") + print(f" Mirror: {mirror}") + print(f" Output: {output_dir}") + print() + + # Create output directory (clean existing non-.gitkeep files) + os.makedirs(output_dir, exist_ok=True) + for item in os.listdir(output_dir): + if item == ".gitkeep": + continue + item_path = os.path.join(output_dir, item) + if os.path.isdir(item_path): + shutil.rmtree(item_path) + else: + os.remove(item_path) + + # Download each engine binary + downloaded = [] + for engine_name in ENGINES: + path = download_engine(mirror, engine_hash, platform, engine_name, output_dir) + downloaded.append(path) + + print() + print(f"Prisma engine cache generated successfully at {output_dir}") + print(f" Files: {', '.join(os.path.basename(p) for p in downloaded)}") + + +if __name__ == "__main__": + main()