This operator aims to ease up the decentralized configuration pattern as described in NATS Docs. To do so, it creates three new CRDs, one for Operator, Account and User objects.
It allows to (nearly) fully customize the issued JWTs, generates a server config and provides a NATS auth server to be run next to a NATS server (cluster).
In order to get started, we install the operator using kustomize, afterwards create a NatsOperator
object.
apiVersion: nats.deinstapel.de/v1alpha1
kind: NatsOperator
metadata:
namespace: nats-cluster
name: root-operator
spec:
signingKeys: [] # Optionally can specify external operator scoped signing keys here.
The operator will start to reconcile the NatsOperator by:
- Creating a KeyPair for the Operator used as Root of Trust.
- Create a NatsAccount and a NatsUser object for the shipped AccountServer
- Create a config file that can be mounted into the NATS server cluster where the Root of Trust and the System account is configured.
In order to enable the configuration, include auth.conf
in your server config file.
Afterwards, you can create NATS Accounts and users within the accounts.
An Account can be created as follows:
apiVersion: nats.deinstapel.de/v1alpha1
kind: NatsAccount
metadata:
namespace: nats-cluster # Must be the same namespace as the operator
name: app-account
spec:
operatorRef:
name: root-operator # Setup the signing key for this account
allowedUserNamespaces:
- app-namespace # Defines the kubernetes namespaces where NatsUser objects for this account will be valid
imports: []
exports: []
limits:
# The default limits are 0 for all items, so a user will not be allowed to connect or subscribe
# Temporary allow unlimited connections, subscriptions and payload sizes.
# TODO User: Adapt this for your app
conn: -1
imports: -1
exports: -1
subs: -1
payload: -1
data: -1
Once you've created an account, it's time to generate a User object. Now here you can also directly inline the permissions.
apiVersion: nats.deinstapel.de/v1alpha1
kind: NatsUser
metadata:
namespace: app-namespace
name: app-backend
spec:
accountRef:
namespace: nats-cluster
name: app-account
limits:
payload: -1
subs: -1
data: -1
permissions:
sub:
allow:
- "app.input.>"
- "app.process.data"
pub:
allow:
- "app.output.>"
resp:
# Allow request/reply
max: 1
ttl: -1
The operator will create:
- A NKey pair
- A JWT signed by the referenced Account
- A user.creds file that can directly be passed into libraries
The user.creds file and the Seed keys will be stored in a secret named like the NatsUser object.
If a user is edited at runtime, the operator will reissue the JWT.
In the future, the operator also will revoke all old JWTs issued for this user.
If you want to use the above manifests with a theoretical NATS helm setup, you can use something like the following values.yaml settings to include the generated manifests:
config:
merge:
# Include the auth.conf our operator wrote to a secret into the server.
00$include: "../custom-auth/auth.conf"
# Patch the containers and statefulSet to include the generated authentication config.
container:
patch:
- op: add
path: "/volumeMounts/-"
value:
name: auth-config
mountPath: "/etc/custom-auth"
statefulSet:
patch:
- op: add
path: /spec/template/spec/volumes/-
value:
name: "auth-config"
secret:
defaultMode: 420
# The secret name is ${operatorCrdName}-server-config
secretName: "root-operator-server-config"
After you've done this, the Operator from your CRD is anchored into the NATS server, with the system-account preloaded in the configuration. Now you can proceed to deploy your account-server, you can use manifests like this:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: nats-account-server
namespace: nats-cluster
rules:
- apiGroups: ["nats.deinstapel.de"]
resources: ["natsaccounts"]
verbs: ["get", "list", "watch"]
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: nats-account-server
namespace: nats-cluster
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: nats-account-server
namespace: nats-cluster
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: nats-account-server
subjects:
- kind: ServiceAccount
name: nats-account-server
namespace: nats-cluster
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nats-account-server
namespace: nats-cluster
spec:
strategy:
type: RollingUpdate
selector:
matchLabels:
app.kubernetes.io/component: nats-account-server
replicas: 1
template:
metadata:
labels:
app.kubernetes.io/component: nats-account-server
spec:
serviceAccountName: nats-account-server
containers:
- name: account-server
image: "ghcr.io/deinstapel/nats-jwt-operator/account-server:edge"
args: ["--metrics-bind-address", ":12003", "--health-probe-bind-address", ":12002"]
env:
- name: "NATS_URL"
value: "nats://${NATS_RELEASE_NAME}-nats-headless.nats-cluster.svc.cluster.local"
- name: "NATS_CREDS_FILE"
value: "/etc/nats/user.creds"
- name: "POD_NAMESPACE"
valueFrom:
fieldRef:
fieldPath: "metadata.namespace"
volumeMounts:
- name: "credentials"
mountPath: "/etc/nats"
readOnly: true
securityContext:
capabilities:
add:
- NET_BIND_SERVICE
drop:
- all
runAsUser: 0
runAsGroup: 0
volumes:
- name: "credentials"
secret:
defaultMode: 420
secretName: "root-operator-jwt"
items:
- key: "user.creds"
path: "user.creds"
mode: 420
This will run a service that's connecting to NATS, watches all K8s NatsAccount resources for the given operator and actively pushes them towards the NATS server, as well as subscribes to the Lookup topic as described here.
If you also want to declaratively manage NATS JetStream resources, the manifests below show a basic example of how to use the generated NATS User JWT in combination with the NACK Account resource to authorize to the NATS server to manage streams.
The first step is to create a jetstream-admin user with this operator in the account in-question:
apiVersion: nats.deinstapel.de/v1alpha1
kind: NatsUser
metadata:
name: app-jetstream-admin
namespace: app-namespace
spec:
# Reference the NatsAccount here to issue the correct JWT
accountRef:
namespace: nats-cluster
name: app-account
limits:
payload: -1
subs: -1
data: -1
permissions:
sub:
# FIXME: NACK doesn't allow us to use a custom _INBOX prefix
allow:
- "_INBOX.>"
- "$JS.>"
pub:
allow:
- "$JS.>"
resp:
max: -1
ttl: -1
After you've created the user, create an Account resource from NACK, telling NACK the Server URLs and the JWT to authorize with:
---
# This one is using the previously generated account to create streams and consumers
apiVersion: jetstream.nats.io/v1beta2
kind: Account
metadata:
name: app-jetstream-admin
namespace: app-namespace
spec:
name: app-jetstream-admin
servers:
- nats://${NATS_RELEASE_NAME}-nats-headless.nats-cluster.svc.cluster.local:4222
creds:
# Pull in the generated user.creds file from the NatsUser we've created above.
secret:
name: app-jetstream-admin
file: "user.creds"
Then you're ready to manage streams and consumers by referencing the Account resource there again:
apiVersion: jetstream.nats.io/v1beta2
kind: Stream
metadata:
name: app-stream
namespace: app-namespace
spec:
name: app-stream
subjects:
- "app.foobar.>"
storage: file
maxAge: 1h
description: "Stores the stream of foobars for app xyz"
replicas: 1
account: app-jetstream-admin
This way, you can declaratively and securely manage not only your users but also the Streams and Consumers
Currently, in NATS NACK the consumer resource does not respect the user.creds file passed in via the account.
A helm chart is provided in deploy/charts
- Install Instances of Custom Resources:
kubectl apply -f config/samples/
- Build and push your image to the location specified by
IMG
:
make docker-build docker-push IMG=<some-registry>/nats-jwt-operator:tag
- Deploy the controller to the cluster with the image specified by
IMG
:
make deploy IMG=<some-registry>/nats-jwt-operator:tag
To delete the CRDs from the cluster:
make uninstall
UnDeploy the controller from the cluster:
make undeploy
PR's welcome!
This project aims to follow the Kubernetes Operator pattern.
It uses Controllers, which provide a reconcile function responsible for synchronizing resources until the desired state is reached on the cluster.
- Install the CRDs into the cluster:
make install
- Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running):
make run
NOTE: You can also run this in one step by running: make install run
If you are editing the API definitions, generate the manifests such as CRs or CRDs using:
make manifests
NOTE: Run make --help
for more information on all potential make
targets
More information can be found via the Kubebuilder Documentation
Copyright 2023.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.