Compare commits

..

31 Commits

Author SHA1 Message Date
Massimiliano Giovagnoli
857c338c53 wip
Signed-off-by: Massimiliano Giovagnoli <me@maxgio.it>
2022-08-15 20:51:45 +02:00
Massimiliano Giovagnoli
5a9c25b125 refactor(api/v1beta1/owner_role.go): split cluster role that need to be cluster bound
Signed-off-by: Massimiliano Giovagnoli <me@maxgio.it>
2022-08-13 18:56:35 +02:00
Massimiliano Giovagnoli
3cd7bfe6d4 chore(controllers/tenant): rename tenant clusterrole controller
Signed-off-by: Massimiliano Giovagnoli <me@maxgio.it>
2022-08-13 18:29:09 +02:00
Massimiliano Giovagnoli
ff53cc2f38 feat(controllers/tenant): ensure per-tenant owners roles
add gitops ready cluster roles per tenant owners.

Signed-off-by: Massimiliano Giovagnoli <me@maxgio.it>
2022-08-13 16:10:48 +02:00
Massimiliano Giovagnoli
852ab16323 feat(api/v1beta1/owner_role): bind gitops roles to owners
Signed-off-by: Massimiliano Giovagnoli <me@maxgio.it>
2022-08-13 16:00:04 +02:00
Massimiliano Giovagnoli
9c18471879 feat(tenant/tenant/spec): add initial knob to enable the gitops-ready rbac
Signed-off-by: Massimiliano Giovagnoli <me@maxgio.it>
2022-08-13 15:50:25 +02:00
Dario Tranchitella
3435f5464b chore: adding maintainers list 2022-08-05 11:48:31 +02:00
Dario Tranchitella
f216d0bd8d docs: adding adopters list section 2022-08-02 10:18:04 +02:00
alegrey91
f9e7256746 ci: add gosec pipeline 2022-08-01 09:37:12 +02:00
Adriano Pezzuto
5b46e8eb81 docs(tutorial): tenant backup and restore with velero (#626) 2022-07-31 11:00:29 +02:00
Adriano Pezzuto
dd5ed4575e Clarify tenant owner permissions in documentation (#625)
* docs(tutorial): clarify tenant owner permissions and minor improvements
2022-07-31 09:37:12 +02:00
Adriano Pezzuto
f9554d4cae Document how to implement Pod Security Standard (#624)
* docs(guides): add pod security guide and other minor enhancements
2022-07-30 21:30:14 +02:00
Dario Tranchitella
a36c7545db chore(helm): bumping up chart 2022-07-26 20:41:33 +02:00
Dario Tranchitella
f612ecea0c chore: bumping up to v0.1.2 release 2022-07-26 20:11:03 +02:00
Dario Tranchitella
098a74b565 refactor(capsuleconfiguration): allowing to skip tls reconciler 2022-07-26 17:48:58 +02:00
Dario Tranchitella
5a8a8ae77a feat(helm): support for cert-manager and externally managed tls secret 2022-07-26 17:48:58 +02:00
Dario Tranchitella
a8430f2e72 fix(helm): missing blank space in the notes 2022-07-26 17:48:58 +02:00
Dario Tranchitella
3afc470534 chore(e2e): triggering e2e also for pkg files 2022-07-22 19:29:27 +00:00
Dario Tranchitella
d84f0be76b fix: tenant owners cannot replace protected namesapce labels or annotations 2022-07-22 19:29:27 +00:00
dependabot[bot]
3a174bf755 build(deps): bump moment from 2.29.2 to 2.29.4 in /docs
Bumps [moment](https://github.com/moment/moment) from 2.29.2 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.2...2.29.4)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-22 18:05:18 +00:00
Massimiliano Giovagnoli
90a2e9c742 docs(guides/flux2-capsule-gitops-multitenancy): add missing picture
Signed-off-by: Massimiliano Giovagnoli <me@maxgio.it>
2022-07-22 09:44:20 +00:00
Massimiliano Giovagnoli
a091331070 docs(guides/flux2-capsule-gitops-multitenancy): strip down introductory content
Signed-off-by: Massimiliano Giovagnoli <me@maxgio.it>
2022-07-22 09:44:20 +00:00
Massimiliano Giovagnoli
cb3439bd3d docs(guides/flux2-capsule-gitops-multitenancy): initial commit
Signed-off-by: Massimiliano Giovagnoli <me@maxgio.it>
2022-07-22 09:44:20 +00:00
dependabot[bot]
1fd390b91e build(deps): bump terser from 4.8.0 to 4.8.1 in /docs
Bumps [terser](https://github.com/terser/terser) from 4.8.0 to 4.8.1.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-22 09:44:08 +00:00
Dario Tranchitella
80c83689f5 docs: documenting capsule-proxy metrics 2022-07-20 16:08:11 +00:00
Dario Tranchitella
da3d42801b chore(helm): releasing new helm chart (#605) 2022-07-18 08:49:33 +02:00
Adriano Pezzuto
9643885574 feat(config): move Grafana dashboard as Config Map 2022-07-18 08:42:32 +02:00
bsctl
ac3f2bbdd7 feat(helm): update manifests 2022-07-14 07:08:29 +00:00
bsctl
adb214f7f9 feat(helm): change values description 2022-07-14 07:08:29 +00:00
bsctl
ef26d0e6db feat(helm): remove scale down before uninstall 2022-07-14 07:08:29 +00:00
bsctl
3d6f29fa43 feat(helm): add DaemonSet deploy option 2022-07-14 07:08:29 +00:00
53 changed files with 3855 additions and 2474 deletions

18
.github/maintainers.yaml vendored Normal file
View File

@@ -0,0 +1,18 @@
- name: Adriano Pezzuto
github: https://github.com/bsctl
company: Clastix
projects:
- https://github.com/clastix/capsule
- https://github.com/clastix/capsule-proxy
- name: Dario Tranchitella
github: https://github.com/prometherion
company: Clastix
projects:
- https://github.com/clastix/capsule
- https://github.com/clastix/capsule-proxy
- name: Maksim Fedotov
github: https://github.com/MaxFedotov
company: wargaming.net
projects:
- https://github.com/clastix/capsule
- https://github.com/clastix/capsule-proxy

View File

@@ -7,6 +7,7 @@ on:
- '.github/workflows/e2e.yml'
- 'api/**'
- 'controllers/**'
- 'pkg/**'
- 'e2e/*'
- 'Dockerfile'
- 'go.*'
@@ -18,6 +19,7 @@ on:
- '.github/workflows/e2e.yml'
- 'api/**'
- 'controllers/**'
- 'pkg/**'
- 'e2e/*'
- 'Dockerfile'
- 'go.*'

18
.github/workflows/gosec.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: CI gosec
on:
push:
branches: [ "*" ]
pull_request:
branches: [ "*" ]
jobs:
tests:
runs-on: ubuntu-latest
env:
GO111MODULE: on
steps:
- name: Checkout Source
uses: actions/checkout@v2
- name: Run Gosec Security Scanner
uses: securego/gosec@master
with:
args: ./...

5
ADOPTERS.md Normal file
View File

@@ -0,0 +1,5 @@
# Adopters
This is a list of companies that have adopted Capsule, feel free to open a Pull-Request to get yours listed.
## Adopters list (alphabetically)

View File

@@ -84,10 +84,18 @@ bash scripts/helm-docs.sh
Join the community, share and learn from it. You can find all the resources to how to contribute code and docs, connect with people in the [community repository](https://github.com/clastix/capsule-community).
## Adopters
See the [ADOPTERS.md](ADOPTERS.md) file for a list of companies that are using Capsule.
# Governance
You can find how the Capsule project is governed [here](https://capsule.clastix.io/docs/contributing/governance).
## Maintainers
Please, refer to the maintainers file available [here](.github/maintainers.yaml).
# FAQ
- Q. How to pronounce Capsule?

View File

@@ -8,5 +8,5 @@ const (
TLSSecretNameAnnotation = "capsule.clastix.io/tls-secret-name"
MutatingWebhookConfigurationName = "capsule.clastix.io/mutating-webhook-configuration-name"
ValidatingWebhookConfigurationName = "capsule.clastix.io/validating-webhook-configuration-name"
GenerateCertificatesAnnotationName = "capsule.clastix.io/generate-certificates"
EnableTLSConfigurationAnnotationName = "capsule.clastix.io/enable-tls-configuration"
)

View File

@@ -38,7 +38,33 @@ func (in OwnerSpec) GetRoles(tenant Tenant, index int) []string {
}
}
return []string{"admin", "capsule-namespace-deleter"}
roles := []string{"admin", "capsule-namespace-deleter"}
if tenant.Spec.GitOpsReady {
roles = append(roles, in.getGitOpsRoles(tenant)...)
}
return roles
}
func (in OwnerSpec) GetClusterRoles(tenant Tenant) []string {
if tenant.Spec.GitOpsReady {
return in.getGitOpsClusterRoles(tenant)
}
return []string{}
}
func (in OwnerSpec) getGitOpsClusterRoles(tenant Tenant) []string {
return []string{
"capsule-tenant-impersonator-" + tenant.Name + "-" + in.Name,
}
}
func (in OwnerSpec) getGitOpsRoles(tenant Tenant) []string {
return []string{
"cluster-admin",
}
}
func (in OwnerSpec) convertMap() map[string]string {

View File

@@ -24,6 +24,8 @@ func GetTypeLabel(t runtime.Object) (label string, err error) {
return "capsule.clastix.io/resource-quota", nil
case *rbacv1.RoleBinding:
return "capsule.clastix.io/role-binding", nil
case *rbacv1.ClusterRoleBinding:
return "capsule.clastix.io/cluster-role-binding", nil
default:
err = fmt.Errorf("type %T is not mapped as Capsule label recognized", v)
}

View File

@@ -35,6 +35,8 @@ type TenantSpec struct {
ImagePullPolicies []ImagePullPolicySpec `json:"imagePullPolicies,omitempty"`
// Specifies the allowed priorityClasses assigned to the Tenant. Capsule assures that all Pods resources created in the Tenant can use only one of the allowed PriorityClasses. Optional.
PriorityClasses *AllowedListSpec `json:"priorityClasses,omitempty"`
// Configured RBAC for machine owners tailored for GitOps controllers.
GitOpsReady bool `json:"gitOpsReady,omitempty"`
}
//+kubebuilder:object:root=true

View File

@@ -21,8 +21,8 @@ sources:
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
version: 0.1.9
version: 0.1.11
# This is the version number of the application being deployed.
# This version number should be incremented each time you make changes to the application.
appVersion: 0.1.1
appVersion: 0.1.2

9
charts/capsule/Makefile Normal file
View File

@@ -0,0 +1,9 @@
docs: HELMDOCS_VERSION := v1.8.1
docs: docker
@docker run --rm -v "$$(pwd):/helm-docs" -u $$(id -u) jnorwood/helm-docs:$(HELMDOCS_VERSION)
docker:
@hash docker 2>/dev/null || {\
echo "You need docker" &&\
exit 1;\
}

View File

@@ -78,6 +78,9 @@ Here the values you can override:
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account. |
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created. |
| serviceAccount.name | string | `"capsule"` | The name of the service account to use. If not set and `serviceAccount.create=true`, a name is generated using the fullname template |
| tls.create | bool | `true` | When cert-manager is disabled, Capsule will generate the TLS certificate for webhook and CRDs conversion. |
| tls.enableController | bool | `true` | Start the Capsule controller that injects the CA into mutating and validating webhooks, and CRD as well. |
| tls.name | string | `""` | Override name of the Capsule TLS Secret name when externally managed. |
| tolerations | list | `[]` | Set list of tolerations for the Capsule pod |
| validatingWebhooksTimeoutSeconds | int | `30` | Timeout in seconds for validating webhooks |
@@ -90,6 +93,7 @@ Here the values you can override:
| manager.image.repository | string | `"clastix/capsule"` | Set the image repository of the capsule. |
| manager.image.tag | string | `""` | Overrides the image tag whose default is the chart appVersion. |
| manager.imagePullSecrets | list | `[]` | Configuration for `imagePullSecrets` so that you can use a private images registry. |
| manager.kind | string | `"Deployment"` | Set the controller deployment mode as `Deployment` or `DaemonSet`. |
| manager.livenessProbe | object | `{"httpGet":{"path":"/healthz","port":10080}}` | Configure the liveness probe using Deployment probe spec |
| manager.options.capsuleUserGroups | list | `["capsule.clastix.io"]` | Override the Capsule user groups |
| manager.options.forceTenantPrefix | bool | `false` | Boolean, enforces the Tenant owner, during Namespace creation, to name it using the selected Tenant name as prefix, separated by a dash |
@@ -174,6 +178,34 @@ And optionally, depending on the values set:
Capsule, as many other add-ons, defines its own set of Custom Resource Definitions (CRDs). Helm3 removed the old CRDs installation method for a more simple methodology. In the Helm Chart, there is now a special directory called `crds` to hold the CRDs. These CRDs are not templated, but will be installed by default when running a `helm install` for the chart. If the CRDs already exist (for example, you already executed `helm install`), it will be skipped with a warning. When you wish to skip the CRDs installation, and do not see the warning, you can pass the `--skip-crds` flag to the `helm install` command.
## Cert-Manager integration
You can enable the generation of certificates using `cert-manager` as follows.
```
helm upgrade --install capsule clastix/capsule --namespace capsule-system --create-namespace \
--set "certManager.generateCertificates=true" \
--set "tls.create=false" \
--set "tls.enableController=false"
```
With the usage of `tls.enableController=false` value, you're delegating the injection of the Validating and Mutating Webhooks' CA to `cert-manager`.
Since Helm3 doesn't allow to template _CRDs_, you have to patch manually the Custom Resource Definition `tenants.capsule.clastix.io` adding the proper annotation (YMMV).
```yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.5.0
cert-manager.io/inject-ca-from: capsule-system/capsule-webhook-cert
creationTimestamp: "2022-07-22T08:32:51Z"
generation: 45
name: tenants.capsule.clastix.io
resourceVersion: "9832"
uid: 61e287df-319b-476d-88d5-bdb8dc14d4a6
```
## More
See Capsule [tutorial](https://github.com/clastix/capsule/blob/master/docs/content/general/tutorial.md) for more information about how to use Capsule.

View File

@@ -127,6 +127,34 @@ And optionally, depending on the values set:
Capsule, as many other add-ons, defines its own set of Custom Resource Definitions (CRDs). Helm3 removed the old CRDs installation method for a more simple methodology. In the Helm Chart, there is now a special directory called `crds` to hold the CRDs. These CRDs are not templated, but will be installed by default when running a `helm install` for the chart. If the CRDs already exist (for example, you already executed `helm install`), it will be skipped with a warning. When you wish to skip the CRDs installation, and do not see the warning, you can pass the `--skip-crds` flag to the `helm install` command.
## Cert-Manager integration
You can enable the generation of certificates using `cert-manager` as follows.
```
helm upgrade --install capsule clastix/capsule --namespace capsule-system --create-namespace \
--set "certManager.generateCertificates=true" \
--set "tls.create=false" \
--set "tls.enableController=false"
```
With the usage of `tls.enableController=false` value, you're delegating the injection of the Validating and Mutating Webhooks' CA to `cert-manager`.
Since Helm3 doesn't allow to template _CRDs_, you have to patch manually the Custom Resource Definition `tenants.capsule.clastix.io` adding the proper annotation (YMMV).
```yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.5.0
cert-manager.io/inject-ca-from: capsule-system/capsule-webhook-cert
creationTimestamp: "2022-07-22T08:32:51Z"
generation: 45
name: tenants.capsule.clastix.io
resourceVersion: "9832"
uid: 61e287df-319b-476d-88d5-bdb8dc14d4a6
```
## More
See Capsule [tutorial](https://github.com/clastix/capsule/blob/master/docs/content/general/tutorial.md) for more information about how to use Capsule.

View File

@@ -5,7 +5,7 @@
# Check the capsule logs
$ kubectl logs -f deployment/{{ template "capsule.fullname" . }}-controller-manager -c manager -n{{ .Release.Namespace }}
$ kubectl logs -f deployment/{{ template "capsule.fullname" . }}-controller-manager -c manager -n {{ .Release.Namespace }}
- Manage this chart:

View File

@@ -113,9 +113,9 @@ Create the jobs fully-qualified Docker image to use
{{- end }}
{{/*
Create the Capsule Deployment name to use
Create the Capsule controller name to use
*/}}
{{- define "capsule.deploymentName" -}}
{{- define "capsule.controllerName" -}}
{{- printf "%s-controller-manager" (include "capsule.fullname" .) -}}
{{- end }}
@@ -123,5 +123,5 @@ Create the Capsule Deployment name to use
Create the Capsule TLS Secret name to use
*/}}
{{- define "capsule.secretTlsName" -}}
{{- printf "%s-tls" (include "capsule.fullname" .) -}}
{{ default ( printf "%s-tls" ( include "capsule.fullname" . ) ) .Values.tls.name }}
{{- end }}

View File

@@ -1,3 +1,4 @@
{{- if or (not .Values.certManager.generateCertificates) (.Values.tls.create) }}
apiVersion: v1
kind: Secret
metadata:
@@ -8,3 +9,4 @@ metadata:
{{- toYaml . | nindent 4 }}
{{- end }}
name: {{ include "capsule.secretTlsName" . }}
{{- end }}

View File

@@ -8,7 +8,7 @@ metadata:
capsule.clastix.io/mutating-webhook-configuration-name: {{ include "capsule.fullname" . }}-mutating-webhook-configuration
capsule.clastix.io/tls-secret-name: {{ include "capsule.secretTlsName" . }}
capsule.clastix.io/validating-webhook-configuration-name: {{ include "capsule.fullname" . }}-validating-webhook-configuration
capsule.clastix.io/generate-certificates: "{{ .Values.manager.options.generateCertificates }}"
capsule.clastix.io/enable-tls-configuration: "{{ .Values.tls.enableController }}"
{{- with .Values.customAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}

View File

@@ -0,0 +1,88 @@
{{- if eq .Values.manager.kind "DaemonSet" }}
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: {{ include "capsule.controllerName" . }}
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.customAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
updateStrategy:
type: RollingUpdate
selector:
matchLabels:
{{- include "capsule.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "capsule.labels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "capsule.serviceAccountName" . }}
{{- if .Values.manager.hostNetwork }}
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
{{- end }}
priorityClassName: {{ .Values.priorityClassName }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
- name: cert
secret:
defaultMode: 420
secretName: {{ include "capsule.secretTlsName" . }}
containers:
- name: manager
command:
- /manager
args:
- --enable-leader-election
- --zap-log-level={{ default 4 .Values.manager.options.logLevel }}
- --configuration-name=default
image: {{ include "capsule.managerFullyQualifiedDockerImage" . }}
imagePullPolicy: {{ .Values.manager.image.pullPolicy }}
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
ports:
- name: webhook-server
containerPort: 9443
protocol: TCP
- name: metrics
containerPort: 8080
protocol: TCP
livenessProbe:
{{- toYaml .Values.manager.livenessProbe | nindent 12}}
readinessProbe:
{{- toYaml .Values.manager.readinessProbe | nindent 12}}
volumeMounts:
- mountPath: /tmp/k8s-webhook-server/serving-certs
name: cert
readOnly: true
resources:
{{- toYaml .Values.manager.resources | nindent 12 }}
securityContext:
allowPrivilegeEscalation: false
{{- end }}

View File

@@ -1,7 +1,8 @@
{{- if eq .Values.manager.kind "Deployment" }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "capsule.deploymentName" . }}
name: {{ include "capsule.controllerName" . }}
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.customAnnotations }}
@@ -29,6 +30,7 @@ spec:
serviceAccountName: {{ include "capsule.serviceAccountName" . }}
{{- if .Values.manager.hostNetwork }}
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
{{- end }}
priorityClassName: {{ .Values.priorityClassName }}
{{- with .Values.nodeSelector }}
@@ -82,3 +84,4 @@ spec:
{{- toYaml .Values.manager.resources | nindent 12 }}
securityContext:
allowPrivilegeEscalation: false
{{- end }}

View File

@@ -4,8 +4,11 @@ metadata:
name: {{ include "capsule.fullname" . }}-mutating-webhook-configuration
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.customAnnotations }}
annotations:
{{- if .Values.certManager.generateCertificates }}
cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "capsule.fullname" . }}-webhook-cert
{{- end }}
{{- with .Values.customAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
webhooks:

View File

@@ -1,3 +1,4 @@
{{- if .Values.tls.create }}
{{- $cmd := printf "while [ -z $$(kubectl -n $NAMESPACE get secret %s -o jsonpath='{.data.tls\\\\.crt}') ];" (include "capsule.secretTlsName" .) -}}
{{- $cmd = printf "%s do echo 'waiting Capsule to be up and running...' && sleep 5;" $cmd -}}
{{- $cmd = printf "%s done" $cmd -}}
@@ -44,4 +45,5 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
serviceAccountName: {{ include "capsule.serviceAccountName" . }}
serviceAccountName: {{ include "capsule.serviceAccountName" . }}
{{- end }}

View File

@@ -1,5 +1,5 @@
{{- $cmd := printf "kubectl scale deployment -n $NAMESPACE %s --replicas 0 &&" (include "capsule.deploymentName" .) -}}
{{- if not .Values.certManager.generateCertificates }}
{{- $cmd := ""}}
{{- if or (.Values.tls.create) (.Values.certManager.generateCertificates) }}
{{- $cmd = printf "%s kubectl delete secret -n $NAMESPACE %s --ignore-not-found &&" $cmd (include "capsule.secretTlsName" .) -}}
{{- end }}
{{- $cmd = printf "%s kubectl delete clusterroles.rbac.authorization.k8s.io capsule-namespace-deleter capsule-namespace-provisioner --ignore-not-found &&" $cmd -}}

View File

@@ -4,8 +4,11 @@ metadata:
name: {{ include "capsule.fullname" . }}-validating-webhook-configuration
labels:
{{- include "capsule.labels" . | nindent 4 }}
{{- with .Values.customAnnotations }}
annotations:
{{- if .Values.certManager.generateCertificates }}
cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "capsule.fullname" . }}-webhook-cert
{{- end }}
{{- with .Values.customAnnotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
webhooks:

View File

@@ -2,9 +2,21 @@
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# Secret Options
tls:
# -- Start the Capsule controller that injects the CA into mutating and validating webhooks, and CRD as well.
enableController: true
# -- When cert-manager is disabled, Capsule will generate the TLS certificate for webhook and CRDs conversion.
create: true
# -- Override name of the Capsule TLS Secret name when externally managed.
name: ""
# Manager Options
manager:
# -- Set the controller deployment mode as `Deployment` or `DaemonSet`.
kind: Deployment
image:
# -- Set the image repository of the capsule.
repository: clastix/capsule
@@ -193,4 +205,4 @@ serviceMonitor:
# -- Set metricRelabelings for the endpoint of the serviceMonitor
metricRelabelings: []
# -- Set relabelings for the endpoint of the serviceMonitor
relabelings: []
relabelings: []

View File

@@ -654,6 +654,9 @@ spec:
allowedRegex:
type: string
type: object
gitOpsReady:
description: Configured RBAC for machine owners tailored for GitOps controllers.
type: boolean
imagePullPolicies:
description: Specify the allowed values for the imagePullPolicies option in Pod resources. Capsule assures that all Pod resources created in the Tenant can use only one of the allowed policy. Optional.
items:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
labels:
# label selector used by Grafana to load the dashboards from Config Maps
grafana_dashboard: "1"
name: capsule-grafana-dashboard

View File

@@ -0,0 +1,8 @@
configMapGenerator:
- name: capsule-grafana-dashboard
files:
- dashboard.json
generatorOptions:
disableNameSuffixHash: true
patchesStrategicMerge:
- dashboard.yaml

View File

@@ -1411,7 +1411,7 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: clastix/capsule:v0.1.2-rc0
image: clastix/capsule:v0.1.2
imagePullPolicy: IfNotPresent
name: manager
ports:

View File

@@ -7,4 +7,4 @@ kind: Kustomization
images:
- name: controller
newName: clastix/capsule
newTag: v0.1.2-rc0
newTag: v0.1.2

View File

@@ -0,0 +1,119 @@
package tenant
import (
"context"
"fmt"
"hash/fnv"
"golang.org/x/sync/errgroup"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
// Sync the dynamic Tenant Owner specific cluster-roles and additional ClusterRole Bindings, which can be used in many ways:
// applying Pod Security Policies or giving access to CRDs or specific API groups.
func (r *Manager) syncClusterRoleBindings(ctx context.Context, tenant *capsulev1beta1.Tenant) (err error) {
// hashing the ClusterRoleBinding name due to DNS RFC-1123 applied to Kubernetes labels
hashFn := func(binding capsulev1beta1.AdditionalRoleBindingsSpec) string {
h := fnv.New64a()
_, _ = h.Write([]byte(binding.ClusterRoleName))
for _, sub := range binding.Subjects {
_, _ = h.Write([]byte(sub.Kind + sub.Name))
}
return fmt.Sprintf("%x", h.Sum64())
}
// getting requested Role Binding keys
keys := make([]string, 0, len(tenant.Spec.Owners))
// Generating for dynamic tenant owners cluster roles
for _, owner := range tenant.Spec.Owners {
for _, clusterRoleName := range owner.GetClusterRoles(*tenant) {
cr := r.ownerClusterRoleBindings(owner, clusterRoleName)
keys = append(keys, hashFn(cr))
}
}
group := new(errgroup.Group)
group.Go(func() error {
return r.syncClusterRoleBinding(ctx, tenant, keys, hashFn)
})
return group.Wait()
}
func (r *Manager) syncClusterRoleBinding(ctx context.Context, tenant *capsulev1beta1.Tenant, keys []string, hashFn func(binding capsulev1beta1.AdditionalRoleBindingsSpec) string) (err error) {
var tenantLabel string
var clusterRoleBindingLabel string
if tenantLabel, err = capsulev1beta1.GetTypeLabel(&capsulev1beta1.Tenant{}); err != nil {
return
}
if clusterRoleBindingLabel, err = capsulev1beta1.GetTypeLabel(&rbacv1.ClusterRoleBinding{}); err != nil {
return
}
if err = r.pruningClusterResources(ctx, keys, &rbacv1.ClusterRoleBinding{}); err != nil {
return
}
var clusterRoleBindings []capsulev1beta1.AdditionalRoleBindingsSpec
for _, owner := range tenant.Spec.Owners {
for _, clusterRoleName := range owner.GetClusterRoles(*tenant) {
clusterRoleBindings = append(clusterRoleBindings, r.ownerClusterRoleBindings(owner, clusterRoleName))
}
}
for i, clusterRoleBinding := range clusterRoleBindings {
clusterRoleBindingHashLabel := hashFn(clusterRoleBinding)
target := &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("capsule-%s-%d-%s", tenant.Name, i, clusterRoleBinding.ClusterRoleName),
},
}
var res controllerutil.OperationResult
res, err = controllerutil.CreateOrUpdate(ctx, r.Client, target, func() error {
target.ObjectMeta.Labels = map[string]string{
tenantLabel: tenant.Name,
clusterRoleBindingLabel: clusterRoleBindingHashLabel,
}
target.RoleRef = rbacv1.RoleRef{
APIGroup: rbacv1.GroupName,
Kind: "ClusterRole",
Name: clusterRoleBinding.ClusterRoleName,
}
target.Subjects = clusterRoleBinding.Subjects
return controllerutil.SetControllerReference(tenant, target, r.Client.Scheme())
})
// TODO: find appropriate event Namespace.
r.emitEvent(tenant, target.GetNamespace(), res, fmt.Sprintf("Ensuring ClusterRoleBinding %s", target.GetName()), err)
if err != nil {
r.Log.Error(err, "Cannot sync ClusterRoleBinding")
}
r.Log.Info(fmt.Sprintf("ClusterRoleBinding sync result: %s", string(res)), "name", target.Name, "namespace", target.Namespace)
if err != nil {
return
}
}
return nil
}

View File

@@ -0,0 +1,66 @@
package tenant
import (
"context"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
const (
ImpersonatorRoleName = "capsule-tenant-impersonator"
)
// Sync the Tenant Owner specific cluster-roles.
// When the Tenant is configured GitOpsReady additional (Cluster)Roles are created, then bound.
func (r *Manager) syncClusterRoles(ctx context.Context, tenant *capsulev1beta1.Tenant) (err error) {
// If the Tenant will be reconciled the GitOps-way,
// Tenant Owners might be machine GitOps reconciler identities.
if tenant.Spec.GitOpsReady {
for _, owner := range tenant.Spec.Owners {
if err = r.ensureOwnerClusterRole(ctx, tenant, &owner, ImpersonatorRoleName); err != nil {
r.Log.Error(err, "Reconciliation for ClusterRole failed", "ClusterRole", ImpersonatorRoleName)
return err
}
}
}
return
}
func (r *Manager) ensureOwnerClusterRole(ctx context.Context, tenant *capsulev1beta1.Tenant, owner *capsulev1beta1.OwnerSpec, roleName string) (err error) {
switch roleName {
case ImpersonatorRoleName:
clusterRole := &rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{
Name: roleName + "-" + tenant.Name + "-" + owner.Name,
},
}
resource := "users"
if owner.Kind == capsulev1beta1.GroupOwner {
resource = "groups"
}
resourceName := owner.Name
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, clusterRole, func() error {
clusterRole.Rules = []rbacv1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{resource},
Verbs: []string{"impersonate"},
ResourceNames: []string{resourceName},
},
}
return nil
})
}
return
}

