nsplease
is a tiny Kubernetes controller to create and configure
namespaces on-demand.
It's intended to be used in CI/CD pipelines, when a particular component (job, pipeline, run...) needs full access to an individual namespace (for instance, to deploy a staging environment for a given branch, commit, pull request...) but shouldn't have any other access outside of that namespace.
Using nsplease
improves the safety of CI/CD pipelines, because
they don't need full access (cluster-admin
) privileges to a
Kubernetes cluster anymore.
nsplease
uses two namespaces.
nsplease-system
: that's where the operator itself is running.nsplease-requests
: that namespace only holds ConfigMaps and Secrets. The ConfigMaps correspond to "requests" (meaning "I want you to create a new Namespace") and the secrets correspond to "responses" (they hold the credentials to access created namespaces).
This is the general flow of operation.
- User wants to create a new Namespace named
hello
. - User creates a ConfigMap named
hello
in Namespacensplease-requests
. - User waits until a Secret named
hello
shows up in Namespacensplease-requests
. nsplease
sees the ConfigMap namedhello
in Namespacensplease-requests
.nsplease
validates the name of the ConfigMap.nsplease
creates the Namespacehello
.nsplease
creates a ServiceAccount namedadmin
in Namespacehello
.nsplease
grants theadmin
ClusterRole to ServiceAccountadmin
in Namespacehello
.nsplease
can optionally prepopulate Namespacehello
with other resources.nsplease
retrieves the token for ServiceACccountadmin
in Namespacehello
.nsplease
creates a Secret namedhello
in Namespacensplease-requests
, containing the token.- User detects the Secret and obtains the token.
- User can now work in the
hello
namespace by using the token. - When User doesn't need the Namespace anymore, they delete the
hello
Namespace. - The ConfigMap and Secret (in
nsplease-requests
) get deleted automatically.
The repository provides a Kubernetes YAML manifest that will run
nsplease
and grant it the privileges that it needs to create further
Namespaces.
You can install it like this:
kubectl apply -f https://raw.githubusercontent.com/jpetazzo/nsplease/main/nsplease.yaml
You can remove it similarly with kubectl delete -f
.
If you remove nsplease
, the Namespaces that it created won't be removed.
If you want to remove all these namespaces, they are labelled, so you can
easily identify them with a selector. No other object is created.
Let's say that you have a CI job that needs to deploy a staging copy of your application in a dedicated namespace.
- Compute the name of the namespace. Let's say that it'll be
ci-projectfoo-pr123
. For convenient, let's put that in an environment variable for the rest of the example.NAMESPACE=ci-projectfoo-pr123
- Create a ConfigMap named
ci-projectfoo-pr123
in the namespacensplease-requests
.kubectl --namespace nsplease-requests \ create configmap $NAMESPACE
- Wait until the Secret named
ci-projectfoo-pr123
in the namespacensplease-request
exists.kubectl --namespace nsplease-requests \ get secret --field-selector=metadata.name=$NAMESPACE --watch \ | head -n 0
- Obtain the token stored in that Secret.
TOKEN=$(kubectl --namespace nsplease-requests \ get secret $NAMESPACE -o "jsonpath={.data.token}" | base64 -d)
- Use it to access the
ci-projectfoo-pr123
namespace.kubectl --namespace $NAMESPACE --token $TOKEN create configmap hello
The examples above use simple kubectl
commands, but of course
you can use whatever else you'd like instead.
If your system doesn't have base64 -d
you can also use openssl base64 -d
or use jq
to decode the Secret.
The token that you obtained is the token of the ServiceAccount
ci-projectfoo-pr123:admin
. That ServiceAccount has admin
privileges
in the namespace ci-projectfoo-pr123
.
In each Namespace created by nsplease
, there is a ServiceAccount named
admin
. That ServiceAccount has the right to delete the Namespace.
Of course, deleting the Namespace will probably be the last operation
that the ServiceAccount will do, since deleting the Namespace will
eventually delete the ServiceAccount as well.
The ConfigMap and Secret corresponding to the Namespace (in the nsplease-requests
Namespace) get automatically deleted when the Namespace is deleted
(thanks to ownerReferences
).
nsplease
itself requires cluster-admin
privileges. However, we hope
that its code is simple enough to be easily audited and be sure that it
doesn't introduce any vulnerability.
Your CI pipeline only needs to be able to:
- create ConfigMaps in
nsplease-requests
- read Secrets in
nsplease-requests
After creating the ConfigMap, it obtains the token that it should use to interact with the Namespace. That token only lets it interact with the Namespace, and nothing else.
If someone compromises my CI pipeline, what can they do?
They won't get cluster-admin
access to the cluster. However, they will
get access to all the ConfigMaps and Secrets in the nsplease-requests
Namespace. They will therefore be able to list the other Namespaces
created by nsplease
, and obtain the tokens allowing to connect to them.
That sounds bad. What can I do about it?
You can change the TOKEN_POLICY_DEFAULT
environment variable to only-once
,
and after obtaining the token for a given Namespace, delete the corresponding
Secret. An attacker won't be able to obtain the tokens of existing namespaces.
(They will still be able to compromise future Namespaces, however.)
Note: it is not enough to delete the Secret, because if the ConfigMap object
is deleted and recreated, the Secret will be recreated as well. Unless
the admin
ServiceAccount has a special annotation that tells nsplease
"only copy that ServiceAccount's token once". By default, that annotation
is nsplease.container.training/issue-token
and the possible values are
only-once
or multiple-times
.
Cool. Is there anything else I can do?
After obtaining the token for ServiceAccount admin
, you can use that
token to rotate the token itself. This is kind of risky business,
because you need to watch changes on Secrets, then delete the Secret.
If your watch request is interrupted before the new Secret is issued,
you will lose access to the Namespace. But if you do that, there will
only be a tiny window during which an attacker (who would have compromised
your CI pipeline) would be able to get the ServiceAccount token.
If you're dealing with a basic compromise (something that intentionally
or accidentally leaks Secrets, for instance) this reduces the risk;
but it won't prevent a very determined attacker from getting access to
the token and getting the new token as you rotate it.