Opt in and policies¶
Opt in¶
A container is mediated only if both hold:
- its namespace matches the agent's
--namespace-selector(defaulthallpass.io/enabled=true), chosen by the cluster admin in the DaemonSet; - its pod has the annotation
hallpass.io/policy: <name>, naming aHallpassPolicy.
Every other container is untouched. If an opted-in pod names a policy that does not exist, its containers fail to start.
In Headlamp, the Sandbox with hallpass pod action sets the annotation on the pod's workload (see Headlamp plugin).
HallpassPolicy¶
apiVersion: hallpass.io/v1alpha1
kind: HallpassPolicy
metadata:
name: agent
spec:
prompt: true # false: deny without asking
promptTimeout: 5m # no answer in time: deny
mediate: [write, exec, connect] # default: write, exec
groups:
"@build": [/usr/bin/make, /usr/bin/cc, /usr/local/go/bin/**]
rules: |
allow write /workspace/**
allow write /tmp/**
deny write /var/run/secrets/**
ask write /workspace/.git/hooks/** # hooks run outside the sandbox
allow exec /usr/bin/make
allow exec /usr/local/go/bin/**
allow exec @shells
allow exec @git
allow exec @build
deny exec /usr/bin/curl
allow connect tcp:*:443
allow connect unix:@my-service
ask connect tcp:10.0.0.1:22
deny connect 169.254.169.254
rules has one rule per line, in the syntax of hallpass answer add
and of rule files: a value with a
blank or a character other than letters, digits and /._-+*~@:=%[] is
quoted ("/workspace/My Documents/**"), # at the start of a word
starts a comment, and group @NAME PROGRAM... defines a group as
groups does (a name is defined once, in either).
Paths are the container's, with no variables ($HOME). The most
specific rule decides, an ask wins over an allow as specific, and a deny
wins over both. A pod naming a policy with an invalid rule fails to
start, as for a missing policy.
write PATH:/workspace/**is/workspaceand everything under it,/workspace/filethat file only. Other globs and relative paths are rejected. Also allowed, without a rule:/dev/nulland the other harmless devices, the terminal (/dev/tty,/dev/pts) and/dev/termination-log.exec PROGRAM: programs in the container, like paths:/usr/bin/makeis that program,/opt/tools/**every program below/opt/tools.allow exec @NAMEallows a group of programs, resolved in the container:@shells,@coreutils,@search,@git,@busybox(see Programs), or one ofgroups; for a busybox image,allow exec @busyboxlets its shell and applets run. They needexecinmediate. Also allowed, by a fixed rule: the container's command (its first argument, looked up in the container's$PATH), with any arguments, any number of times, also throughkubectl exec. Other programs are asked about, and a prompt about a program of a group offers the group. Programs are matched by path only: an allowed interpreter runs any code it is given (python3 -c,bash -c),ld.so /path/to/programruns a program without executing it, a copy of a program under an allowed path runs, busybox applets installed as symlinks are all the same program, abinfmt_mischandler needs an allow in strict mode (it is never asked about), and amemfdis never allowed for good. See Programs for details.connect DESTINATION: destinations in the--allow-connectsyntax (Network);allis every destination, unix sockets included. They needconnectinmediate. Loopback and unix listeners started in the container are allowed without asking. A host name (allow connect ghcr.io:443, or*.githubusercontent.com:443) matches the addresses the container's own DNS answers gave it; the cloud metadata addresses are denied unless a rule names them.http METHOD... URL: HTTPS requests through hallpass-node's proxy (allow http GET https://ghcr.io/**), see HTTPS requests.
Host names, http rules and credentials need the container to drop
NET_RAW (securityContext.capabilities.drop: [NET_RAW]), which
containerd grants by default: otherwise its names are for display only
(Names on Kubernetes),
and with http rules or credentials it is denied port 443.
The other fields:
prompt(defaulttrue): ask about the other writes and programs. Withfalse, they are denied at once, and so are requests made in advance.promptTimeout(default5m): a request with no answer by then is denied (ETIMEDOUTfor a request made in advance).0waits forever, likehallpass --timeout=0.mediate(default[write, exec]): the syscall categories mediated.execasks about the programs the container runs.connect(experimental) asks about connections and sends, likehallpass --mediate(Network). A request made in advance fails at once withENOSYSwhen its category is left out (write,execorconnect, as the request's operation).groups: groups of programs of the policy's own, by name: a new group (@buildabove), or a replacement for the built-in group of the same name. Their programs are paths in the container, as inexecrules, resolved when the container starts.allow exec @NAMEallows them, and a prompt about one of their programs offers the group. Names are@, a lowercase letter, then lowercase letters, digits or-. They needexecinmediate.
The fields rules replaced (allowPaths, denyPaths, askPaths,
allowConnect, denyConnect, askConnect, allowExec, denyExec) are refused:
the API server rejects a policy that has them.
Policy changes apply to containers started afterwards: a running session keeps the policy it started with.
The syscalls mediated are the same as hallpass's: writes, renames,
links, removals... (see Files), plus the programs
run with exec in mediate, connections and sends with connect, and new user and
mount namespaces are refused. The policy is merged into the container's
own seccomp profile (RuntimeDefault, Localhost or Unconfined):
hallpass's rules first, the rest kept.
deploy/kubernetes/sample.yaml has a complete example.
Credentials¶
A credential NAME line keeps a token out of the container: the
container gets a placeholder in the profile's variable (GH_TOKEN for
gh), and hallpass-node's HTTPS proxy puts the real value on the wire,
to the credential's hosts only (see
Credentials). The
value is a key of a Secret in the pod's namespace, never mounted in the
pod:
apiVersion: hallpass.io/v1alpha1
kind: HallpassPolicy
metadata: {name: ci}
spec:
mediate: [write, connect]
rules: |
allow connect github.com:443
allow http GET https://api.github.com/**
credential gh
credentials:
gh: {secretRef: {name: gh-token, key: token}}
hallpass-node reads no Secret by default. The namespace's owner grants it the ones its pods use, by name:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: {name: hallpass-credentials, namespace: ci}
rules:
- apiGroups: [""]
resources: [secrets]
resourceNames: [gh-token]
verbs: [get]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: {name: hallpass-credentials, namespace: ci}
roleRef: {apiGroup: rbac.authorization.k8s.io, kind: Role, name: hallpass-credentials}
subjects:
- {kind: ServiceAccount, name: hallpass-node, namespace: hallpass-system}
Without the grant, or without the Secret, the container is not created,
with the reason in its events. The value stays in hallpass-node's
memory; a restart of the agent before the container starts loses it
(requests then carry the placeholder). A request carrying the
credential to another host is refused and reported as a
HallpassCredentialLeak Warning Event on the pod: a tripwire for the
naive attempt, not the protection (see
Credentials).
A plain name is a built-in profile (gh). config:NAME is the key
NAME.toml of the ConfigMap hallpass-credential-profiles in
hallpass-node's namespace, in the format of
credentials.d (its
source is not used: the value comes from the Secret, and mode must
be mask). Neither falls back to the other: a ConfigMap profile never
shadows a built-in one, and deleting it refuses the containers rather
than sending the value to a built-in profile's hosts. For example, a
GitHub Enterprise server's token, credential config:ghe with
credentials: {"config:ghe": {secretRef: ...}} in the policy:
apiVersion: v1
kind: ConfigMap
metadata: {name: hallpass-credential-profiles, namespace: hallpass-system}
data:
ghe.toml: |
name = "ghe"
hosts = ["github.example.com"]
source = "env:GH_TOKEN"
mode = "mask"
prefix = "ghp_"
chars = "A-Za-z0-9"
length = 36
env = { GH_ENTERPRISE_TOKEN = "{placeholder}", GH_HOST = "github.example.com" }
A policy may name a profile that does not exist yet; the containers of its pods are refused until it does. Only who may edit that ConfigMap decides where a credential's value may go: keep it to the cluster's administrators.