View File

@@ -105,6 +105,22 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct
return
}
// Ensuring ClusterRoles resources
r.Log.Info("Ensuring ClusterRoles for Owners and Tenant")
if err = r.syncClusterRoles(ctx, instance); err != nil {
r.Log.Error(err, "Cannot sync ClusterRoles items")
return
}
// Ensuring ClusterRoleBindings resources
r.Log.Info("Ensuring ClusterRoleBindings for Owners and Tenant")
if err = r.syncClusterRoleBindings(ctx, instance); err != nil {
r.Log.Error(err, "Cannot sync ClusterRoleBindings items")
return
}
// Ensuring RoleBinding resources
r.Log.Info("Ensuring RoleBindings for Owners and Tenant")

View File

@@ -14,6 +14,47 @@ import (
capsulev1beta1 "github.com/clastix/capsule/api/v1beta1"
)
// pruningClusterResources is taking care of removing the no more requested sub-resources as LimitRange, ResourceQuota or
// NetworkPolicy using the "exists" and "notin" LabelSelector to perform an outer-join removal.
func (r *Manager) pruningClusterResources(ctx context.Context, keys []string, obj client.Object) (err error) {
var capsuleLabel string
if capsuleLabel, err = capsulev1beta1.GetTypeLabel(obj); err != nil {
return
}
selector := labels.NewSelector()
var exists *labels.Requirement
if exists, err = labels.NewRequirement(capsuleLabel, selection.Exists, []string{}); err != nil {
return
}
selector = selector.Add(*exists)
if len(keys) > 0 {
var notIn *labels.Requirement
if notIn, err = labels.NewRequirement(capsuleLabel, selection.NotIn, keys); err != nil {
return err
}
selector = selector.Add(*notIn)
}
r.Log.Info("Pruning objects with label selector " + selector.String())
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
return r.DeleteAllOf(ctx, obj, &client.DeleteAllOfOptions{
ListOptions: client.ListOptions{
LabelSelector: selector,
},
DeleteOptions: client.DeleteOptions{},
})
})
}
// pruningResources is taking care of removing the no more requested sub-resources as LimitRange, ResourceQuota or
// NetworkPolicy using the "exists" and "notin" LabelSelector to perform an outer-join removal.
func (r *Manager) pruningResources(ctx context.Context, ns string, keys []string, obj client.Object) (err error) {

View File

@@ -177,31 +177,21 @@ func (r Reconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.R
return reconcile.Result{}, err
}
if r.Configuration.GenerateCertificates() {
certificate, err := cert.GetCertificateFromBytes(certSecret.Data[corev1.TLSCertKey])
if err != nil {
return reconcile.Result{}, err
}
now := time.Now()
requeueTime := certificate.NotAfter.Add(-(certificateExpirationThreshold - 1*time.Second))
rq := requeueTime.Sub(now)
r.Log.Info("Reconciliation completed, processing back in " + rq.String())
return reconcile.Result{Requeue: true, RequeueAfter: rq}, nil
certificate, err := cert.GetCertificateFromBytes(certSecret.Data[corev1.TLSCertKey])
if err != nil {
return reconcile.Result{}, err
}
return reconcile.Result{}, nil
now := time.Now()
requeueTime := certificate.NotAfter.Add(-(certificateExpirationThreshold - 1*time.Second))
rq := requeueTime.Sub(now)
r.Log.Info("Reconciliation completed, processing back in " + rq.String())
return reconcile.Result{Requeue: true, RequeueAfter: rq}, nil
}
func (r Reconciler) shouldUpdateCertificate(secret *corev1.Secret) bool {
if !r.Configuration.GenerateCertificates() {
r.Log.Info("Skipping TLS certificate generation as it is disabled in CapsuleConfiguration")
return false
}
if _, ok := secret.Data[corev1.ServiceAccountRootCAKey]; !ok {
return true
}

View File

@@ -17,6 +17,8 @@ In the context of Capsule project, we consider the following roles:
The release process will be governed by Maintainers.
Please, refer to the [maintainers file](https://github.com/clastix/capsule/blob/master/.github/blob/master/maintainers.yaml) available in the source code.
## Roadmap Planning
Maintainers will share roadmap and release versions as milestones in GitHub.

View File

@@ -452,6 +452,45 @@ $ curl -H "Authorization: Bearer $TOKEN" http://localhost:9001/api/v1/namespaces
> NOTE: `kubectl` will not work against a `http` server.
## Metrics
Starting from the v0.3.0 release, Capsule Proxy exposes Prometheus metrics available at `http://0.0.0.0:8080/metrics`.
The offered metrics are related to the internal `controller-manager` code base, such as work-queue and REST client requests, and the Go runtime ones.
Along with these, metrics `capsule_proxy_response_time_seconds` and `capsule_proxy_requests_total` have been introduced and are specific to the Capsule Proxy code-base and functionalities.
`capsule_proxy_response_time_seconds` offers a bucket representation of the HTTP request duration.
The available variables for this metrics are the following ones:
- `path`: the HTTP path of each single request that Capsule Proxy passes to the upstream
`capsule_proxy_requests_total` counts the global requests that Capsule Proxy is passing to the upstream with the following labels.
- `path`: the HTTP path of each single request that Capsule Proxy passes to the upstream
- `status`: the HTTP status code of the request
> Example output of the metrics:
> ```
> # HELP capsule_proxy_requests_total Number of requests
> # TYPE capsule_proxy_requests_total counter
> capsule_proxy_requests_total{path="/api/v1/namespaces",status="403"} 1
> # HELP capsule_proxy_response_time_seconds Duration of capsule proxy requests.
> # TYPE capsule_proxy_response_time_seconds histogram
> capsule_proxy_response_time_seconds_bucket{path="/api/v1/namespaces",le="0.005"} 0
> capsule_proxy_response_time_seconds_bucket{path="/api/v1/namespaces",le="0.01"} 0
> capsule_proxy_response_time_seconds_bucket{path="/api/v1/namespaces",le="0.025"} 0
> capsule_proxy_response_time_seconds_bucket{path="/api/v1/namespaces",le="0.05"} 0
> capsule_proxy_response_time_seconds_bucket{path="/api/v1/namespaces",le="0.1"} 0
> capsule_proxy_response_time_seconds_bucket{path="/api/v1/namespaces",le="0.25"} 0
> capsule_proxy_response_time_seconds_bucket{path="/api/v1/namespaces",le="0.5"} 0
> capsule_proxy_response_time_seconds_bucket{path="/api/v1/namespaces",le="1"} 0
> capsule_proxy_response_time_seconds_bucket{path="/api/v1/namespaces",le="2.5"} 1
> capsule_proxy_response_time_seconds_bucket{path="/api/v1/namespaces",le="5"} 1
> capsule_proxy_response_time_seconds_bucket{path="/api/v1/namespaces",le="10"} 1
> capsule_proxy_response_time_seconds_bucket{path="/api/v1/namespaces",le="+Inf"} 1
> capsule_proxy_response_time_seconds_sum{path="/api/v1/namespaces"} 2.206192787
> capsule_proxy_response_time_seconds_count{path="/api/v1/namespaces"} 1
> ```
## Contributing
`capsule-proxy` is an open-source software released with Apache2 [license](https://github.com/clastix/capsule-proxy/blob/master/LICENSE).

View File

@@ -2,108 +2,21 @@
Capsule is a framework to implement multi-tenant and policy-driven scenarios in Kubernetes. In this tutorial, we'll focus on a hypothetical case covering the main features of the Capsule Operator.
***Acme Corp***, our sample organization, is building a Container as a Service platform (CaaS) to serve multiple lines of business. Each line of business has its team of engineers that are responsible for the development, deployment, and operating of their digital products. We'll work with the following actors:
***Acme Corp***, our sample organization, is building a Container as a Service platform (CaaS) to serve multiple lines of business, or departments, e.g. _Oil_, _Gas_, _Solar_, _Wind_, _Water_. Each department has its team of engineers that are responsible for the development, deployment, and operating of their digital products. We'll work with the following actors:
* ***Bill***: the cluster administrator from the operations department of Acme Corp.
* ***Bill***: the cluster administrator from the operations department of _Acme Corp_.
* ***Alice***: the IT Project Leader in the Oil & Gas Business Units. She is responsible for a team made of different job responsibilities (developers, administrators, SRE engineers, etc.) working in separate multiple departments.
* ***Alice***: the project leader in the _Oil_ & _Gas_ departments. She is responsible for a team made of different job responsibilities: e.g. developers, administrators, SRE engineers, etc.
* ***Joe***:
He works at Acme Corp, as a lead developer of a distributed team in Alice's organization.
* ***Joe***: works as a lead developer of a distributed team in Alice's organization.
* ***Bob***:
He is the head of Engineering for the Water Business Unit, the main and historical line of business at Acme Corp.
* ***Bob***: is the head of engineering for the _Water_ department, the main and historical line of business at _Acme Corp_.
## Assign Tenant ownership
### Roles assigned to Tenant Owners
By default, all Tenant Owners will be granted with two ClusterRole resources using the RoleBinding API:
1. the Kubernetes default one, `admin`, that grants most of the Namespace scoped resources management operations
2. a custom one, named `capsule-namespace-deleter`, allowing to delete the created Namespace
In the example below, assuming Alice create a namespace `oil-production` in Tenant `oil`,getting the tenant owner's Alice default
ClusterRoles command:
```
$: kubectl get rolebindings.rbac.authorization.k8s.io -n oil-production
NAME ROLE AGE
capsule-oil-0-admin ClusterRole/admin 6s
capsule-oil-1-capsule-namespace-deleter ClusterRole/capsule-namespace-deleter 5s
capsule-oil-2-admin ClusterRole/admin 5s
capsule-oil-3-capsule-namespace-deleter ClusterRole/capsule-namespace-deleter 5s
```
Capsule supports the dynamic management of the assigned ClusterRole resources for each Tenant Owner.
```yaml
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
annotations:
clusterrolenames.capsule.clastix.io/user.alice: editor,manager
clusterrolenames.capsule.clastix.io/group.sre: readonly
name: oil
spec:
owners:
- kind: User
name: alice
- kind: Group
name: sre
```
For the given configuration, the resulting RoleBinding resources are the following ones:
```
$: kubectl get rolebindings.rbac.authorization.k8s.io
NAME ROLE AGE
capsule-oil-0-editor ClusterRole/editor 21s
capsule-oil-1-manager ClusterRole/manager 19s
capsule-oil-2-readonly ClusterRole/readonly 2s
```
> The pattern for the annotation is `clusterrolenames.capsule.clastix.io/${KIND}.${NAME}`.
> The placeholders `${KIND}` and `${NAME}` are referring to the Tenant Owner specification fields, both lower-cased.
>
> In the case of users that are identified using their email address, the symbol `@` wouldn't be supported by the RFC 1123.
> For such cases, the `@` symbol can be replaced with the placeholder `__AT__`.
>
> ```yaml
> apiVersion: capsule.clastix.io/v1beta1
> kind: Tenant
> metadata:
> annotations:
> clusterrolenames.capsule.clastix.io/alice__AT__clastix.io: editor,manager
> spec:
> owners:
> - kind: User
> name: alice@org.tld
> - kind: User
> name: alice@clastix.io
> ```
>
> Instead, with the resulting annotation key exceeding 63 characters length, the zero-based index of the owner can be specified as follows:
>
> ```yaml
> apiVersion: capsule.clastix.io/v1beta1
> kind: Tenant
> metadata:
> annotations:
> clusterrolenames.capsule.clastix.io/1: editor,manager
> spec:
> owners:
> - kind: User
> name: alice@org.tld
> - kind: User
> name: very-long-user-name-that-breaks-rfc-1123@org.tld
> ```
>
> This latter example will assign the roles `editor` and `manager`, assigned to the user `very-long-user-name-that-breaks-rfc-1123@org.tld`.
### User as tenant owner
Bill, the cluster admin, receives a new request from Acme Corp.'s CTO asking for a new tenant to be onboarded and Alice user will be the tenant owner. Bill then assigns Alice's identity of `alice` in the Acme Corp. identity management system. Since Alice is a tenant owner, Bill needs to assign `alice` the Capsule group defined by `--capsule-user-group` option, which defaults to `capsule.clastix.io`.
Bill, the cluster admin, receives a new request from _Acme Corp_'s CTO asking for a new tenant to be onboarded and Alice user will be the tenant owner. Bill then assigns Alice's identity of `alice` in the _Acme Corp_. identity management system. Since Alice is a tenant owner, Bill needs to assign `alice` the Capsule group defined by `--capsule-user-group` option, which defaults to `capsule.clastix.io`.
To keep things simple, we assume that Bill just creates a client certificate for authentication using X.509 Certificate Signing Request, so Alice's certificate has `"/CN=alice/O=capsule.clastix.io"`.
@@ -258,19 +171,24 @@ system:serviceaccounts:{service-account-namespace}
> Please, pay attention when setting a service account acting as tenant owner. Make sure you're not using the group `system:serviceaccounts` or the group `system:serviceaccounts:{capsule-namespace}` as Capsule group, otherwise you'll create a short-circuit in the Capsule controller, being Capsule itself controlled by a serviceaccount.
### Roles assigned to Tenant Owners
## Create namespaces
Alice, once logged with her credentials, can create a new namespace in her tenant, as simply issuing:
By default, all Tenant Owners will be granted with two ClusterRole resources using the RoleBinding API:
1. the Kubernetes default one, [`admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles), that grants most of the namespace scoped resources
2. a custom one, created by Capsule, named `capsule-namespace-deleter`, allowing to delete the created namespaces
In the example below, assuming the tenant owner creates a namespace `oil-production` in Tenant `oil`, you'll see the Role Bindings giving the tenant owner full permissions on the tenant namespaces:
```
kubectl create ns oil-production
$: kubectl get rolebindings -n oil-production
NAME ROLE AGE
capsule-oil-0-admin ClusterRole/admin 6s
capsule-oil-1-capsule-namespace-deleter ClusterRole/capsule-namespace-deleter 5s
```
Alice started the name of the namespace prepended by the name of the tenant: this is not a strict requirement but it is highly suggested because it is likely that many different tenants would like to call their namespaces `production`, `test`, or `demo`, etc.
The enforcement of this naming convention is optional and can be controlled by the cluster administrator with the `--force-tenant-prefix` option as an argument of the Capsule controller.
When Alice creates the namespace, the Capsule controller listening for creation and deletion events assigns to Alice the following roles:
When Alice creates the namespaces, the Capsule controller assigns to Alice the following permissions, so that Alice can act as the admin of all the tenant namespaces.
```yaml
---
@@ -301,28 +219,191 @@ roleRef:
apiGroup: rbac.authorization.k8s.io
```
So Alice is the admin of the namespaces:
In some cases, the cluster admin needs to narrow the range of permissions assigned to tenant owners by assigning a Cluster Role with less permissions than above. Capsule supports the dynamic assignment of any ClusterRole resources for each Tenant Owner.
For example, assign user `Joe` the tenant ownership with only [view](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) permissions on tenant namespaces:
```yaml
kubectl apply -f - << EOF
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
name: oil
annotations:
clusterrolenames.capsule.clastix.io/user.joe: view
spec:
owners:
- name: alice
kind: User
- name: joe
kind: User
EOF
```
you'll see the new Role Bindings assigned to Joe:
```
kubectl get rolebindings -n oil-development
kubectl -n oil-production get rolebindings
NAME ROLE AGE
capsule-oil-0-admin ClusterRole/admin 5s
capsule-oil-1-capsule-namespace-deleter ClusterRole/capsule-namespace-deleter 4s
capsule-oil-0-admin ClusterRole/admin 8d
capsule-oil-1-capsule-namespace-deleter ClusterRole/capsule-namespace-deleter 8d
capsule-oil-2-view ClusterRole/edit 5s
```
The said Role Binding resources are automatically created by Capsule controller when the tenant owner Alice creates a namespace in the tenant.
so that Joe can only view resources in the tenant namespaces:
Alice can deploy any resource in the namespace, according to the predefined
[`admin` cluster role](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles).
```
kubectl --as joe --as-group capsule.clastix.io auth can-i delete pods -n oil-marketing
no
```
> Please, note that, despite created with more restricted permissions, a tenant owner can still create namespaces in the tenant because he belongs to the `capsule.clastix.io` group. If you want a user not acting as tenant owner, but still operating in the tenant, you can assign additional `RoleBindings` without assigning him the tenant ownership.
Custom ClusterRoles are also supported. Assuming the cluster admin creates:
```yaml
kubectl apply -f - << EOF
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: prometheus-servicemonitors-viewer
rules:
- apiGroups: ["monitoring.coreos.com"]
resources: ["servicemonitors"]
verbs: ["get", "list", "watch"]
EOF
```
These permissions can be granted to Joe
```yaml
kubectl apply -f - << EOF
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
name: oil
annotations:
clusterrolenames.capsule.clastix.io/user.joe: view,prometheus-servicemonitors-viewer
spec:
owners:
- name: alice
kind: User
- name: joe
kind: User
EOF
```
For the given configuration, the resulting RoleBinding resources are the following ones:
```
kubectl -n oil-production get rolebindings
NAME ROLE AGE
capsule-oil-0-admin ClusterRole/admin 8d
capsule-oil-1-capsule-namespace-deleter ClusterRole/capsule-namespace-deleter 8d
capsule-oil-2-view ClusterRole/view 11m
capsule-oil-3-prometheus-servicemonitors-viewer ClusterRole/prometheus-servicemonitors-viewer 18s
```
> The pattern for the annotation is `clusterrolenames.capsule.clastix.io/${KIND}.${NAME}`.
> The placeholders `${KIND}` and `${NAME}` are referring to the Tenant Owner specification fields, both lower-cased.
>
> In the case of users that are identified using their email address, the symbol `@` wouldn't be supported by the RFC 1123.
> For such cases, the `@` symbol can be replaced with the placeholder `__AT__`.
>
> ```yaml
> apiVersion: capsule.clastix.io/v1beta1
> kind: Tenant
> metadata:
> annotations:
> clusterrolenames.capsule.clastix.io/alice__AT__clastix.io: editor,manager
> spec:
> owners:
> - kind: User
> name: alice@org.tld
> - kind: User
> name: alice@clastix.io
> ```
>
> Instead, with the resulting annotation key exceeding 63 characters length, the zero-based index of the owner can be specified as follows:
>
> ```yaml
> apiVersion: capsule.clastix.io/v1beta1
> kind: Tenant
> metadata:
> annotations:
> clusterrolenames.capsule.clastix.io/1: editor,manager
> spec:
> owners:
> - kind: User
> name: alice@org.tld
> - kind: User
> name: very-long-user-name-that-breaks-rfc-1123@org.tld
> ```
>
> This latter example will assign the roles `editor` and `manager`, assigned to the user `very-long-user-name-that-breaks-rfc-1123@org.tld`.
### Assign additional Role Bindings
The tenant owner acts as admin of tenant namespaces. Other users can operate inside the tenant namespaces with different levels of permissions and authorizations.
Assuming the cluster admin creates:
```yaml
kubectl apply -f - << EOF
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: prometheus-servicemonitors-viewer
rules:
- apiGroups: ["monitoring.coreos.com"]
resources: ["servicemonitors"]
verbs: ["get", "list", "watch"]
EOF
```
These permissions can be granted to a user without giving the role of tenant owner:
```yaml
kubectl apply -f - << EOF
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
name: oil
spec:
owners:
- name: alice
kind: User
additionalRoleBindings:
- clusterRoleName: 'prometheus-servicemonitors-viewer'
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: joe
EOF
```
## Create namespaces
Alice, once logged with her credentials, can create a new namespace in her tenant, as simply issuing:
```
kubectl create ns oil-production
```
Alice started the name of the namespace prepended by the name of the tenant: this is not a strict requirement but it is highly suggested because it is likely that many different tenants would like to call their namespaces `production`, `test`, or `demo`, etc.
The enforcement of this naming convention is optional and can be controlled by the cluster administrator with the `--force-tenant-prefix` option as an argument of the Capsule controller.
Alice can deploy any resource in any of the namespaces
```
kubectl -n oil-development run nginx --image=docker.io/nginx
kubectl -n oil-development get pods
```
Bill, the cluster admin, can control how many namespaces Alice, creates by setting a quota in the tenant manifest `spec.namespaceOptions.quota`
The cluster admin, can control how many namespaces Alice, creates by setting a quota in the tenant manifest `spec.namespaceOptions.quota`
```yaml
kubectl apply -f - << EOF
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
@@ -333,6 +414,7 @@ spec:
kind: User
namespaceOptions:
quota: 3
EOF
```
Alice can create additional namespaces according to the quota:
@@ -368,58 +450,6 @@ admission webhook "namespace.capsule.clastix.io" denied the request.
```
The enforcement on the maximum number of namespaces per Tenant is the responsibility of the Capsule controller via its Dynamic Admission Webhook capability.
## Assign permissions
Alice acts as the tenant admin. Other users can operate inside the tenant with different levels of permissions and authorizations. Alice is responsible for creating additional roles and assigning these roles to other users to work in the same tenant.
One of the key design principles of the Capsule is self-provisioning management from the tenant owner's perspective. Alice, the tenant owner, does not need to interact with Bill, the cluster admin, to complete her day-by-day duties. On the other side, Bill does not have to deal with multiple requests coming from multiple tenant owners that probably will overwhelm him.
Capsule leaves Alice, and the other tenant owners, the freedom to create RBAC roles at the namespace level, or using the pre-defined cluster roles already available in Kubernetes. Since roles and rolebindings are limited to a namespace scope, Alice can assign the roles to the other users accessing the same tenant only after the namespace is created. This gives Alice the power to administer the tenant without the intervention of the cluster admin.
From the cluster admin perspective, the only required action for Bill is to provide the other identities, eg. `joe` in the Identity Management system. This task can be done once when onboarding the tenant and the number of users accessing the tenant can be part of the tenant business profile.
Alice can create Roles and RoleBindings only in the namespaces she owns
```
kubectl auth can-i get roles -n oil-development
yes
kubectl auth can-i get rolebindings -n oil-development
yes
```
so she can assign the role of namespace `oil-development` admin to Joe, another user accessing the tenant `oil`
```yaml
kubectl --as alice --as-group capsule.clastix.io apply -f - << EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
labels:
name: oil-development:admin
namespace: oil-development
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: admin
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: joe
EOF
```
Joe now can operate on the namespace `oil-development` as admin but he has no access to the other namespaces `oil-production`, and `oil-test` that are part of the same tenant:
```
kubectl --as joe --as-group capsule.clastix.io auth can-i create pod -n oil-development
yes
kubectl --as joe --as-group capsule.clastix.io auth can-i create pod -n oil-production
no
```
> Please, note the user `joe`, in the example above, is not acting as tenant owner. He can just operate in `oil-development` namespace as admin.
## Assign multiple tenants
A single team is likely responsible for multiple lines of business. For example, in our sample organization Acme Corp., Alice is responsible for both the Oil and Gas lines of business. It's more likely that Alice requires two different tenants, for example, `oil` and `gas` to keep things isolated.
@@ -1085,7 +1115,7 @@ Also, Bill can make sure pods belonging to a tenant namespace cannot access othe
Bill can set network policies in the tenant manifest, according to the requirements:
```yaml
kubectl -n oil-production apply -f - << EOF
kubectl apply -f - << EOF
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
@@ -1186,7 +1216,7 @@ Bob, an attacker, could try to schedule a Pod on the same node where Alice is ru
To avoid this kind of attack, Bill, the cluster admin, can force Alice, the tenant owner, to start her Pods using only the allowed values for `ImagePullPolicy`, enforcing the `kubelet` to check the authorization first.
```yaml
kubectl -n oil-production apply -f - << EOF
kubectl apply -f - << EOF
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
@@ -1212,7 +1242,7 @@ The spec `containerRegistries` addresses this task and can provide a combination
```yaml
kubectl -n oil-production apply -f - << EOF
kubectl apply -f - << EOF
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
@@ -1237,95 +1267,13 @@ A Pod running `internal.registry.foo.tld/capsule:latest` as registry will be all
Any attempt of Alice to use a not allowed `containerRegistries` value is denied by the Validation Webhook enforcing it.
## Assign Pod Security Policies
Bill, the cluster admin, can assign a dedicated Pod Security Policy (PSP) to Alice's tenant. This is likely to be a requirement in a multi-tenancy environment.
The cluster admin creates a PSP:
```yaml
kubectl -n oil-production apply -f - << EOF
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: psp:restricted
spec:
privileged: false
# Required to prevent escalations to root.
allowPrivilegeEscalation: false
...
EOF
```
Then create a _ClusterRole_ using or granting the said item
```yaml
kubectl -n oil-production apply -f - << EOF
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: psp:restricted
rules:
- apiGroups: ['policy']
resources: ['podsecuritypolicies']
resourceNames: ['psp:restricted']
verbs: ['use']
EOF
```
Bill can assign this role to all namespaces in the Alice's tenant by setting it in the tenant manifest:
```yaml
kubectl -n oil-production apply -f - << EOF
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
name: oil
spec:
owners:
- name: alice
kind: User
additionalRoleBindings:
- clusterRoleName: psp:privileged
subjects:
- kind: "Group"
apiGroup: "rbac.authorization.k8s.io"
name: "system:authenticated"
EOF
```
With the given specification, Capsule will ensure that all Alice's namespaces will contain a _RoleBinding_ for the specified _Cluster Role_.
For example, in the `oil-production` namespace, Alice will see:
```yaml
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: 'capsule-oil-psp:privileged'
namespace: oil-production
labels:
capsule.clastix.io/role-binding: a10c4c8c48474963
capsule.clastix.io/tenant: oil
subjects:
- kind: Group
apiGroup: rbac.authorization.k8s.io
name: 'system:authenticated'
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: 'psp:privileged'
```
With the above example, Capsule is forbidding any authenticated user in `oil-production` namespace to run privileged pods and to perform privilege escalation as declared by the Cluster Role `psp:privileged`.
## Create Custom Resources
Capsule grants admin permissions to the tenant owners but is only limited to their namespaces. To achieve that, it assigns the ClusterRole [admin](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) to the tenant owner. This ClusterRole does not permit the installation of custom resources in the namespaces.
In order to leave the tenant owner to create Custom Resources in their namespaces, the cluster admin defines a proper Cluster Role. For example:
```yaml
kubectl -n oil-production apply -f - << EOF
kubectl apply -f - << EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
@@ -1350,7 +1298,7 @@ EOF
Bill can assign this role to any namespace in the Alice's tenant by setting it in the tenant manifest:
```yaml
kubectl -n oil-production apply -f - << EOF
kubectl apply -f - << EOF
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
@@ -1448,10 +1396,10 @@ spec:
> This feature is still in an alpha stage and requires a high amount of computing resources due to the dynamic client requests.
## Taint namespaces
With Capsule, Bill can _"taint"_ the namespaces created by Alice with additional labels and/or annotations. There is no specific semantic assigned to these labels and annotations: they just will be assigned to the namespaces in the tenant as they are created by Alice. This can help the cluster admin to implement specific use cases. As it can be used to implement backup as a service for namespaces in the tenant.
## Assign Additional Metadata
The cluster admin can _"taint"_ the namespaces created by tenant onwers with additional metadata as labels and annotations. There is no specific semantic assigned to these labels and annotations: they will be assigned to the namespaces in the tenant as they are created. This can help the cluster admin to implement specific use cases as, for example, leave only a given tenant to be backuped by a backup service.
Bill assigns additional labels and annotations to all namespaces created in the `oil` tenant:
Assigns additional labels and annotations to all namespaces created in the `oil` tenant:
```yaml
kubectl apply -f - << EOF
@@ -1466,18 +1414,42 @@ spec:
namespaceOptions:
additionalMetadata:
annotations:
capsule.clastix.io/backup: "true"
storagelocationtype: s3
labels:
capsule.clastix.io/tenant: oil
capsule.clastix.io/backup: "true"
EOF
```
When Alice creates a namespace, this will inherit the given label and/or annotation.
When the tenant owner creates a namespace, it inherits the given label and/or annotation:
## Taint services
With Capsule, Bill can _"taint"_ the services created by Alice with additional labels and/or annotations. There is no specific semantic assigned to these labels and annotations: they just will be assigned to the services in the tenant as they are created by Alice. This can help the cluster admin to implement specific use cases.
```yaml
apiVersion: v1
kind: Namespace
metadata:
annotations:
storagelocationtype: s3
labels:
capsule.clastix.io/tenant: oil
kubernetes.io/metadata.name: oil-production
name: oil-production
capsule.clastix.io/backup: "true"
name: oil-production
ownerReferences:
- apiVersion: capsule.clastix.io/v1beta1
blockOwnerDeletion: true
controller: true
kind: Tenant
name: oil
spec:
finalizers:
- kubernetes
status:
phase: Active
```
Bill assigns additional labels and annotations to all services created in the `oil` tenant:
Additionally, the cluster admin can _"taint"_ the services created by the tenant owners with additional metadata as labels and annotations.
Assigns additional labels and annotations to all services created in the `oil` tenant:
```yaml
kubectl apply -f - << EOF
@@ -1491,14 +1463,30 @@ spec:
kind: User
serviceOptions:
additionalMetadata:
annotations:
capsule.clastix.io/backup: "true"
labels:
capsule.clastix.io/tenant: oil
capsule.clastix.io/backup: "true"
EOF
```
When Alice creates a service in a namespace, this will inherit the given label and/or annotation.
When the tenant owner creates a service in a tenant namespace, it inherits the given label and/or annotation:
```yaml
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: oil-production
labels:
capsule.clastix.io/backup: "true"
spec:
ports:
- protocol: TCP
port: 80
targetPort: 8080
selector:
run: nginx
type: ClusterIP
```
## Cordon a Tenant

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -0,0 +1,470 @@
# Multi-tenancy the GitOps way
This guide is intended to cover how to use Flux v2 with [multi-tenancy lockdown features](https://fluxcd.io/docs/installation/#multi-tenancy-lockdown) with Capsule and Capsule Proxy together, to enable a Namespace-as-a-Service the GitOps-way.
### Flux and multi-tenancy
Flux v2 released a [set of features](https://fluxcd.io/blog/2022/05/may-2022-security-announcement/#whats-next-for-flux) that further increasesed security for multi-tenancy scenarios.
These features enable you to:
- disable cross-Namespace reference of Source CRs from Reconciliation CRs and Notification CRs. This way, especially for tenants, they can't access resources outside their space. This can be achieved with `--no-cross-namespace-refs=true` option of kustomize, helm, notification, image-reflector, image-automation controllers.
- set a default `ServiceAccount` impersonation for Reconciliation CRs. This is supposed to be an unprivileged SA that reconciles just the tenant's desired state. This will be enforced when is not otherwise specified explicitely in Reconciliation CR spec. This can be enforced with the `--default-service-account=<name>` option of helm and kustomize controllers.
> For this responsibility we identify a Tenant GitOps Reconciler identity, which is a ServiceAccount and it's also the tenant owner (more on tenants and owners later on, with Capsule).
- disallow remote bases for Kustomizations. Actually, this is not stryctly required, but it decreases the risk of referencing Kustomizations which aren't part of the controlled GitOps pipelines. In a multi-tenant scenario this is important too. They can be disabled with `--no-remote-bases=true` option of the kustomize controller.
Where required, to ensure privileged Reconciliation resources have the needed privileges to be reconciled, we can explicitely set a privileged `ServiceAccount`s.
In any case, is required that the `ServiceAccount` is in the same `Namespace` of the `Kustomization`, so unprivileged spaces should not have privileged `ServiceAccount`s available.
For example, for the root `Kustomization`:
```yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: flux-system
namespace: flux-system
spec:
serviceAccountName: kustomize-controller # It has cluster-admin permissions
path: ./clusters/staging
sourceRef:
kind: GitRepository
name: flux-system
```
In example, the cluster admin is supposed to apply this Kustomization, during the cluster bootstrap that i.e. will reconcile also Flux itself.
All the remaining Reconciliation resources can be children of this Kustomization.
![bootstrap](./assets/kustomization-hierarchy-root-tenants.png)
### Namespace-as-a-Service
Tenants could have his own set of Namespaces to operate on but it should be prepared by higher-level roles, like platform admins: the declarations would be part of the platform space.
They would be responsible of tenants administration, and each change (e.g. new tenant Namespace) should be a request that would pass through approval.
![no-naas](./assets/flux-tenants-reconciliation.png)
What if we would like to provide tenants the ability to manage also their own space the GitOps-way? Enter Capsule.
![naas](./assets/flux-tenants-capsule-reconciliation.png)
## The ingredients of the recipe
> Legenda:
> - Privileged space: group of Namespaces which are not part of any Tenant.
> - Privileged identity: identity that won't pass through Capsule tenant access control.
> - Unprivileged space: group of Namespaces which are part of a Tenant.
> - Unprivileged identity: identity that would pass through Capsule tenant access control.
> - Tenant GitOps Reconciler: a machine Tenant Owner expected to reconcile Tenant desired state.
### Capsule
Capsule provides a Custom Resource `Tenant` and ability to set its owners through `spec.owners` as references to:
- `User`
- `Group`
- `ServiceAccount`
#### Tenant and Tenant Owner
We would like to let a machine reconcile Tenant's states, we'll need a `ServiceAccount` as a Tenant Owner:
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: gitops-reconciler
namespace: my-tenant
---
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
name: my-tenant
spec:
owners:
- name: system:serviceaccount:my-tenant:gitops-reconciler # the Tenant GitOps Reconciler
```
From now on, we'll refer to it as the **Tenant GitOps Reconciler**.
#### Tenant Groups
We also need to state that Capsule should enforce tenant access control for requests coming from tenants, and we can do that by specifying one of the `Group`s bound by default by Kubernetes to the Tenant GitOps Reconciler `ServiceAccount` in the `CapsuleConfiguration`:
```yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: CapsuleConfiguration
metadata:
name: default
spec:
userGroups:
- system:serviceaccounts:my-tenant
```
Other privileged requests, e.g. for reconciliation coming from the Flux privileged `ServiceAccount`s like `flux-system/kustomize-controller` will bypass Capsule.
### Flux
Flux enables to specify with which identity Reconciliation resources are reconciled, through:
- `ServiceAccount` impersonation
- `kubeconfig`
#### ServiceAccount
As by default Flux reconciles those resources with Flux `cluster-admin` Service Accounts, we set at controller-level the **default `ServiceAccount` impersonation** to the unprivileged **Tenant GitOps Reconciler**:
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- flux-controllers.yaml
patches:
- patch: |
- op: add
path: /spec/template/spec/containers/0/args/0
value: --default-service-account=gitops-reconciler # the Tenant GitOps Reconciler
target:
kind: Deployment
name: "(kustomize-controller|helm-controller)"
```
This way tenants can't make Flux apply their Reconciliation resources with Flux's privileged Service Accounts, by not specifying a `spec.ServiceAccountName` on them.
At the same time at resource-level in privileged space we still can specify a privileged ServiceAccount, and its reconciliation requests won't pass through Capsule validation:
```yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: flux-system
namespace: flux-system
spec:
serviceAccountName: kustomize-controller
path: ./clusters/staging
sourceRef:
kind: GitRepository
name: flux-system
```
#### Kubeconfig
We also need to specify on Tenant's Reconciliation resources, the `Secret` with **`kubeconfig`** configured to use the **Capsule Proxy** as the API server in order to provide the Tenant GitOps Reconciler the ability to list cluster-level resources.
The `kubeconfig` would specify also as the token the Tenant GitOps Reconciler SA token,
For example:
```yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: my-app
namespace: my-tenant
spec:
kubeConfig:
secretRef:
name: gitops-reconciler-kubeconfig
key: kubeconfig
sourceRef:
kind: GitRepository
name: my-tenant
path: ./staging
```
> We'll see how to prepare the related `Secret` (i.e. *gitops-reconciler-kubeconfig*) later on.
Each request made with this kubeconfig will be done impersonating the user of the default impersonation SA, that is the same of the token specified in the kubeconfig.
To deepen on this please go to [#Insights](#insights).
## The recipe
### How to setup Tenants GitOps-ready
Given that [Capsule](github.com/clastix/capsule) and [Capsule Proxy](github.com/clastix/capsule-proxy) are installed, and [Flux v2](https://github.com/fluxcd/flux2) configured with [multi-tenancy lockdown](https://fluxcd.io/docs/installation/#multi-tenancy-lockdown) features, of which the patch below:
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- flux-components.yaml
patches:
- patch: |
- op: add
path: /spec/template/spec/containers/0/args/0
value: --no-cross-namespace-refs=true
target:
kind: Deployment
name: "(kustomize-controller|helm-controller|notification-controller|image-reflector-controller|image-automation-controller)"
- patch: |
- op: add
path: /spec/template/spec/containers/0/args/-
value: --no-remote-bases=true
target:
kind: Deployment
name: "kustomize-controller"
- patch: |
- op: add
path: /spec/template/spec/containers/0/args/0
value: --default-service-account=gitops-reconciler # The Tenant GitOps Reconciler
target:
kind: Deployment
name: "(kustomize-controller|helm-controller)"
- patch: |
- op: add
path: /spec/serviceAccountName
value: kustomize-controller
target:
kind: Kustomization
name: "flux-system"
```
this is the required set of resources to setup a Tenant:
- `Namespace`: the Tenant GitOps Reconciler "home". This is not part of the Tenant to avoid a chicken & egg problem:
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: my-tenant
```
- `ServiceAccount` of the Tenant GitOps Reconciler, in the above `Namespace`:
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: gitops-reconciler
namespace: my-tenant
```
- `Tenant` resource with the above Tenant GitOps Reconciler's SA as Tenant Owner, with:
- Additional binding to *cluster-admin* `ClusterRole` for the Tenant's `Namespace`s and `Namespace` of the Tenant GitOps Reconciler' `ServiceAccount`.
By default Capsule binds only `admin` ClusterRole, which has no privileges over Custom Resources, but *cluster-admin* has. This is needed to operate on Flux CRs:
```yaml
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
name: my-tenant
spec:
additionalRoleBindings:
- clusterRoleName: cluster-admin
subjects:
- name: gitops-reconciler
kind: ServiceAccount
namespace: my-tenant
owners:
- name: system:serviceaccount:my-tenant:gitops-reconciler
kind: ServiceAccount
```
- Additional binding to *cluster-admin* `ClusterRole` for home `Namespace` of the Tenant GitOps Reconciler' `ServiceAccount`, so that the Tenant GitOps Reconciler can create Flux CRs on the tenant home Namespace and use Reconciliation resource's `spec.targetNamespace` to place resources to `Tenant` `Namespace`s:
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: gitops-reconciler
namespace: my-tenant
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: gitops-reconciler
namespace: my-tenant
```
- Additional `Group` in the `CapsuleConfiguration` to make Tenant GitOps Reconciler requests pass through Capsule admission (group `system:serviceaccount:<tenant-gitops-reconciler-home-namespace>`):
```yaml
apiVersion: capsule.clastix.io/v1alpha1
kind: CapsuleConfiguration
metadata:
name: default
spec:
userGroups:
- system:serviceaccounts:my-tenant
```
- Additional `ClusterRole` with related `ClusterRoleBinding` that allows to `PATCH` requests on Namespaces, besides `CREATE`. Flux kustomize controller will `kubectl-apply` resources:
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: capsule-namespace-provisioner-gitops
rules:
- apiGroups:
- ""
resources:
- namespaces
verbs:
- patch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: capsule-namespace-provisioner-gitops-my-tenant
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: capsule-namespace-provisioner-gitops
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: system:serviceaccounts:my-tenant
```
- Additional `ClusterRole` with related `ClusterRoleBinding` that allows the Tenant GitOps Reconciler to impersonate his own `User` (e.g. `system:serviceaccount:my-tenant:gitops-reconciler`):
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: my-tenant-gitops-reconciler-impersonator
rules:
- apiGroups: [""]
resources: ["users"]
verbs: ["impersonate"]
resourceNames: ["system:serviceaccount:my-tenant:gitops-reconciler"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: my-tenant-gitops-reconciler-impersonate
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: my-tenant-gitops-reconciler-impersonator
subjects:
- name: gitops-reconciler
kind: ServiceAccount
namespace: my-tenant
```
- `Secret` with `kubeconfig` for the Tenant GitOps Reconciler with Capsule Proxy as `kubeconfig.server` and the SA token as `kubeconfig.token`.
> This is supported only with Service Account static tokens.
- Flux Source and Reconciliation resources that refer to Tenant desired state. This typically points to a specific path inside a dedicated Git repository, where tenant's root configuration reside:
```yaml
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: GitRepository
metadata:
name: my-tenant
namespace: my-tenant
spec:
url: https://github.com/my-tenant/all.git # Git repository URL
ref:
branch: main # Git reference
---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: my-tenant
namespace: my-tenant
spec:
kubeConfig:
secretRef:
name: gitops-reconciler-kubeconfig
key: kubeconfig
sourceRef:
kind: GitRepository
name: my-tenant
path: config # Path to config from GitRepository Source
```
This `Kustomization` can in turn refer to further `Kustomization` resources creating a tenant configuration hierarchy.
#### Generate the Capsule Proxy kubeconfig Secret
You need to create a `Secret` in the Tenant GitOps Reconciler home `Namespace`, containing the `kubeconfig` that specifies:
- `server`: Capsule Proxy `Service` URL with related CA certificate for TLS
- `token`: the token of the `Tenant` GitOps Reconciler
With required privileges over the target `Namespace` to create `Secret`, you can generate it with the `proxy-kubeconfig-generator` utility:
```sh
$ go install github.com/maxgio92/proxy-kubeconfig-generator@latest
$ proxy-kubeconfig-generator \
--kubeconfig-secret-key kubeconfig \
--namespace my-tenant \
--server 'https://capsule-proxy.capsule-system.svc:9001' \
--server-tls-secret-namespace capsule-system \
--server-tls-secret-name capsule-proxy \
--serviceaccount gitops-reconciler
```
### How a Tenant can declare his state
Considering the example above, a Tenant `my-tenant` could place in his own repository (i.e. `https://github.com/my-tenant/all`), on branch `main` at path `/config` further Reconciliation resources, like:
```yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
name: my-apps
namespace: my-tenant
spec:
kubeConfig:
secretRef:
name: gitops-reconciler-kubeconfig
key: kubeconfig
sourceRef:
kind: GitRepository
name: my-tenant
path: config/apps
```
that refer to the same Source but different path (i.e. `config/apps`) that could contain his applications' manifests.
The same is valid for a `HelmRelease`s, that instead will refer to an `HelmRepository` Source.
The reconciliation requests will pass through Capsule Proxy as Tenant GitOps Reconciler with impersonation. Then, as the identity group of the requests matches the Capsule groups they will be validated by Capsule, and finally the RBAC will provide boundaries to Tenant GitOps Reconciler privileges.
> If the `spec.kubeConfig` is not specified the Flux privileged `ServiceAccount` will impersonate the default unprivileged Tenant GitOps Reconciler `ServiceAccount` as configured with `--default-service-account` option of kustomize and helm controllers, but it list requests on cluster-level resources like `Namespace`s will fail.
## Full setup
To have a glimp on a full setup you can follow the [flux2-capsule-multi-tenancy](https://github.com/clastix/flux2-capsule-multi-tenancy.git) repository.
For simplicity, the system and tenants declarations are on the same repository but on dedicated git branches.
It's a fork of [flux2-multi-tenancy](https://github.com/fluxcd/flux2-multi-tenancy.git) but with the integration we saw with Capsule.
## Insights
### Why ServiceAccount that impersonates its own User
As stated just above, you'd be wondering why a user would make a request impersonating himself (i.e. the Tenant GitOps Reconciler ServiceAccount User).
This is because we need to make tenant reconciliation requests through Capsule Proxy and we want to protect from risk of privilege escalation done through bypass of impersonation.
### Threats
##### Bypass unprivileged impersonation
The reason why we can't set impersonation to be optional is because, as each tenant is allowed to not specify neither the kubeconfig nor the impersonation SA for the Reconciliation resource, and because in any case that kubeconfig could contain whatever privileged credentials, Flux would otherwise use the privileged ServiceAccount, to reconcile tenant resources.
That way, a tenant would be capable of managing the GitOps way the cluster as he was a cluster admin.
Furthermore, let's see if there are other vulnerabilities we are able to protect from.
##### Impersonate privileged SA
Then, what if a tenant tries to escalate by using one of the Flux controllers privileged `ServiceAccount`s?
As `spec.ServiceAccountName` for Reconciliation resource cannot cross-namespace reference Service Accounts, tenants are able to let Flux apply his own resources only with ServiceAccounts that reside in his own Namespaces. Which is, Namespace of the ServiceAccount and Namespace of the Reconciliation resource must match.
He could neither create the Reconciliation resource where a privileged ServiceAccount is present (like flux-system), as the Namespace has to be owned by the Tenant. Capsule would block those Reconciliation resource creation requests.
##### Create and impersonate privileged SA
Then, what if a tenant tries to escalate by creating a privileged `ServiceAccount` inside on of his own `Namespace`s?
A tenant could create a `ServiceAccount` in an owned `Namespace`, but he can't neither bind at cluster-level nor at a non-owned Namespace-level a ClusterRole, as that wouldn't be permitted by Capsule admission controllers.
Now let's go on with the practical part.
##### Change ownership of privileged Namespaces (e.g. flux-system)
He could try to use privileged `ServiceAccount` by changing ownership of a privileged Namespace so that he could create Reconciliation resource there and using the privileged SA.
This is not permitted as he can't patch Namespaces which have not been created by him. Capsule request validation would not pass.
For other protections against threats in this multi-tenancy scenario please see the Capsule [Multi-Tenancy Benchmark](/docs/general/mtb).
## References
- https://fluxcd.io/docs/installation/#multi-tenancy-lockdown
- https://fluxcd.io/blog/2022/05/may-2022-security-announcement/
- https://github.com/clastix/capsule-proxy/issues/218
- https://github.com/clastix/capsule/issues/528
- https://github.com/clastix/flux2-capsule-multi-tenancy
- https://github.com/fluxcd/flux2-multi-tenancy
- https://fluxcd.io/docs/guides/repository-structure/

View File

@@ -47,9 +47,15 @@ Verify that the service monitor is working correctly through the Prometheus "tar
![Prometheus Targets](./assets/prometheus_targets.png)
### Deploy dashboard
A dashboard for Grafana is provided as [dashboard.json](https://github.com/clastix/capsule/blob/master/config/grafana/dashboard.json).
Simply upload [dashboard.json](https://github.com/clastix/capsule/blob/master/config/grafana/dashboard.json) file to Grafana through _Create_ -> _Import_,
making sure to select the correct Prometheus data source:
Render with `kustomize` the dashboard as a ConfigMap and apply in the namespace where Grafana is installed, making sure to select the correct Prometheus datasource:
```
kubectl -n <grafana-namespace> apply -k config/grafana
```
Alternatively, manual upload the dashboard in JSON format to Grafana through _Create -> Import_:
![Grafana Import](./assets/upload_json.png)

View File

@@ -0,0 +1,258 @@
# Pod Security
In Kubernetes, by default, workloads run with administrative access, which might be acceptable if there is only a single application running in the cluster or a single user accessing it. This is seldomly required and youll consequently suffer a noisy neighbour effect along with large security blast radiuses.
Many of these concerns were addressed initially by [PodSecurityPolicies](https://kubernetes.io/docs/concepts/security/pod-security-policy) which have been present in the Kubernetes APIs since the very early days.
The Pod Security Policies are deprecated in Kubernetes 1.21 and removed entirely in 1.25. As replacement, the [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) and [Pod Security Admission](https://kubernetes.io/docs/concepts/security/pod-security-admission/) has been introduced. Capsule support the new standard for tenants under its control as well as the oldest approach.
## Pod Security Policies
As stated in the documentation, *"PodSecurityPolicies enable fine-grained authorization of pod creation and updates. A Pod Security Policy is a cluster-level resource that controls security sensitive aspects of the pod specification. The `PodSecurityPolicy` objects define a set of conditions that a pod must run with in order to be accepted into the system, as well as defaults for the related fields."*
Using the [Pod Security Policies](https://kubernetes.io/docs/concepts/security/pod-security-policy), the cluster admin can impose limits on pod creation, for example the types of volume that can be consumed, the linux user that the process runs as in order to avoid running things as root, and more. From multi-tenancy point of view, the cluster admin has to control how users run pods in their tenants with a different level of permission on tenant basis.
Assume the Kubernetes cluster has been configured with [Pod Security Policy Admission Controller](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#podsecuritypolicy) enabled in the APIs server: `--enable-admission-plugins=PodSecurityPolicy`
The cluster admin creates a `PodSecurityPolicy`:
```yaml
kubectl apply -f - << EOF
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: psp:restricted
spec:
privileged: false
# Required to prevent escalations to root.
allowPrivilegeEscalation: false
EOF
```
Then create a _ClusterRole_ using or granting the said item
```yaml
kubectl apply -f - << EOF
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: psp:restricted
rules:
- apiGroups: ['policy']
resources: ['podsecuritypolicies']
resourceNames: ['psp:restricted']
verbs: ['use']
EOF
```
He can assign this role to all namespaces in a tenant by setting the tenant manifest:
```yaml
kubectl apply -f - << EOF
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
name: oil
spec:
owners:
- name: alice
kind: User
additionalRoleBindings:
- clusterRoleName: psp:privileged
subjects:
- kind: "Group"
apiGroup: "rbac.authorization.k8s.io"
name: "system:authenticated"
EOF
```
With the given specification, Capsule will ensure that all tenant namespaces will contain a _RoleBinding_ for the specified _Cluster Role_:
```yaml
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: 'capsule-oil-psp:privileged'
namespace: oil-production
labels:
capsule.clastix.io/tenant: oil
subjects:
- kind: Group
apiGroup: rbac.authorization.k8s.io
name: 'system:authenticated'
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: 'psp:privileged'
```
Capsule admission controller forbids the tenant owner to run privileged pods in `oil-production` namespace and perform privilege escalation as declared by the above Cluster Role `psp:privileged`.
As tenant owner, creates a namespace:
```
kubectl --kubeconfig alice-oil.kubeconfig create ns oil-production
```
and create a pod with privileged permissions:
```yaml
kubectl --kubeconfig alice-oil.kubeconfig apply -f - << EOF
apiVersion: v1
kind: Pod
metadata:
name: nginx
namespace: oil-production
spec:
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
securityContext:
privileged: true
EOF
```
Since the assigned `PodSecurityPolicy` explicitly disallows privileged containers, the tenant owner will see her request to be rejected by the Pod Security Policy Admission Controller.
## Pod Security Standards
One of the issues with Pod Secury Policies is that it is difficult to apply restrictive permissions on a granular level, increasing security risk. Also the Pod Security Policies get applied when the request is submitted and there is no way of applying them to pods that are already running. For these, and other reasons, the Kubernetes community decided to deprecate the Pod Secury Policies.
As the Pod Secury Policies get deprecated and removed, the [Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) is used in place. It defines three different policies to broadly cover the security spectrum. These policies are cumulative and range from highly-permissive to highly-restrictive:
- **Privileged**: unrestricted policy, providing the widest possible level of permissions.
- **Baseline**: minimally restrictive policy which prevents known privilege escalations.
- **Restricted**: heavily restricted policy, following current Pod hardening best practices.
Kubernetes provides a built-in [Admission Controller](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#podsecurity) to enforce the Pod Security Standards at either:
1. cluster level which applies a standard configuration to all namespaces in a cluster
2. namespace level, one namespace at a time
For the first case, the cluster admin has to configure the Admission Controller and pass the configuration to the `kube-apiserver` by mean of the `--admission-control-config-file` extra argument, for example:
```yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1beta1
kind: PodSecurityConfiguration
defaults:
enforce: "baseline"
enforce-version: "latest"
warn: "restricted"
warn-version: "latest"
audit: "restricted"
audit-version: "latest"
exemptions:
usernames: []
runtimeClasses: []
namespaces: [kube-system]
```
For the second case, he can just assign labels to the specific namespace he wants enforce the policy since the Pod Security Admission Controller is enabled by default starting from Kubernetes 1.23+:
```yaml
apiVersion: v1
kind: Namespace
metadata:
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/audit: restricted
name: development
```
## Pod Security Standards with Capsule
According to the regular Kubernetes segregation model, the cluster admin has to operate either at cluster level or at namespace level. Since Capsule introduces a further segregation level (the _Tenant_ abstraction), the cluster admin can implement Pod Security Standards at tenant level by simply forcing specific labels on all the namespaces created in the tenant.
As cluster admin, create a tenant with additional labels:
```yaml
kubectl apply -f - << EOF
apiVersion: capsule.clastix.io/v1beta1
kind: Tenant
metadata:
name: oil
spec:
namespaceOptions:
additionalMetadata:
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
owners:
- kind: User
name: alice
EOF
```
All namespaces created by the tenant owner, will inherit the Pod Security labels:
```yaml
apiVersion: v1
kind: Namespace
metadata:
labels:
capsule.clastix.io/tenant: oil
kubernetes.io/metadata.name: oil-development
name: oil-development
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/audit: restricted
name: oil-development
ownerReferences:
- apiVersion: capsule.clastix.io/v1beta1
blockOwnerDeletion: true
controller: true
kind: Tenant
name: oil
```
and the regular Pod Security Admission Controller does the magic:
```yaml
kubectl --kubeconfig alice-oil.kubeconfig apply -f - << EOF
apiVersion: v1
kind: Pod
metadata:
name: nginx
namespace: oil-production
spec:
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
securityContext:
privileged: true
EOF
```
The request gets denied:
```
Error from server (Forbidden): error when creating "STDIN":
pods "nginx" is forbidden: violates PodSecurity "baseline:latest": privileged
(container "nginx" must not set securityContext.privileged=true)
```
If the tenant owner tries to change o delete the above labels, Capsule will reconcile them to the original tenant manifest set by the cluster admin.
As additional security measure, the cluster admin can also prevent the tenant owner to make an improper usage of the above labels:
```
kubectl annotate tenant oil \
capsule.clastix.io/forbidden-namespace-labels-regexp="pod-security.kubernetes.io\/(enforce|warn|audit)"
```
In that case, the tenant owner gets denied if she tries to use the labels:
```
kubectl --kubeconfig alice-oil.kubeconfig label ns oil-production \
pod-security.kubernetes.io/enforce=restricted \
--overwrite
Error from server (Label pod-security.kubernetes.io/audit is forbidden for namespaces in the current Tenant ...
```

View File

@@ -1,23 +1,125 @@
# Tenants Backup and Restore with Velero
[Velero](https://velero.io) is a backup and restore solution that performs disaster recovery and migrates Kubernetes cluster resources and persistent volumes.
[Velero](https://velero.io) is a backup and restore solution that performs data protection, disaster recovery and migrates Kubernetes cluster from on-premises to the Cloud or between different Clouds.
Using Velero in a Kubernetes cluster where Capsule is installed can lead to an incomplete restore of the cluster's Tenants. This is because Velero omits the `ownerReferences` section from the tenant's namespace manifests when backup them.
When coming to backup and restore in Kubernetes, we have two main requirements:
To avoid this problem you can use the script `velero-restore.sh` under the `hack/` folder.
- Configurations backup
- Data backup
In case of a data loss, the right thing to do is to restore the cluster with **Velero** at first. Once Velero has finished, you can proceed using the script to complete the restoration.
The first requirement aims to backup all the resources stored into `etcd` database, for example: `namespaces`, `pods`, `services`, `deployments`, etc. The second is about how to backup stateful application data as volumes.
```bash
./velero-restore.sh --kubeconfing /path/to/your/kubeconfig restore
The main limitation of Velero is the multi tenancy. Currently, Velero does not support multi tenancy meaning it can be only used from admin users and so it cannot provided "as a service" to the users. This means that the cluster admin needs to take care of users' backup.
Assuming you have multiple tenants managed by Capsule, for example `oil` and `gas`, as cluster admin, you can to take care of scheduling backups for:
- Tenant cluster resources
- Namespaces belonging to each tenant
## Create backup of a tenant
Create a backup of the tenant `oil`. It consists in two different backups:
- backup of the tenant resource
- backup of all the resources belonging to the tenant
To backup the `oil` tenant selectively, label the tenant as:
```
kubectl label tenant oil capsule.clastix.io/tenant=oil
```
Running this command, we are going to patch the tenant's namespaces manifests that are actually `ownerReferences`-less. Once the command has finished its run, you got the cluster back.
and create the backup
Additionally, you can also specify a selected range of tenants to be restored:
```bash
./velero-restore.sh --tenant "gas oil" restore
```
velero create backup oil-tenant \
--include-cluster-resources=true \
--include-resources=tenants.capsule.clastix.io \
--selector capsule.clastix.io/tenant=oil
```
In this way, only the tenants **gas** and **oil** will be restored.
resulting in the following Velero object:
```yaml
apiVersion: velero.io/v1
kind: Backup
metadata:
name: oil-tenant
spec:
defaultVolumesToRestic: false
hooks: {}
includeClusterResources: true
includedNamespaces:
- '*'
includedResources:
- tenants.capsule.clastix.io
labelSelector:
matchLabels:
capsule.clastix.io/tenant: oil
metadata: {}
storageLocation: default
ttl: 720h0m0s
```
Create a backup of all the resources belonging to the `oil` tenant namespaces:
```
velero create backup oil-namespaces \
--include-cluster-resources=false \
--include-namespaces oil-production,oil-development,oil-marketing
```
resulting ti the following Velero object:
```yaml
apiVersion: velero.io/v1
kind: Backup
metadata:
name: oil-namespaces
spec:
defaultVolumesToRestic: false
hooks: {}
includeClusterResources: false
includedNamespaces:
- oil-production
- oil-development
- oil-marketing
metadata: {}
storageLocation: default
ttl: 720h0m0s
```
> Velero requires an Object Storage backend where to store backups, you should take care of this requirement before to use Velero.
## Restore a tenant from the backup
To recover the tenant after a disaster, or to migrate it to another cluster, create a restore from the previous backups:
```
velero create restore --from-backup oil-tenant
velero create restore --from-backup oil-namespaces
```
Using Velero to restore a Capsule tenant can lead to an incomplete recovery of tenant because the namespaces restored with Velero do not have the `OwnerReference` field used to bind the namespaces to the tenant. For this reason, all restored namespaces are not bound to the tenant:
```
kubectl get tnt
NAME STATE NAMESPACE QUOTA NAMESPACE COUNT NODE SELECTOR AGE
gas active 9 5 {"pool":"gas"} 34m
solar active 9 8 {"pool":"solar"} 33m
oil active 9 0 # <<< {"pool":"oil"} 54m
```
To avoid this problem you can use the script `velero-restore.sh` located under the `hack/` folder:
```
./velero-restore.sh --kubeconfing /path/to/your/kubeconfig --tenant "oil" restore
```
Running this command, we are going to patch the tenant's namespaces manifests that are actually `ownerReferences`-less. Once the command has finished its run, you got the tenant back.
```
kubectl get tnt
NAME STATE NAMESPACE QUOTA NAMESPACE COUNT NODE SELECTOR AGE
gas active 9 5 {"pool":"gas"} 44m
solar active 9 8 {"pool":"solar"} 43m
oil active 9 3 # <<< {"pool":"oil"} 12s
```

View File

@@ -70,10 +70,18 @@ module.exports = function (api) {
label: 'Upgrading Tenant version',
path: '/docs/guides/upgrading'
},
{
label: 'Multi-tenant GitOps with Flux',
path: '/docs/guides/flux2-capsule'
},
{
label: 'Install on Charmed Kubernetes',
path: '/docs/guides/charmed'
},
{
label: 'Control Pod Security',
path: '/docs/guides/pod-security'
},
{
title: 'Managed Kubernetes',
subItems: [

28
docs/package-lock.json generated
View File

@@ -2143,9 +2143,9 @@
}
},
"arg": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz",
"integrity": "sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA==",
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true
},
"argparse": {
@@ -7703,9 +7703,9 @@
"integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="
},
"lilconfig": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz",
"integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==",
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz",
"integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==",
"dev": true
},
"lines-and-columns": {
@@ -8243,9 +8243,9 @@
"dev": true
},
"moment": {
"version": "2.29.2",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg=="
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"move-concurrently": {
"version": "1.0.1",
@@ -9549,7 +9549,7 @@
"postcss-functions": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postcss-functions/-/postcss-functions-3.0.0.tgz",
"integrity": "sha1-DpTQFERwCkgd4g3k1V+yZAVkJQ4=",
"integrity": "sha512-N5yWXWKA+uhpLQ9ZhBRl2bIAdM6oVJYpDojuI1nF2SzXBimJcdjFwiAouBVbO5VuOF3qA6BSFWFc3wXbbj72XQ==",
"dev": true,
"requires": {
"glob": "^7.1.2",
@@ -10669,7 +10669,7 @@
"pretty-hrtime": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz",
"integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=",
"integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==",
"dev": true
},
"prism-themes": {
@@ -13186,9 +13186,9 @@
"integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="
},
"terser": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz",
"integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==",
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz",
"integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==",
"requires": {
"commander": "^2.20.0",
"source-map": "~0.6.1",

View File

@@ -6441,9 +6441,9 @@ modern-normalize@^1.1.0:
integrity sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==
moment@^2.24.0:
version "2.29.2"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4"
integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==
version "2.29.4"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
move-concurrently@^1.0.1:
version "1.0.1"
@@ -9022,9 +9022,9 @@ source-map-resolve@^0.5.0:
urix "^0.1.0"
source-map-support@~0.5.12:
version "0.5.20"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9"
integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==
version "0.5.21"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
dependencies:
buffer-from "^1.0.0"
source-map "^0.6.0"
@@ -9538,9 +9538,9 @@ terser-webpack-plugin@^1.4.3:
worker-farm "^1.7.0"
terser@^4.1.2:
version "4.8.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17"
integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==
version "4.8.1"
resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f"
integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==
dependencies:
commander "^2.20.0"
source-map "~0.6.1"

41
main.go
View File

@@ -142,29 +142,30 @@ func main() {
directCfg := configuration.NewCapsuleConfiguration(ctx, directClient, configurationName)
tlsReconciler := &tlscontroller.Reconciler{
Client: directClient,
Log: ctrl.Log.WithName("controllers").WithName("TLS"),
Namespace: namespace,
Configuration: directCfg,
}
if directCfg.EnableTLSConfiguration() {
tlsReconciler := &tlscontroller.Reconciler{
Client: directClient,
Log: ctrl.Log.WithName("controllers").WithName("TLS"),
Namespace: namespace,
Configuration: directCfg,
}
if err = tlsReconciler.SetupWithManager(manager); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Namespace")
os.Exit(1)
}
if err = tlsReconciler.SetupWithManager(manager); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Namespace")
os.Exit(1)
}
tlsCert := &corev1.Secret{}
tlsCert := &corev1.Secret{}
if err = directClient.Get(ctx, types.NamespacedName{Namespace: namespace, Name: directCfg.TLSSecretName()}, tlsCert); err != nil {
setupLog.Error(err, "unable to get Capsule TLS secret")
os.Exit(1)
}
// Reconcile TLS certificates before starting controllers and webhooks
if err = tlsReconciler.ReconcileCertificates(ctx, tlsCert); err != nil {
setupLog.Error(err, "unable to reconcile Capsule TLS secret")
os.Exit(1)
if err = directClient.Get(ctx, types.NamespacedName{Namespace: namespace, Name: directCfg.TLSSecretName()}, tlsCert); err != nil {
setupLog.Error(err, "unable to get Capsule TLS secret")
os.Exit(1)
}
// Reconcile TLS certificates before starting controllers and webhooks
if err = tlsReconciler.ReconcileCertificates(ctx, tlsCert); err != nil {
setupLog.Error(err, "unable to reconcile Capsule TLS secret")
os.Exit(1)
}
}
if err = (&tenantcontroller.Manager{

View File

@@ -78,8 +78,8 @@ func (c capsuleConfiguration) TLSSecretName() (name string) {
return
}
func (c capsuleConfiguration) GenerateCertificates() bool {
annotationValue, ok := c.retrievalFn().Annotations[capsulev1alpha1.GenerateCertificatesAnnotationName]
func (c capsuleConfiguration) EnableTLSConfiguration() bool {
annotationValue, ok := c.retrievalFn().Annotations[capsulev1alpha1.EnableTLSConfigurationAnnotationName]
if ok {
value, err := strconv.ParseBool(annotationValue)

View File

@@ -19,7 +19,9 @@ const (
type Configuration interface {
ProtectedNamespaceRegexp() (*regexp.Regexp, error)
ForceTenantPrefix() bool
GenerateCertificates() bool
// EnableTLSConfiguration enabled the TLS reconciler, responsible for creating CA and TLS certificate required
// for the CRD conversion and webhooks.
EnableTLSConfiguration() bool
TLSSecretName() string
MutatingWebhookConfigurationName() string
ValidatingWebhookConfigurationName() string

View File

@@ -115,7 +115,7 @@ func (r *userMetadataHandler) OnUpdate(client client.Client, decoder *admission.
var labels, annotations map[string]string
for key, value := range newNs.GetLabels() {
if _, ok := oldNs.GetLabels()[key]; !ok {
if _, ok := oldNs.GetLabels()[key]; ok {
if labels == nil {
labels = make(map[string]string)
}
@@ -125,7 +125,7 @@ func (r *userMetadataHandler) OnUpdate(client client.Client, decoder *admission.
}
for key, value := range newNs.GetAnnotations() {
if _, ok := oldNs.GetAnnotations()[key]; !ok {
if _, ok := oldNs.GetAnnotations()[key]; ok {
if annotations == nil {
annotations = make(map[string]string)
}