Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# To re-generate a bundle for another specific version without changing the standard setup, you can:
# - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2)
# - use environment variables to overwrite this value (e.g export VERSION=0.0.2)
VERSION ?= 0.2.1
VERSION ?= 0.3.0

# CHANNELS define the bundle channels used in the bundle.
# Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable")
Expand Down Expand Up @@ -104,6 +104,7 @@ $(HELMIFY): $(LOCALBIN)
helm: manifests kustomize helmify ## Generate helm chart using helmify.
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default | $(HELMIFY)
./hack/add-ca-cert-to-helm.sh


.PHONY: fmt
Expand Down
74 changes: 64 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# dvls-kubernetes-operator
:warning: **This operator is a work in progress, expect breaking changes between releases** :warning:

Operator to sync Devolutions Server `Credential Entry - Username / Password` entries as Kubernetes Secrets
Operator to sync Devolutions Server `Credential Entry` entries as Kubernetes Secrets

## Description
This operator uses the defined custom resource DvlsSecret which manages its own Kubernetes Secret and will keep itself up to date at a defined interval (every minute by default).
Expand All @@ -13,17 +13,15 @@ The following Environment Variables can be used to configure the operator :
- `DEVO_OPERATOR_DVLS_APPID` (required) - DVLS Application ID
- `DEVO_OPERATOR_DVLS_APPSECRET` (required) - DVLS Application Secret
- `DEVO_OPERATOR_REQUEUE_DURATION` (optional) - Entry/Secret resync interval (default 60s). Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
- `SSL_CERT_FILE` (optional) - Path to a custom CA certificate file for DVLS servers with self-signed certificates. This is automatically set by the Helm chart when `instanceSecret.caCert` is provided.

