ShelllessRelease (ShelllessRelease v0.1.0)

Copy Markdown View Source

Build a shell-free, distribution-hardened mix release for distroless images.

mix release emits a layered set of #!/bin/sh scripts as the launch interface (bin/<name>releases/<v>/elixirerts-*/bin/erl), all of which ultimately exec the native erlexec. On a distroless :nonroot image there is no /bin/sh, so those scripts cannot run.

This library replaces them with a single native launcher (compiled from the bundled priv/launcher/start.c) that execves the BEAM directly and pre-starts epmd itself, so the image needs no shell. The launcher is a multi-call binary installed over the existing bin/<name> entry points, preserving the interface so nothing downstream changes. It also enforces:

  • a fixed command whitelist (server + your declared task verbs) — no generic eval, so controlling the container's args can't run arbitrary code;
  • a pinned distribution port (no ephemeral fallback → firewallable);
  • mandatory mutual-TLS distribution (fail-closed without certs);
  • a fail-closed cookie strength check.

Usage

Add to your release in mix.exs:

releases: [
  my_app: [
    steps: [:assemble, &ShelllessRelease.harden/1]
  ]
]

Configure it (all keys optional except :tasks if you want task verbs):

config :shellless_release,
  # zero-arg task verbs -> compiled-in expressions (the whitelist).
  # These replace `bin/<verb>` shell overlays; each runs non-distributed.
  tasks: [
    migrate: "MyApp.Release.migrate()",
    seed: "MyApp.Release.seed()"
  ],
  # pinned Erlang distribution port (build-time constant; default 24369)
  dist_port: 24369,
  # require TLS distribution + the cert bundle (default true). When false,
  # distribution is cookie-only (only sensible for non-clustered apps).
  require_tls: true,
  # extra entry-point names to install the launcher at, beyond "server"
  # and the task verbs (rarely needed)
  extra_entry_points: [],
  # remove the generated shell launchers after install (default true)
  strip_shell_scripts: true

The launcher is compiled by this step during mix release, which runs in your Dockerfile's builder stage (where a C compiler is present). The library itself ships only source — it never compiles C at deps.compile time.

See ShelllessRelease.EpmdLess for EPMD-less distribution (drops port 4369).

Summary

Functions

Post-:assemble release step. Compiles the launcher, installs it over the entry points, stages the TLS config, and strips the shell launchers.

Functions

harden(release)

@spec harden(Mix.Release.t()) :: Mix.Release.t()

Post-:assemble release step. Compiles the launcher, installs it over the entry points, stages the TLS config, and strips the shell launchers.