Manual single-server container deployment¶
Install OpenShell and a harness sandbox on a dedicated RHEL 9 x86_64 VM or
bare-metal host. Run every shell block in Bash on that host, logged in as the
administrator, unless a command uses sudo or os_run. Keep one shell session
open from the repository checkout through the setup so repo and os_run
remain available. For the OS-image route, use the separate bootc guide.
OpenShell runs under a locked service account with rootless Podman. Praxis is an optional, separately installed model gateway. Review the qualification and policy limits before extending this deployment.
1. Install OpenShell¶
Use a dedicated RHEL 9 x86_64 host. For first qualification, use a disposable
instance before changing a long-lived server. Install Git, Podman, Python 3,
OpenSSH clients, curl, and the SELinux management tools. RHEL 9 does not
package ripgrep, so install the pinned static binary used by the image:
sudo dnf install -y git podman python3 policycoreutils openssh-clients curl
rg_version=14.1.1
rg_sha256=4cf9f2741e6c465ffdb7c26f38056a59e2a2544b51f7cc128ef28337eeae4d8e
curl -fsSL "https://github.com/BurntSushi/ripgrep/releases/download/${rg_version}/ripgrep-${rg_version}-x86_64-unknown-linux-musl.tar.gz" -o /tmp/rg.tar.gz
printf '%s /tmp/rg.tar.gz\n' "${rg_sha256}" | sha256sum -c - &&
tar -xzf /tmp/rg.tar.gz -C /tmp &&
sudo install -m 0755 "/tmp/ripgrep-${rg_version}-x86_64-unknown-linux-musl/rg" /usr/local/bin/rg
In the same host shell, create a checkout under /opt and review the commit
before installing. The checkout and its parent directory are readable by
openshell-svc, so service-owner commands can use the scripts there:
sudo install -d -o "$(id -un)" -m 0755 /opt/secure-single-server
git clone https://github.com/redhat-et/secure-single-server /opt/secure-single-server
cd /opt/secure-single-server
repo="$PWD"
git log -1 --oneline
The installer creates the locked openshell-svc account, enables lingering so
the gateway remains available after logout, and starts the loopback-only
OpenShell gateway with TLS/mTLS for the service operator.
It first verifies Landlock ABI 3 or newer, then pre-pulls every pinned supported
harness image; the selected harness is chosen when its sandbox is created. If
Landlock is unavailable or too old, installation stops with that kernel
diagnostic instead of a later ContainerExited sandbox failure. If it reports
that cgroup delegation needs a host reboot, reboot and rerun the same installer.
2. Set up the service-owner shell helper¶
Run the remaining commands in the same Bash session. The helper runs commands
as the locked OpenShell service account with its home and user bus configured.
Keep the checkout and its parent directories readable by openshell-svc.
repo="$PWD"
uid="$(id -u openshell-svc)"
os_run() {
sudo runuser -u openshell-svc -- env HOME=/var/lib/openshell-svc \
PATH=/usr/local/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin \
XDG_RUNTIME_DIR=/run/user/"$uid" \
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/"$uid"/bus \
OPENSHELL_BIN="${OPENSHELL_BIN:-/usr/local/bin/openshell}" \
"$@"
}
The harness scripts look for the OpenShell CLI at /usr/local/bin/openshell
by default. Export OPENSHELL_BIN before using os_run when the CLI lives
elsewhere — on bootc hosts, for example, it is /usr/bin/openshell.
3. Create and connect the harness¶
Create the selected harness with its dev policy. The profile is part of the
create operation:
harness=opencode # opencode or openclaw
os_run "$repo/openshell/harnesses/$harness/create.sh" \
--profile dev --name "$harness"-dev
Connect from the same owner environment:
OpenCode launches its CLI. OpenClaw opens a shell for inspecting the sandbox. For OpenClaw model tasks, configure Praxis and use the bounded model and tool workflow.
To inspect any harness sandbox with a Bash session, pass bash as the remote
command instead of modifying the harness script:
Use dev for the initial deployment. Each sandbox defaults to two CPUs, 4 GiB
of memory, and 2048 PIDs. See the qualification reference for
profile and harness limitations.
4. Choose model access¶
OpenAI API key¶
The following example uses OpenCode. Run in Bash on the server after OpenShell is ready. Create and review this non-secret profile; a fresh gateway has no pre-imported provider profiles:
cat > /var/tmp/openai-model.yaml <<'YAML'
id: openai-model
display_name: OpenAI model access
category: inference
credentials:
- name: api_key
env_vars: [OPENAI_API_KEY]
required: true
auth_style: bearer
header_name: authorization
endpoints:
- {host: api.openai.com, port: 443, protocol: rest, access: read-write, enforcement: enforce}
binaries: [/usr/local/bin/opencode, /usr/bin/node-26]
YAML
os_run openshell profile lint -f /var/tmp/openai-model.yaml
os_run openshell profile import -f /var/tmp/openai-model.yaml
Register your actual OpenAI API key at the hidden prompt. The key is read inside the service-account process, so it survives the administrator-to-service-account boundary without appearing in command arguments:
os_run bash -c '
cd /
set -eu
set +x
IFS= read -r -s -p "OpenAI API key: " OPENAI_API_KEY; printf "\n"
export OPENAI_API_KEY
openshell provider create --name openai-key --type openai-model \
--credential OPENAI_API_KEY
unset OPENAI_API_KEY
'
Attach the provider when creating a new sandbox, then connect:
os_run "$repo/openshell/harnesses/opencode/create.sh" --profile dev --name opencode-openai --provider openai-key
os_run "$repo/openshell/harnesses/opencode/connect.sh" --name opencode-openai
In OpenCode, use /models to select an OpenAI model available to your account.
The attached provider supplies an opaque OPENAI_API_KEY placeholder; OpenShell
substitutes the real key only for the profile's authorized endpoint. Do not paste
the real key into OpenCode's /connect prompt or store it in harness config.
Follow the model verification steps.
Verify model access on your host. The
real-model deployment qualification is
historical 0.1.3 evidence and does not replace a current-host check.
Local OpenAI-compatible endpoint and key¶
For an existing authenticated local endpoint, use the same import, hidden-prompt, and sandbox creation flow above with these substitutions:
| Setting | Local endpoint example |
|---|---|
Profile file and id |
/var/tmp/local-model.yaml, local-model |
| Profile endpoint | host: inference.internal, port: 443 |
| Provider creation | --name local-key --type local-model --credential OPENAI_API_KEY |
| Prompt value | The key issued by your local inference server, not an OpenAI cloud key |
| Sandbox | --name opencode-local --provider local-key |
| Client base URL | https://inference.internal/v1 |
| Model | The exact model ID served by that endpoint |
See the complete local profile and OpenCode configuration
for copyable examples. Use the endpoint's actual host and port in both the
profile and client configuration. Changing only the base URL does not authorize
credential delivery to a new host. For inference on the OpenShell host, use
host.openshell.internal, rather than sandbox localhost.
A server that enforces bearer authentication needs its real key. A server that
does not authenticate requests needs a credentialless profile; an SDK may still
require a non-empty placeholder such as unused. The placeholder does not grant
access to an authenticated server.
5. Verify the deployment¶
Set sandbox to the name you created, then verify its runtime ceilings from
the host:
sandbox=opencode-openai # use opencode-dev or opencode-local for those examples
cd /
container_id="$(
os_run podman ps --filter name=openshell-default--"$sandbox" -q)"
os_run podman inspect "$container_id" \
--format 'cpus={{.HostConfig.NanoCpus}} memory={{.HostConfig.Memory}} pids={{.HostConfig.PidsLimit}}'
The expected default is cpus=2000000000 (two CPUs), memory=4294967296
(4 GiB), and pids=2048.
Run the controlled policy qualification as the service owner from the reviewed repository checkout:
The test starts a local HTTP fixture, creates temporary allow and deny
sandboxes, proves the allowed request reaches the fixture, and proves the denied
request returns EACCES without reaching the server. A 401 from the fixture
is a successful positive control; an external website error is not.
Follow the harness checks to test workspace writes and, when model access is configured, inference and tool execution.
Optional Praxis routing¶
Install the Praxis gateway and OpenShell add-on,
then follow the OpenCode integration configuration.
The gateway installation is a prerequisite for the add-on. Use an integrated
sandbox without a direct --provider binding. For OpenClaw, follow the
bounded model and tool workflow.
Sandbox operations¶
For a one-off network requirement, add --policy-advisor when creating the
sandbox. After the harness reports a denial and proposal, review and approve it
as the same service owner:
os_run "$repo/openshell/scripts/policy-approve.sh" list HARNESS-dev
os_run "$repo/openshell/scripts/policy-approve.sh" approve HARNESS-dev --chunk-id PROPOSAL-ID
The grant applies only to that sandbox instance and disappears when the sandbox is recreated. Successful approvals are recorded in a local JSONL audit file.
Inspect the sandbox and remove it only after exporting needed work: