# ShelllessRelease
Shell-free, distribution-hardened `mix release` for distroless images.
`mix release` emits a layered set of `#!/bin/sh` scripts as its launch interface
(`bin/<name>` → `releases/<v>/elixir` → `erts-*/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.
`ShelllessRelease` replaces them with a single native launcher (compiled from the
bundled `priv/launcher/start.c`) that `execve`s the BEAM directly and pre-starts
distribution 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.
On top of being shell-free, it hardens the release:
- **Command whitelist** — a fixed set of verbs (the server plus your declared
task verbs) baked into the launcher at build time. There is no generic `eval`,
so an attacker who controls the container's args cannot run arbitrary code.
- **Pinned distribution port** — no ephemeral fallback, so the distribution port
is firewallable.
- **Mandatory mutual-TLS distribution** — fail-closed without certs.
- **Fail-closed cookie strength check.**
- **EPMD-less mode** (optional) — drops the Erlang Port Mapper Daemon and port
4369 entirely. See [`ShelllessRelease.EpmdLess`](https://hexdocs.pm/shellless_release/ShelllessRelease.EpmdLess.html).
## Installation
Add `shellless_release` to your deps in `mix.exs`:
```elixir
def deps do
[
{:shellless_release, "~> 0.1"}
]
end
```
## Usage
Add the hardening step to your release in `mix.exs`:
```elixir
def project do
[
# ...
releases: [
my_app: [
steps: [:assemble, &ShelllessRelease.harden/1]
]
]
]
end
```
Configure it (all keys are optional):
```elixir
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,
# EPMD-less distribution (default true). Requires one node per IP.
epmdless: 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 is expected
to run in your Dockerfile's builder stage where a C compiler (`cc`/`gcc`) is
present. The library itself ships only C source — it never compiles anything at
`deps.compile` time.
At runtime the entry points behave exactly like a stock release:
```sh
bin/server # boot the application
bin/migrate # run a whitelisted task verb (non-distributed)
```
## Requirements
- Elixir `~> 1.15`
- A C compiler (`cc` or `gcc`) available in the release build stage — typically
your Dockerfile builder image (e.g. `build-essential`).
## Documentation
Full documentation is on [HexDocs](https://hexdocs.pm/shellless_release). Start
with the [`ShelllessRelease`](https://hexdocs.pm/shellless_release/ShelllessRelease.html)
moduledoc for the launcher, and
[`ShelllessRelease.EpmdLess`](https://hexdocs.pm/shellless_release/ShelllessRelease.EpmdLess.html)
for EPMD-less distribution.
## License
MIT — see [LICENSE](LICENSE).