Assert documented security controls re-checks the claims on every change, so
removing a control from the chart fails the build.
Three companion pages carry the detail:
Pod security
The measured Pod Security Admission level for each namespace.
Network policy
Every rule the shipped policies allow, and what has no rule.
Supply chain
Signature and bill-of-materials verification commands.
Boundaries and defense in depth
The most useful thing to know about a control is whether crossing it requires defeating something other than the component being constrained. Controls in the first list are enforced by the Kubernetes API server, the kubelet or the network fabric, so a compromised Admin Server cannot step around them. Controls in the second list raise the cost of an attack but are enforced by the component itself. Security boundaries
Defense in depth
Identity and RBAC
The chart creates threeServiceAccount objects, one per workload that needs to
reach the Kubernetes API. The Admin UI gets none, and sets
automountServiceAccountToken: false.
What the Admin Server may do cluster-wide
Defined intemplates/admin-server.yaml under adminServer.rbac.create, bound
by a ClusterRoleBinding of the same name. No rule uses a wildcard.
Three of these need a justification.
namespaces: get and create
namespaces: get and create
A
Namespace is cluster-scoped, so neither verb can be confined by a
RoleBinding. create is what lets the console provision a database into a
new namespace without an administrator preparing one first.list is deliberately absent. The console derives the namespaces it shows
from the namespaces that hold a FalkorDB cluster, so the server never
enumerates the cluster’s namespaces and cannot use this grant to discover
what else the cluster runs.rolebindings: create and delete
rolebindings: create and delete
create is how the Admin Server extends its own Secret access into a
namespace as it adds that namespace. delete cleans up the RoleBinding
objects KubeBlocks leaves behind when a ComponentDefinition changes.On its own this pair would be a privilege-escalation path: anything that can
create a RoleBinding can grant itself any role in the cluster. The next
grant is what closes it.clusterroles: bind, restricted by name
clusterroles: bind, restricted by name
Kubernetes requires the
bind verb before a subject may reference a role it
does not already hold. The chart grants bind on exactly one name, the
namespace-scoped ClusterRole generated by
falkordb-enterprise.adminServerNamespacedRoleName.The API server rejects any RoleBinding the Admin Server creates that
references a different role. The create grant above is therefore bounded
to a single, known set of permissions. That set includes creating Jobs,
though, and a Job can run as any ServiceAccount in its namespace: the
admission policy closes that.KubeBlocks definitions: read-only
KubeBlocks definitions: read-only
The Admin Server only reads
ClusterDefinition, ComponentDefinition and
their version objects, which the addon chart installs. Write access would
be a grant of its own: the KubeBlocks operator gives the pods of every
component the policyRules of its ComponentDefinition.What the Admin Server may do per namespace
Secret and ConfigMap access is not in theClusterRole above. It lives in a
second ClusterRole, generated by
falkordb-enterprise.adminServerNamespacedRoleName, which is never bound
cluster-wide:
The chart binds it into the release namespace with a
RoleBinding, where the
Admin Server keeps its settings, identity provider credentials and the pull
secrets it replicates outward. The Admin Server binds it into each database
namespace as it adds that namespace, using the create and bind grants above.
The consequence is the one worth writing down: the Admin Server has no access
to Secret or ConfigMap objects in any namespace it has not been bound
into, apart from the read-only access below. That is enforced by the API
server, not by the product.
One exception: in each namespace listed in
adminServer.rdbTransfer.credentialNamespaces (kb-system and default by
default), a Role grants get on secrets, and nothing else. RDB export and
import read and write backup repositories on object storage directly, and the
Admin Server reads a repository’s credential Secret to give each transfer Job
its storage configuration. The configuration goes into the Job’s own Secret.
Set the list to [] to drop the grant; RDB transfers to repositories whose
credentials it can’t read then fail with a message naming the Secret.
The batch and persistentvolumeclaims grants serve RDB export and import:
the Admin Server runs redis-rdb-cli in Jobs (and in a CronJob for each export schedule)
in the database namespaces, and creates the backup repository’s volume in a
namespace that has not used it yet, as KubeBlocks would. Each Job gets its own
Secret holding the connection details, owned by the Job so it is deleted with
it. The Jobs run as a non-root user on a read-only root filesystem, with no
service account token.
What the admission policy refuses
RBAC decides which objects the Admin Server may write, not what is inside them, and several of the objects it writes decide whichServiceAccount a pod
runs as. Without more, a compromised Admin Server could bind its
namespace-scoped role into another namespace and run a Job there as any
ServiceAccount, the KubeBlocks operator’s included.
The chart therefore installs a ValidatingAdmissionPolicy, defined in
templates/admin-server-admission-policy.yaml under
adminServer.rbac.admissionPolicy.enabled. It applies only to requests made
as the Admin Server’s ServiceAccount, fails closed, and objects to:
Everything the Admin Server does today passes. The policy needs the
admissionregistration.k8s.io/v1 API, served from Kubernetes 1.30; on older
clusters the chart installs without it, and only RBAC applies.
In this release the policy warns and records an audit event rather than
refusing (
adminServer.rbac.admissionPolicy.validationActions: [Warn, Audit]),
so a violation nobody anticipated can’t break an install. The next release
switches the default to [Deny]. To enforce it now, set [Deny].- the API server’s audit log, which records an annotation for each one;
- the
apiserver_validating_admission_policy_check_totalmetric for the policy (named<release>-admin-server). It counts each violation, and each time the policy fails to evaluate; a request that passes adds nothing, so any sample at all needs a look:
- the objects its own code builds are committed as fixtures and evaluated
against the rendered policy with the Kubernetes API server’s own policy
compiler (
tools/admission-policy-check); - a test fails when code that writes a type the policy covers is added without a fixture for what it sends;
- on a live cluster with databases, copies of the
ClusterandBackupPolicyobjects KubeBlocks generated, written as the Admin Server would, must pass; - after the CI jobs that drive real databases through the Admin Server, the metric above must have counted nothing for it, and must count a deliberate violation sent right after, which proves it was tracking the policy.
Job roles
Both jobs are scoped down from the Admin Server’s reach.
The preflight role is cluster-scoped only because the resources it removes are
cluster-scoped. The browser encryption key job is a namespace-scoped
Role,
because everything it touches is in the release namespace.
The KubeBlocks operator
KubeBlocks generates aClusterRole per cluster component and binds it to that
component’s ServiceAccount, so its operator needs ClusterRole write access.
This grant cannot be narrowed with resourceNames: that field has no effect on
create, and the generated names are not known in advance.
Kubernetes’ own escalation guard bounds the risk, in that the operator can only
create a ClusterRole whose rules it already holds. But the operator is broadly
privileged in its own right, so when you review the trust boundary of a cluster
running FalkorDB Enterprise, treat the KubeBlocks operator ServiceAccount as a
cluster-admin-equivalent identity.
Workload hardening
All four workloads the chart owns satisfy therestricted Pod Security Standard.
Each sets the same properties:
emptyDir:
Pod Security Admission
The installer labels namespaces (apply_pod_security_labels in
scripts/install.sh); the chart does not. The strictest level each namespace
can run at was measured, not assumed:
The four workloads the chart owns would each pass
restricted. The release
namespace is held at baseline by a single subchart. See
Pod security for the reported violations per
workload.
Network isolation
Network policies are off by default, because a cluster whose CNI plugin does not implementNetworkPolicy would accept the objects and silently enforce
nothing. Enable them with networkPolicy.enabled=true once you have confirmed
your CNI plugin enforces them.
Two policies are created, one for the Admin Server and one for the Admin UI. Both
set policyTypes: [Ingress, Egress], so anything not listed is denied:
Narrowing
networkPolicy.externalEgress.cidrs to your control plane and identity
provider ranges is the single most valuable change you can make to the shipped
policies. See Network policy for the flows that
deliberately have no rule.
Encryption in transit
TLS is handled in two different places. Each one decides its own protocol versions.
The Admin Server and the Admin UI serve plain HTTP inside the Kubernetes cluster. The chart doesn’t terminate TLS for them. It renders the
tls block you supply into the Ingress, and your controller serves it. The versions your users get are the ones your controller allows:
- ingress-nginx allows TLS 1.2 and TLS 1.3 by default. Change this with the
ssl-protocolskey in its ConfigMap. - OpenShift routes follow the
tlsSecurityProfileof the cluster’sIngressController. The defaultIntermediateprofile requires TLS 1.2 or later. - Cloud load balancers follow the TLS or SSL policy you attach to them. On AWS, GKE and Azure, some older predefined policies still allow TLS 1.0 and TLS 1.1. Choose one that requires TLS 1.2 or later.
Secrets
Set the values that hold credentials through
--set or a values file you keep
out of version control, or point the corresponding existingSecret key at a
Secret you manage yourself.
Registry credentials
global.imageCredentials.create makes the chart create a pull secret in the
release namespace. The Admin Server copies the secrets named in
falkordb-enterprise.imagePullSecretNames into each database namespace, because
a pull secret is only usable in the namespace it lives in.
This replication is the reason the Admin Server holds create on secrets in
the namespace-scoped role rather than read-only access.
Supply chain
Container images and the Helm chart are signed withcosign using a key pair
held by the release pipeline, and images carry a CycloneDX bill of materials as
an attestation. Signing does not use the public transparency log, so
verification needs --insecure-ignore-tlog=true:
adminServer.image.digest and adminUi.image.digest
to remove the registry’s ability to change what a tag points at. See
Supply chain for chart verification, bill-of-materials
verification and how mirroring preserves signatures.
Verification is not enforced at admission time by the chart. To make a signature
a boundary rather than a claim, run an admission controller that verifies it,
such as the Sigstore policy controller or Kyverno.
What CI enforces
scripts/assert-security-controls.py runs against the rendered chart on every
change to it. It fails the build if any of the following stops being true:
- No role the chart owns uses a wildcard in
apiGroups,resourcesorverbs. - The
namespacesgrant is exactlygetandcreate. - The Admin Server
ClusterRolegrants no cluster-widesecretsorconfigmaps. - The
clusterrolesgrant isbindonly, and carries aresourceNamesrestriction. - The namespace-scoped
ClusterRoleis never referenced by aClusterRoleBinding. - The Admin Server
ClusterRolegrants onlyget,listandwatchon KubeBlocks cluster and component definitions and versions. - The Admin Server’s admission policy fails closed, is limited to its
ServiceAccount, covers creates and updates of every type in the table above, and has a binding that denies. - Every workload the chart owns sets
runAsNonRoot,seccompProfile: RuntimeDefault,allowPrivilegeEscalation: false,readOnlyRootFilesystem: trueand drops all capabilities. - The Admin UI does not mount a
ServiceAccounttoken. - Both network policies restrict egress as well as ingress.