A sample of the custom resource can be found [here](https://github.com/Devolutions/dvls-kubernetes-operator/blob/master/config/samples/dvls_v1alpha1_dvlssecret.yaml).
The entry ID can be fetched by going in the entry properties, `Advanced -> Session ID`.

### Devolutions Server configuration
We recommend creating an [Application ID](https://helpserver.devolutions.net/webinterface_applications.html?q=application) specifically to be used with the Operator that has [minimal access to a vault](https://helpserver.devolutions.net/vaults_applications.html?q=application) that only contains the secrets to be synchronized.

Only `Credential Entry - Username / Password` entries are supported at the moment. The following entry data is available per secret :
- entry name
- username
- password
Only `Credential Entry` entries are supported at the moment. The available entry data will depend on the `Credential Entry` type.

### Kubernetes configuration
Since this operator uses Kubernetes Secrets, it is recommended that you follow [best practices](https://kubernetes.io/docs/concepts/security/secrets-good-practices/) surrounding secrets, especially [encryption at rest](https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/).
Expand All @@ -33,17 +31,73 @@ You’ll need a Kubernetes cluster to run against. You can use [KIND](https://si
**Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows).

### Helm Chart
An Helm Chart is available to simplify installation, just add our helm chart repository, create a `values.yaml` from [the default values](https://github.com/Devolutions/dvls-kubernetes-operator/blob/master/chart/values.yaml) as a baseline and update values as necessary. Run `helm install` and add your `values.yaml`.
The following values should be updated from your `values.yaml`
- controllerManager.manager.env.devoOperatorDvlsBaseuri
- controllerManager.manager.env.devoOperatorDvlsAppid
- instanceSecret.secret
A Helm Chart is available to simplify installation. Add the Devolutions Helm chart repository, create a `values.yaml` from [the default values](https://github.com/Devolutions/dvls-kubernetes-operator/blob/master/chart/values.yaml) as a baseline, and update values as necessary.

#### Required Configuration
The following values **must** be configured in your `values.yaml`:
- `controllerManager.manager.env.devoOperatorDvlsBaseuri` - Your DVLS server URL (e.g., `https://dvls.example.com`)
- `controllerManager.manager.env.devoOperatorDvlsAppid` - Application ID from your DVLS server
- `instanceSecret.secret` - Application Secret from your DVLS server

#### Optional Configuration
- `instanceSecret.caCert` - Custom CA certificate for self-signed DVLS servers (see below)
- `controllerManager.manager.env.devoOperatorRequeueDuration` - How often to sync secrets (default: `60s`)

#### Basic Example Configuration

Create a `values.yaml` file with your DVLS configuration:

```yaml
controllerManager:
manager:
env:
devoOperatorDvlsAppid: "00000000-0000-0000-0000-000000000000"
devoOperatorDvlsBaseuri: "https://dvls.example.com"
devoOperatorRequeueDuration: "60s"

instanceSecret:
secret: "your-app-secret-here"
```

#### Installation
```sh
helm repo add devolutions-helm-charts https://devolutions.github.io/helm-charts
helm repo update
helm install dvls-kubernetes-operator devolutions-helm-charts/dvls-kubernetes-operator --values values.yaml
```

#### Using a Custom CA Certificate

If your DVLS server uses a self-signed certificate (common in test/development environments), you need to provide the CA certificate so the operator can establish a trusted TLS connection.

**When to use this:**
- Testing with self-signed certificates
- Internal CA certificates not in the system trust store
- Development/staging environments with custom PKI

**Configuration:**

Add the CA certificate content to your `values.yaml`:

```yaml
controllerManager:
manager:
env:
devoOperatorDvlsAppid: "00000000-0000-0000-0000-000000000000"
devoOperatorDvlsBaseuri: "https://dvls.example.com"
devoOperatorRequeueDuration: "60s"

instanceSecret:
secret: "your-app-secret"
# Add your CA certificate here (PEM format)
caCert: |
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKZ...
(your CA certificate content)
...
-----END CERTIFICATE-----
```

### Running on the cluster
1. Install Instances of Custom Resources:

Expand Down
4 changes: 2 additions & 2 deletions chart/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ type: application
# 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.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.2.1
version: 0.3.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "0.2.1"
appVersion: "0.3.0"
14 changes: 14 additions & 0 deletions chart/templates/ca-cert-secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{{- if .Values.instanceSecret.caCert }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "chart.fullname" . }}-ca-cert
labels:
app.kubernetes.io/component: manager
app.kubernetes.io/created-by: dvls-kubernetes-operator
app.kubernetes.io/part-of: dvls-kubernetes-operator
control-plane: controller-manager
{{- include "chart.labels" . | nindent 4 }}
stringData:
ca.crt: {{ .Values.instanceSecret.caCert | quote }}
{{- end }}
23 changes: 23 additions & 0 deletions chart/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ spec:
env:
- name: KUBERNETES_CLUSTER_DOMAIN
value: {{ quote .Values.kubernetesClusterDomain }}
{{- if .Values.instanceSecret.caCert }}
- name: SSL_CERT_FILE
value: /etc/dvls-ca-cert/ca.crt
{{- end }}
image: {{ .Values.controllerManager.kubeRbacProxy.image.repository }}:{{ .Values.controllerManager.kubeRbacProxy.image.tag
| default .Chart.AppVersion }}
name: kube-rbac-proxy
Expand Down Expand Up @@ -73,6 +77,10 @@ spec:
name: {{ include "chart.fullname" . }}-instance-secret
- name: KUBERNETES_CLUSTER_DOMAIN
value: {{ quote .Values.kubernetesClusterDomain }}
{{- if .Values.instanceSecret.caCert }}
- name: SSL_CERT_FILE
value: /etc/dvls-ca-cert/ca.crt
{{- end }}
image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag
| default .Chart.AppVersion }}
livenessProbe:
Expand All @@ -92,7 +100,22 @@ spec:
}}
securityContext: {{- toYaml .Values.controllerManager.manager.containerSecurityContext
| nindent 10 }}
{{- if .Values.instanceSecret.caCert }}
volumeMounts:
- name: dvls-ca-cert
mountPath: /etc/dvls-ca-cert
readOnly: true
{{- end }}
securityContext: {{- toYaml .Values.controllerManager.podSecurityContext | nindent
8 }}
serviceAccountName: {{ include "chart.fullname" . }}-controller-manager
terminationGracePeriodSeconds: 10
{{- if .Values.instanceSecret.caCert }}
volumes:
- name: dvls-ca-cert
secret:
secretName: {{ include "chart.fullname" . }}-ca-cert
items:
- key: ca.crt
path: ca.crt
{{- end }}
2 changes: 1 addition & 1 deletion chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ controllerManager:
devoOperatorRequeueDuration: 60s
image:
repository: devolutions/dvls-kubernetes-operator
tag: 0.2.1
tag: 0.3.0
resources:
limits:
memory: 128Mi
Expand Down
2 changes: 1 addition & 1 deletion config/manager/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ kind: Kustomization
images:
- name: controller
newName: devolutions/dvls-kubernetes-operator
newTag: 0.2.1
newTag: 0.3.0
111 changes: 88 additions & 23 deletions controllers/dvlssecret_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func (r *DvlsSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return ctrl.Result{}, fmt.Errorf("failed to get DvlsSecret object, %w", err)
}

