Monofor Pam Operator
Monofor PAM Operator — Kubernetes Integration Guide
Monofor PAM Operator is a native Kubernetes operator that automatically synchronises
secrets from Monofor PAM Vault into Kubernetes Secret objects. Unlike the webhook approach, PAM Operator connects directly to the PAM API — no middleware or webhook configuration is required.
Prerequisites: A valid license and a running Monofor PAM instance are required before
proceeding.
How It Works
MonoSecret (CRD)
│
▼
MonoStore (CRD) ──► Monofor PAM API
│
▼
Kubernetes Secret
MonoStore holds the PAM connection URL and the Access Key reference.
MonoSecret defines which vaults to fetch and which Kubernetes Secret to write into.
The operator reconciles on creation, update, and periodically based on
refreshInterval.
Step 1 — Install the CRDs
Apply the two CustomResourceDefinitions that define MonoStore and MonoSecret:
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.20.1
name: monostores.pam.monofor.com
spec:
group: pam.monofor.com
names:
kind: MonoStore
listKind: MonoStoreList
plural: monostores
shortNames:
- mstore
singular: monostore
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .spec.provider.monoforPAM.url
name: URL
type: string
- jsonPath: .status.conditions[?(@.type=='Ready')].status
name: Ready
type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: MonoStore defines the connection configuration (URL and credentials)
for Monofor PAM Vault.
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
spec:
properties:
provider:
properties:
monoforPAM:
properties:
auth:
properties:
accessKeySecretRef:
properties:
key:
type: string
name:
type: string
namespace:
type: string
required:
- key
- name
type: object
type: object
url:
type: string
required:
- auth
- url
type: object
required:
- monoforPAM
type: object
required:
- provider
type: object
status:
properties:
conditions:
items:
properties:
lastTransitionTime:
format: date-time
type: string
message:
maxLength: 32768
type: string
observedGeneration:
format: int64
minimum: 0
type: integer
reason:
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
enum:
- "True"
- "False"
- Unknown
type: string
type:
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
type: object
type: object
served: true
storage: true
subresources:
status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.20.1
name: monosecrets.pam.monofor.com
spec:
group: pam.monofor.com
names:
kind: MonoSecret
listKind: MonoSecretList
plural: monosecrets
shortNames:
- msec
singular: monosecret
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .spec.monoStoreRef.name
name: Store
type: string
- jsonPath: .status.conditions[?(@.type=='Ready')].status
name: Ready
type: string
- jsonPath: .status.refreshTime
name: Last Synced
type: date
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: MonoSecret defines which vaults to fetch from Monofor PAM and
which Kubernetes Secret to write the resulting data into.
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
spec:
properties:
data:
items:
properties:
remoteRef:
properties:
property:
type: string
vaultID:
type: string
required:
- vaultID
type: object
secretKey:
type: string
required:
- remoteRef
- secretKey
type: object
type: array
dataFrom:
items:
properties:
extract:
properties:
property:
type: string
vaultID:
type: string
required:
- vaultID
type: object
required:
- extract
type: object
type: array
monoStoreRef:
properties:
name:
type: string
required:
- name
type: object
refreshInterval:
default: 1h
type: string
rollout:
properties:
enabled:
type: boolean
targets:
items:
properties:
kind:
enum:
- Deployment
- DaemonSet
- StatefulSet
type: string
name:
type: string
required:
- kind
- name
type: object
type: array
type: object
target:
properties:
creationPolicy:
default: Owner
enum:
- Owner
- Merge
- None
type: string
deletionPolicy:
default: Delete
enum:
- Delete
- Retain
type: string
name:
type: string
template:
properties:
metadata:
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
type: object
type:
type: string
type: object
type: object
required:
- monoStoreRef
- target
type: object
status:
properties:
conditions:
items:
properties:
lastTransitionTime:
format: date-time
type: string
message:
maxLength: 32768
type: string
observedGeneration:
format: int64
minimum: 0
type: integer
reason:
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
enum:
- "True"
- "False"
- Unknown
type: string
type:
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
refreshTime:
format: date-time
type: string
secretDataHash:
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}
kubectl apply -f crds.yaml
# Verify
kubectl get crds | grep monofor
# Expected:
# monosecrets.pam.monofor.com
# monostores.pam.monofor.com
Step 2 — Deploy the Operator
Apply the namespace, RBAC resources, and the operator Deployment in order.
Namespace
apiVersion: v1
kind: Namespace
metadata:
name: pam-operator-system
labels:
app.kubernetes.io/name: pam-operator
RBAC
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: pam-operator
namespace: pam-operator-system
labels:
app.kubernetes.io/name: pam-operator
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pam-operator-manager-role
rules:
- apiGroups: ["pam.monofor.com"]
resources: ["monosecrets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["pam.monofor.com"]
resources: ["monosecrets/status"]
verbs: ["get", "update", "patch"]
- apiGroups: ["pam.monofor.com"]
resources: ["monosecrets/finalizers"]
verbs: ["update"]
- apiGroups: ["pam.monofor.com"]
resources: ["monostores"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch"]
- apiGroups: ["apps"]
resources: ["deployments", "daemonsets", "statefulsets"]
verbs: ["get", "list", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: pam-operator-manager-rolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: pam-operator-manager-role
subjects:
- kind: ServiceAccount
name: pam-operator
namespace: pam-operator-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pam-operator-leader-election-role
namespace: pam-operator-system
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: pam-operator-leader-election-rolebinding
namespace: pam-operator-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: pam-operator-leader-election-role
subjects:
- kind: ServiceAccount
name: pam-operator
namespace: pam-operator-system
Deployment
Replace <VERSION> with the image version you have pushed to the registry
(e.g. cr.monofor.com/monofor/pam-operator:1.0.0).
apiVersion: apps/v1
kind: Deployment
metadata:
name: pam-operator
namespace: pam-operator-system
labels:
app.kubernetes.io/name: pam-operator
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: pam-operator
template:
metadata:
labels:
app.kubernetes.io/name: pam-operator
spec:
serviceAccountName: pam-operator
securityContext:
runAsNonRoot: true
containers:
- name: manager
image: cr.monofor.com/monofor/pam-operator:<VERSION>
imagePullPolicy: IfNotPresent
command: ["/manager"]
args:
- "--leader-elect=true"
- "--metrics-bind-address=:8080"
- "--health-probe-bind-address=:8081"
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
ports:
- name: metrics
containerPort: 8080
protocol: TCP
- name: health
containerPort: 8081
protocol: TCP
livenessProbe:
httpGet:
path: /healthz
port: 8081
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /readyz
port: 8081
initialDelaySeconds: 5
periodSeconds: 10
resources:
limits:
cpu: 200m
memory: 128Mi
requests:
cpu: 10m
memory: 64Mi
terminationGracePeriodSeconds: 10
kubectl apply -f namespace.yaml
kubectl apply -f rbac.yaml
kubectl apply -f deployment.yaml
# Verify the pod is running
kubectl -n pam-operator-system get pods
# Expected:
# NAME READY STATUS RESTARTS AGE
# pam-operator-xxxx-xxxx 1/1 Running 0 30s
# Follow operator logs
kubectl -n pam-operator-system logs -f deployment/pam-operator
Step 3 — Create the PAM Access Key Secret
In Monofor PAM, navigate to Access Keys and generate a new Access Key.
Copy the generated UUID.
Store the Access Key in a Kubernetes Secret in the same namespace where you will create
your MonoStore:
apiVersion: v1
kind: Secret
metadata:
name: pam-access-key
namespace: default
type: Opaque
stringData:
key: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
kubectl apply -f pam-access-key.yaml
Step 4 — Create a MonoStore
A MonoStore defines the connection to your Monofor PAM instance. Replace <MONOPAM-URL>
with your PAM API base URL.
apiVersion: pam.monofor.com/v1alpha1
kind: MonoStore
metadata:
name: monofor-pam
namespace: default
spec:
provider:
monoforPAM:
url: "https://<MONOPAM-URL>"
auth:
accessKeySecretRef:
name: pam-access-key
key: key
namespace: default
kubectl apply -f monostore.yaml
# Verify
kubectl get mstore -n default
Parameter | Description |
|---|---|
| Base URL of your Monofor PAM API (e.g. |
| Name of the Kubernetes Secret containing the Access Key |
| Key within the Secret whose value is the Access Key UUID |
| Namespace of the Secret (defaults to MonoStore namespace if omitted) |
Step 5 — Create a MonoSecret
A MonoSecret tells the operator which PAM vaults to fetch and where to write the result.
The Vault ID (UUID) is found in the Monofor PAM UI on each vault item's detail page.
Reading a SecretText vault
apiVersion: pam.monofor.com/v1alpha1
kind: MonoSecret
metadata:
name: app-secret
namespace: default
spec:
monoStoreRef:
name: monofor-pam
target:
name: app-secret-k8s
refreshInterval: "1h"
data:
- secretKey: SECRET_VALUE
remoteRef:
vaultID: "<VAULT-ID>"
property: "SecretText"
Reading UserName and Password from a UserNamePassword vault
apiVersion: pam.monofor.com/v1alpha1
kind: MonoSecret
metadata:
name: database-credentials
namespace: default
spec:
monoStoreRef:
name: monofor-pam
target:
name: database-credentials-secret
refreshInterval: "1h"
data:
- secretKey: username
remoteRef:
vaultID: "<VAULT-ID>"
property: "UserName"
- secretKey: password
remoteRef:
vaultID: "<VAULT-ID>"
property: "Password"
Reading all fields at once with dataFrom
dataFrom fetches all fields from a vault in a single operation. Each field is written as
a separate key in the Kubernetes Secret — no prefix is added.
apiVersion: pam.monofor.com/v1alpha1
kind: MonoSecret
metadata:
name: pki-credentials
namespace: default
spec:
monoStoreRef:
name: monofor-pam
target:
name: pki-credentials-secret
refreshInterval: "24h"
dataFrom:
- extract:
vaultID: "<VAULT-ID>"
Reading an OTP code
Note: OTP codes are time-based. Use a short
refreshInterval(e.g."30s") to keep
the Kubernetes Secret in sync with the current code.
apiVersion: pam.monofor.com/v1alpha1
kind: MonoSecret
metadata:
name: otp-code
namespace: default
spec:
monoStoreRef:
name: monofor-pam
target:
name: otp-code-secret
refreshInterval: "30s"
data:
- secretKey: OTP_CODE
remoteRef:
vaultID: "<VAULT-ID>"
property: "Code"
Parameter reference
Parameter | Description |
|---|---|
| Name of the MonoStore to use for PAM connectivity |
| Name of the Kubernetes Secret to create or update |
|
|
|
|
| How often to re-sync from PAM. Valid formats: |
| Key name in the target Kubernetes Secret |
| UUID of the vault item in Monofor PAM |
| Specific field to read from the vault (see table below) |
| UUID of the vault item — all fields are fetched and written as separate keys |
Vault item types and available properties
Vault Item Type | Available Properties |
|---|---|
SecretText |
|
UserName and Password |
|
UserName and Password (with Domain) |
|
PKI |
|
OTP |
|
Step 6 — Verify the Sync
# Check sync status
kubectl get msec -n default
# Expected output:
# NAME STORE READY LAST SYNCED AGE
# database-credentials monofor-pam True 2s 5s
# Inspect the synced Kubernetes Secret
kubectl get secret database-credentials-secret -n default -o yaml
# Decode a specific value
kubectl get secret database-credentials-secret -n default \
-o jsonpath='{.data.username}' | base64 -d
Step 7 — Test with a Deployment (Optional)
The following example deploys an nginx pod that reads credentials from the synced Secret
as environment variables.
apiVersion: apps/v1
kind: Deployment
metadata:
name: monofor-test
namespace: default
labels:
app: monofor-test
spec:
replicas: 1
selector:
matchLabels:
app: monofor-test
template:
metadata:
labels:
app: monofor-test
spec:
containers:
- name: monofor-test
image: nginx
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: database-credentials-secret
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: database-credentials-secret
key: password
kubectl apply -f monofor-test.yaml
# Wait for the pod to start
kubectl get pods -n default
# Verify the environment variables are injected
kubectl exec -it -n default deploy/monofor-test -- /bin/sh -c 'env | grep DB_'
# Cleanup
kubectl delete -f monofor-test.yaml
Step 8 — Auto-Rollout on Secret Change (Optional)
PAM Operator can automatically restart workloads when a PAM-synced secret changes.
The restart is triggered only when the secret data actually changes — not on everyrefreshInterval tick. No external tools (e.g. Stakater Reloader) are required.
Mode A — Explicit targets in the MonoSecret spec
spec:
rollout:
enabled: true
targets:
- kind: Deployment
name: my-app
- kind: DaemonSet
name: my-daemon
- kind: StatefulSet
name: my-stateful
Mode B — Workload annotation
Add the annotation to the top-level metadata of any Deployment, DaemonSet, or
StatefulSet. The value must match the target Kubernetes Secret name
(MonoSecret.spec.target.name):
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: default
annotations:
pam.monofor.com/reload-on: "database-credentials-secret"
Important: The annotation must be placed on the workload's top-level
metadata.annotations, not insidespec.template.metadata.annotations.
Both modes are evaluated on every sync. A workload matched by either mode is restarted once
per change (deduplication is handled automatically). Supported workload kinds: Deployment,DaemonSet, StatefulSet.
# Confirm a rollout was triggered
kubectl get deployment my-app -n default \
-o jsonpath='{.spec.template.metadata.annotations.pam\.monofor\.com/restartedAt}'
# Watch the rollout
kubectl rollout status deployment/my-app -n default