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 frankenphp and php;
  • 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.

ImageUse
1x.ax/mamluk/enterprise/php/8.5:frankenThe runtime, at the latest PHP 8.5 release
1x.ax/mamluk/enterprise/php/8.5:franken-buildThe build image, for installing dependencies and for CI
1x.ax/mamluk/enterprise/php/8.5:franken-alpineThe 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_open and pcntl_exec) are disabled, because the image contains no programs. The Git driver runs the git program, 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_ADMIN to localhost:2019 and CADDY_GLOBAL_OPTIONS to metrics.
  • No Mercure or Vulcain. Caddy is built with only the modules an API needs.
  • No Xdebug and no JIT. Develop with the free franken-dev image. 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.

Previous
Deployment
Next
Routes