Introduction
Hardened Runtime Image
The Kipchak Enterprise runtime image runs an API as a single static FrankenPHP binary on an empty filesystem, with no shell, no package manager and no root user.
Introduction
Note: The runtime image is only available with Kipchak Enterprise.
The runtime image is a container image for running Kipchak APIs in production, for organisations whose security requirements a general-purpose PHP image does not meet.
FrankenPHP, PHP and every library they use are compiled from source into one static binary, with a static PHP command-line interface alongside it for console scripts. The image contains those two binaries and nothing else that a running API does not need:
- no shell, no package manager and no programs other than
frankenphpandphp; - no C library or other shared libraries;
- no root user: the server runs as an unprivileged user (
65532); - one writable directory,
/tmp, so the rest of the filesystem can be mounted read-only.
The image is approximately 310 MB, almost all of it the two binaries. The free Kipchak FrankenPHP image is approximately 2 GB.
It is built for PHP 8.4 and PHP 8.5, for linux/amd64 and linux/arm64.
Images
Images are delivered from the authenticated Kipchak Enterprise container registry. If you have an enterprise licence, please contact your account representative for access.
| Image | Use |
|---|---|
1x.ax/mamluk/enterprise/php/8.5:franken | The runtime, at the latest PHP 8.5 release |
1x.ax/mamluk/enterprise/php/8.5:franken-build | The build image, for installing dependencies and for CI |
1x.ax/mamluk/enterprise/php/8.5:franken-alpine | The runtime on Alpine Linux, for APIs that need to add programs |
Each tag also exists pinned to a PHP release, for example franken-8.5.11, franken-build-8.5.11 and franken-alpine-8.5.11. The same images exist for PHP 8.4 under 1x.ax/mamluk/enterprise/php/8.4.
Extensions
Extensions are compiled into the binary, and cannot be added to the image. The image includes:
apcu, bcmath, calendar, ctype, curl, dom, exif, fileinfo, filter, gd, gettext, gmp, iconv, igbinary, intl, ldap, mbregex, mbstring, memcached, mysqli, mysqlnd, opcache, openssl, password-argon2, pcntl, pdo, pdo_mysql, pdo_pgsql, pdo_sqlite, pgsql, phar, posix, protobuf, rdkafka, redis, session, simplexml, sockets, sodium, sqlite3, sysvmsg, sysvsem, sysvshm, tidy, tokenizer, xml, xmlreader, xmlwriter, xsl, yaml, zip, zlib, zstd.
This covers every Kipchak driver, including the Kafka driver, which needs rdkafka. The free Kipchak images do not include rdkafka.
Each image lists its extensions and their versions in /usr/share/kipchak-runtime/extensions.txt, and every source compiled into it in /usr/share/kipchak-runtime/sources.txt, for a software bill of materials.
Using the runtime image
The runtime image has no shell, so dependencies cannot be installed in it. Install them in the build image, which has the same binary, a PHP command-line interface with the same extensions, Composer and git, and copy the result into the runtime image:
FROM 1x.ax/mamluk/enterprise/php/8.5:franken-build AS build
COPY composer.json composer.lock ./
RUN --mount=type=secret,id=composer-auth,target=/root/.composer/auth.json \
composer install --no-dev --no-scripts --no-autoloader --no-interaction
COPY . .
RUN composer dump-autoload --no-dev --classmap-authoritative \
&& rm -rf .git .gitignore tests
FROM 1x.ax/mamluk/enterprise/php/8.5:franken
COPY --from=build /var/www /var/www
Build it with your Composer credentials as a build secret, so that they are not stored in the image:
docker build --secret id=composer-auth,src=$HOME/.composer/auth.json -t my-kipchak-api .
Because the build image has exactly the runtime's extensions, Composer checks your API's requirements against the extensions it will run with.
The default configuration runs html/index.php as a worker, as in the starter project. To use your own Caddyfile, copy it to /etc/frankenphp/Caddyfile. To add PHP settings, add .ini files to /etc/frankenphp/php.d/ rather than replacing /etc/frankenphp/php.ini.
What is different from the free images
- Port 8080. The server does not run as root, so it cannot listen on ports below 1024. Map the port in your Kubernetes Service, or with
docker run -p 80:8080. - Programs cannot be run. The PHP functions that start programs (
exec,shell_exec,system,passthru,popen,proc_openandpcntl_exec) are disabled, because the image contains no programs. The Git driver runs thegitprogram, and needs the Alpine variant. - Code cannot change. OPcache does not check files for changes. Your code is owned by root and is read-only to the server.
- Plain HTTP and HTTP/2. TLS ends at your load balancer or ingress. The server manages no certificates.
- The administration API is off. To expose Prometheus metrics, set
CADDY_ADMINtolocalhost:2019andCADDY_GLOBAL_OPTIONStometrics. - No Mercure or Vulcain. Caddy is built with only the modules an API needs.
- No Xdebug and no JIT. Develop with the free
franken-devimage. OPcache is enabled; its JIT compiler is not available in this build of PHP.
Adding programs
The franken-alpine variant is the same binary, with the same settings and the same unprivileged user, on Alpine Linux. It has a shell and Alpine's package manager, so programs can be added, and the functions that start programs are enabled. To use the Git driver:
FROM 1x.ax/mamluk/enterprise/php/8.5:franken-alpine
USER root
RUN apk add --no-cache git
USER 65532
COPY --from=build /var/www /var/www
Use the Alpine variant only when an API needs a program. Each package added is software to keep patched, and a shell is a tool available to an attacker. Extensions cannot be added to the Alpine variant either.
Command-line scripts
Run PHP scripts, such as database migrations and queue consumers, with the php command-line interface in the image. It has the same extensions and settings as the server. The image's entrypoint is frankenphp, so name php as the entrypoint:
docker run --rm --entrypoint php my-kipchak-api /var/www/bin/console migrations:up
In Kubernetes, set it as the command of the container, for example in a Job that runs migrations before a deployment:
command: ["php", "/var/www/bin/console", "migrations:up"]
Kubernetes
The image runs under the most restrictive pod security settings. Mount a writable volume at /tmp:
containers:
- name: kipchak-api
image: my-registry/my-kipchak-api:1.0.0
ports:
- containerPort: 8080
securityContext:
runAsNonRoot: true
runAsUser: 65532
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
There is no shell to open in a running container. Use HTTP readiness and liveness probes, the container's logs, and kubectl debug with an ephemeral container to inspect a running pod.
Updates
A static binary receives no updates from an operating system, so the images are rebuilt:
- daily, at the latest release of each PHP version, so a PHP release is in the images within a day;
- weekly, from the latest release of every library and extension, so a security release of a library such as OpenSSL or curl is in the images within a week.
Container vulnerability scanners identify the Go modules in the binary, such as Caddy, but not the C libraries compiled into it. The weekly rebuild and the list of sources in each image address this. To receive updates, rebuild your API's image from the moving tags, such as franken, or move a pinned tag forward.