if dvlsSecret.Status.Conditions == nil || len(dvlsSecret.Status.Conditions) == 0 || dvlsSecret.Status.EntryModifiedDate.IsZero() {
if len(dvlsSecret.Status.Conditions) == 0 || dvlsSecret.Status.EntryModifiedDate.IsZero() {
meta.SetStatusCondition(&dvlsSecret.Status.Conditions, v1.Condition{Type: statusAvailableDvlsSecret, Status: v1.ConditionUnknown, Reason: "Reconciling"})
dvlsSecret.Status.EntryModifiedDate = v1.Date(0001, time.January, 1, 1, 1, 1, 1, time.UTC)
if err := r.Status().Update(ctx, dvlsSecret); err != nil {
Expand All @@ -102,15 +102,6 @@ func (r *DvlsSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return ctrl.Result{}, nil
}

if entry.Type != string(dvls.ServerConnectionCredential) || entry.SubType != string(dvls.ServerConnectionSubTypeDefault) {
log.Error(err, "entry type not supported, only username/password entries are supported", "entryId", dvlsSecret.Spec.EntryID, "entryType", entry.Type, "entrySubType", entry.SubType)
meta.SetStatusCondition(&dvlsSecret.Status.Conditions, v1.Condition{Type: statusDegradedDvlsSecret, Status: v1.ConditionTrue, Reason: "Reconciling", Message: "Entry type not supported, only username/password entries are supported"})
if err := r.Status().Update(ctx, dvlsSecret); err != nil {
log.Error(err, "Failed to update DvlsSecret status")
}
return ctrl.Result{}, nil
}

kSecret := &corev1.Secret{}
err = r.Get(ctx, req.NamespacedName, kSecret)
if err != nil && !apierrors.IsNotFound(err) {
Expand All @@ -130,19 +121,9 @@ func (r *DvlsSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request)
}, nil
}

defaultData, ok := entry.GetCredentialDefaultData()
if !ok {
return ctrl.Result{}, fmt.Errorf(
"failed to extract credential data for entry ID %s: unsupported or unexpected entry type (type: %s, subtype: %s)",
dvlsSecret.Spec.EntryID, entry.Type, entry.SubType)
}

secretMap := make(map[string]string)
secretMap["entry-id"] = entry.Id
secretMap["entry-name"] = entry.Name
secretMap["username"] = defaultData.Username
if defaultData.Password != "" {
secretMap["password"] = defaultData.Password
secretMap, err := setSecretMap(entry)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to set secret map, %w", err)
}

if kSecretNotFound {
Expand Down Expand Up @@ -210,3 +191,87 @@ func (r *DvlsSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
Owns(&corev1.Secret{}).
Complete(r)
}

func setSecretMap(entry dvls.Entry) (map[string]string, error) {
secretMap := make(map[string]string)
secretMap["entry-id"] = entry.Id
secretMap["entry-name"] = entry.Name

switch entry.SubType {
case dvls.EntryCredentialSubTypeDefault:
if data, ok := entry.GetCredentialDefaultData(); ok {
if data.Username != "" {
secretMap["username"] = data.Username
}
if data.Password != "" {
secretMap["password"] = data.Password
}
if data.Domain != "" {
secretMap["domain"] = data.Domain
}
}

case dvls.EntryCredentialSubTypeAccessCode:
if data, ok := entry.GetCredentialAccessCodeData(); ok {
if data.Password != "" {
secretMap["password"] = data.Password
}
}

case dvls.EntryCredentialSubTypeApiKey:
if data, ok := entry.GetCredentialApiKeyData(); ok {
if data.ApiId != "" {
secretMap["api-id"] = data.ApiId
}
if data.ApiKey != "" {
secretMap["api-key"] = data.ApiKey
}
if data.TenantId != "" {
secretMap["tenant-id"] = data.TenantId
}
}

case dvls.EntryCredentialSubTypeAzureServicePrincipal:
if data, ok := entry.GetCredentialAzureServicePrincipalData(); ok {
if data.ClientId != "" {
secretMap["client-id"] = data.ClientId
}
if data.ClientSecret != "" {
secretMap["client-secret"] = data.ClientSecret
}
if data.TenantId != "" {
secretMap["tenant-id"] = data.TenantId
}
}

case dvls.EntryCredentialSubTypeConnectionString:
if data, ok := entry.GetCredentialConnectionStringData(); ok {
if data.ConnectionString != "" {
secretMap["connection-string"] = data.ConnectionString
}
}

case dvls.EntryCredentialSubTypePrivateKey:
if data, ok := entry.GetCredentialPrivateKeyData(); ok {
if data.Username != "" {
secretMap["username"] = data.Username
}
if data.Password != "" {
secretMap["password"] = data.Password
}
if data.PrivateKey != "" {
secretMap["private-key"] = data.PrivateKey
}
if data.PublicKey != "" {
secretMap["public-key"] = data.PublicKey
}
if data.Passphrase != "" {
secretMap["passphrase"] = data.Passphrase
}
}
default:
return nil, fmt.Errorf("unsupported credential subtype: %s", entry.SubType)
}

return secretMap, nil
}
Loading