SiYuan
Private knowledge workspace with a persistent filesystem, native access-code authentication and optional OIDC. The chart
uses the official docker.io/b3log/siyuan:v3.8.3 image, verified for Linux amd64, arm64 and arm.
Production contract
- Exactly one writer per workspace, enforced even for RWX claims. Deployment strategy is Recreate.
- Complete workspace persistence, including documents, configuration, history, assets and encrypted content.
- Native access code in a Kubernetes Secret, generated once and retained on Helm upgrades.
- Existing Secret and External Secrets Operator support for GitOps credentials.
- Optional native OIDC with explicit claim admission rules and Secret-backed client credentials.
- Non-root UID/GID 1000, read-only image filesystem, RuntimeDefault seccomp, dropped capabilities and no API token.
- Native boot-completion readiness, resource defaults, dual-stack Service, Ingress, Gateway API and explicit egress policies.
- Functional notebook/document tests, pod-replacement persistence checks and quiesced restore into a fresh PVC.
Install
helm repo add helmforge https://repo.helmforge.dev
helm repo update
helm install siyuan helmforge/siyuan --namespace siyuan --create-namespace
kubectl -n siyuan port-forward service/siyuan-siyuan 6806:6806
Open http://localhost:6806 and use the access code described in Helm NOTES. For remote access, configure an HTTPS hostname and a WebSocket-capable ingress or Gateway. SiYuan is a personal administrative workspace; this is not a tenant-isolated collaboration server.
Storage and availability
The default claim is 10Gi ReadWriteOnce and mounts at /siyuan/workspace. The chart keeps the generated PVC on
uninstall. Existing claims are never created or deleted by the chart. Reusing a retained claim requires
persistence.existingClaim.
persistence:
existingClaim: restored-workspace
There is no external PostgreSQL/MySQL/Redis backend. Do not scale replicas or share the workspace between independent releases. RWX does not make multiple writers safe. Recreate deliberately permits downtime during upgrades so the old process exits before the new writer starts. No HPA or PDB is provided for this singleton.
persistence.enabled: false is for disposable trials; replacement loses workspace data. A StorageClass of - requests
no dynamic provisioning. Provision a compatible PV yourself in that case. fsGroupChangePolicy OnRootMismatch avoids
repeated ownership walks where supported by the CSI driver. The chart does not run a privileged recursive chown init
container.
Authentication
The native access code is read from SIYUAN_ACCESS_AUTH_CODE. It never appears in container arguments or ConfigMaps.
Empty auth.accessCode generates a random 32-character code and retains it via Helm lookup. Existing-secret mode
bypasses generated credentials.
auth:
existingSecret: siyuan-auth
accessCodeKey: access-code
Use a Secret containing that key and avoid committing plaintext credentials. Changing an existing Secret requires a pod restart or your configured Secret-reload controller. Generated inline values change the checksum and roll the pod. Preserve the Secret for disaster recovery. The API token and the access code are different native credentials.
Local access-code login remains available when OIDC is enabled. Treat it as an administrative recovery credential and restrict access to its Secret. OIDC admission grants administration of the same workspace; it does not create isolated per-user workspaces.
Native OIDC
oidc:
enabled: true
provider: custom
issuerURL: https://identity.example.com/realms/personal
clientID: siyuan
existingSecret: siyuan-oidc
clientSecretKey: client-secret
redirectURL: https://notes.example.com/api/system/oidc/callback
claimRules:
- claim: email
operator: equals
values:
- [email protected]
- claim: email_verified
operator: equals
values:
- 'true'
Register the exact HTTPS callback in your provider. Remote HTTP callbacks are rejected upstream. Configure DNS and HTTPS egress to the issuer’s discovery, JWKS and token endpoints. Use the standard custom OIDC provider contract; provider-specific OAuth adapters are not exposed by this chart.
Rules are ANDed, with OR across values within each rule. Claims are top-level keys. equals is case-sensitive;
contains performs substring matching and is unsuitable for many exact identity restrictions. Boolean claims are
converted to strings. Empty rules are rejected unless allowAll explicitly grants the workspace to every authenticated
identity. Keep allowAll false.
The runtime fixture exercises real discovery, JWKS/RS256 validation, authorization code, PKCE, nonce and rejected identity claims. Its instant-login Node provider is disposable test infrastructure and must never be deployed as an identity service.
External Secrets
auth:
existingSecret: siyuan-auth
externalSecrets:
enabled: true
items:
- fullnameOverride: siyuan-auth
spec:
secretStoreRef:
name: production
kind: ClusterSecretStore
data:
- secretKey: access-code
remoteRef:
key: applications/siyuan/access-code
Install External Secrets Operator and the store first. The chart renders the v1 API and requires a store or per-item source reference. The target Secret must match auth.existingSecret or oidc.existingSecret. Native login in the ESO CI profile verifies actual consumption after Ready=True.
Network and exposure
The Service defaults to ClusterIP:6806. Use a dedicated root hostname; configure long-lived WebSocket support at the edge. Gateway API requires existing v1 CRDs and a Gateway controller. TLS configuration belongs to the edge; the native kernel’s local certificates are retained in workspace configuration.
Default NetworkPolicy permits HTTP from pods in the release namespace and DNS egress. It denies other outbound traffic. Set ingressFrom for the edge namespace, and add explicit webEgress/extraEgress for OIDC, cloud sync, webhooks or extension downloads. Permit all endpoints involved in your provider or sync service. Policy enforcement requires a capable CNI.
PreferDualStack can fall back to single-stack. RequireDualStack requires cluster support. Extra containers inherit the pod identity but need their own restrictive security context and resource limits. They are an integration extension, not an alternative application architecture.
Health and resources
Startup and readiness inspect native boot progress and require 100 percent completion. Liveness uses the unauthenticated version endpoint so an external sync outage does not restart a healthy workspace. Initial requests are 100m CPU and 256Mi memory, with 1 CPU/1Gi limits. Size for indexing, document count and plugins. A large workspace may require a longer startup budget and more memory.
No native Prometheus endpoint is exposed by this release. The chart does not fabricate a ServiceMonitor. Monitor workload/PVC metrics and edge availability through your existing observability stack; use authenticated behavioral checks for deeper application coverage.
Backup and recovery
Back up the complete workspace after gracefully stopping the writer, or use an application-consistent storage procedure appropriate to your platform. Copying a live SQLite file alone is unsafe and omits documents, assets and encryption state. Protect backups as sensitive: configuration includes identity and cryptographic material.
The recovery CI profile scales the writer to zero, archives the complete workspace, restores it into a fresh claim, then starts the chart using existingClaim. It verifies native login and an identical exported Unicode document. See recovery procedure.
Encrypted notebooks also require their recovery passwords and native encrypted backups. Kubernetes storage alone cannot reconstruct a lost encryption password. Keep recovery credentials separately protected and exercise restore regularly.
Upgrade
helm upgrade siyuan helmforge/siyuan -n siyuan -f production.yaml
kubectl -n siyuan rollout status deployment/siyuan-siyuan
Take a quiesced backup first. Review upstream storage-format changes and allow Recreate downtime. Helm rollback changes manifests and image, not data migrations. Restore a compatible workspace backup when an older version cannot read newer storage.
Security Scan
Security Scan: siyuan
| Framework | Score |
|---|---|
| MITRE + NSA + SOC2 | 100% |
Security posture acceptable.
Measured with Kubescape 4.0.13 against default manifests. This assesses deployment configuration, not every upstream feature or extension.
Values
See values.yaml and values.schema.json for the full contract, including scheduling and dual-stack settings.
Workspace recovery
Consistent backup
Schedule downtime, stop the Deployment and wait for the old pod to terminate. Use the same namespace and explicit context for all commands. Do not run a second SiYuan process against the claim.
kubectl -n siyuan scale deployment/siyuan-siyuan --replicas=0
kubectl -n siyuan wait --for=delete pod -l app.kubernetes.io/instance=siyuan --timeout=60s
Mount the workspace read-only in a temporary restricted maintenance pod, along with a separate writable backup volume. Use the pinned SiYuan image’s tar utility to archive the entire workspace. Store each backup under a unique name. Preserve file permissions and the full data/conf/history/storage tree, including encrypted content and identity material.
After a successful archive and integrity check, remove the maintenance pod and scale the original Deployment back to one. Replicas remain one in Helm values so a subsequent upgrade does not preserve maintenance downtime accidentally.
Restore
Provision a new empty claim of sufficient capacity. With no application writer using it, mount the archive volume and extract the entire workspace into the new claim using UID/GID 1000. Remove the maintenance pod before starting the application.
persistence:
existingClaim: siyuan-recovered
Install the same tested image version with this value and the original auth/OIDC Secret references. Verify native login, notebook listing, document export, attachments and any encrypted notebooks before routing traffic. The chart’s recovery CI follows this procedure with a quiesced archive and a fresh claim, and verifies identical Unicode document content.
Do not substitute a copy of the live SQLite index for a workspace backup. Do not expect Helm rollback to undo storage migrations. Keep recovery passwords for encrypted notebooks separately; also create native encrypted-notebook backups where appropriate.
Retention
The generated PVC is kept on release uninstall by default. Uninstalling is not a backup. Namespace deletion, underlying-volume deletion and storage-provider policies are separate operations. Configure backup encryption, off-cluster copies and retention according to your data requirements.
Complete values
# SPDX-License-Identifier: Apache-2.0
# -- Override the chart name used in resource names.
nameOverride: ''
# -- Override the complete resource name.
fullnameOverride: ''
# -- Extra resource labels; selector labels are reserved.
commonLabels: {}
# -- Exactly one writer per workspace, including RWX volumes.
replicaCount: 1
# -- Image.
image:
# -- Repository.
repository: docker.io/b3log/siyuan
# -- Verified stable image tag.
tag: v3.8.3
# -- Kubernetes pull policy.
pullPolicy: IfNotPresent
# -- Registry credentials for a private mirror.
imagePullSecrets: []
# -- Auth.
auth:
# -- Native administrative access code. Empty generates and retains 32 random alphanumeric characters.
accessCode: ''
# -- Existing access-code Secret; do not combine with an inline code.
existingSecret: ''
# -- Key containing the native access code, distinct from an API token.
accessCodeKey: access-code
# -- Native kernel HTTP listener.
server:
# -- Container listen port; Service and probes follow this value.
port: 6806
# -- Additional environment entries; chart-managed security variables cannot be overridden.
extraEnv: []
# -- Additional envFrom Secret/ConfigMap references.
envFrom: []
# -- Service Account.
serviceAccount:
# -- Create a dedicated ServiceAccount with no API permissions.
create: true
# -- Existing or overridden ServiceAccount name.
name: ''
# -- ServiceAccount annotations.
annotations: {}
# -- Automount Service Account Token.
automountServiceAccountToken: false
# -- Service.
service:
# -- Kubernetes Service type.
type: ClusterIP
# -- Service HTTP port.
port: 6806
# -- Service annotations.
annotations: {}
# -- Service IP family policy; empty uses cluster default.
ipFamilyPolicy: ''
# -- Requested address families; RequireDualStack needs a dual-stack cluster.
ipFamilies: []
# -- Ingress.
ingress:
# -- Enable Ingress. TLS is configured through ingress.tls.
enabled: false
# -- Ingress controller class; empty omits the field.
ingressClassName: ''
# -- Annotations.
annotations: {}
# -- Host/path rules; at least one explicit host is required when enabled.
hosts: []
# -- TLS host/Secret entries.
tls: []
# -- Gateway.
gatewayAPI:
# -- Render canonical Gateway API HTTPRoutes.
enabled: false
# -- Route definitions with parentRefs, hostnames, rules, labels and annotations.
httpRoutes: []
# -- External Secrets.
externalSecrets:
# -- Enabled.
enabled: false
# -- Default operator refresh interval.
refreshInterval: 1h
# -- Items.
items: []
# -- Network Policy.
networkPolicy:
# -- Enabled.
enabled: true
# -- Allowed ingress peers. Empty permits pods in the same namespace only.
ingressFrom: []
# -- Enable outbound isolation, allowing DNS and configured web ports.
egressIsolation: true
# -- DNS peers; defaults to cluster pods in any namespace, restricted to DNS ports.
dnsEgress:
- namespaceSelector: {}
# -- Explicit web destinations for OIDC, sync or plugins; empty denies web egress.
webEgress: []
# -- TCP ports allowed to the declared web destinations.
webPorts:
- 443
# -- Additional egress rules, for example internal APIs on alternate ports.
extraEgress: []
# -- Probes.
probes:
# -- Wait for native kernel boot completion before liveness starts.
startup:
# -- Enabled.
enabled: true
# -- Path.
path: /api/system/bootProgress
# -- Require native boot progress 100 before startup/readiness succeeds.
requireBootComplete: true
# -- Period Seconds.
periodSeconds: 5
# -- Timeout Seconds.
timeoutSeconds: 2
# -- Failure Threshold.
failureThreshold: 60
# -- Check local HTTP availability without requiring external services.
liveness:
# -- Enabled.
enabled: true
# -- Path.
path: /api/system/version
# -- Period Seconds.
periodSeconds: 20
# -- Timeout Seconds.
timeoutSeconds: 3
# -- Failure Threshold.
failureThreshold: 3
# -- Require complete kernel initialization before admitting traffic.
readiness:
# -- Enabled.
enabled: true
# -- Path.
path: /api/system/bootProgress
# -- Require native boot progress 100 before startup/readiness succeeds.
requireBootComplete: true
# -- Period Seconds.
periodSeconds: 10
# -- Timeout Seconds.
timeoutSeconds: 3
# -- Failure Threshold.
failureThreshold: 3
# -- Resource sizing for indexing and workspace operations; increase memory for large collections.
resources:
# -- Requests.
requests:
# -- Cpu.
cpu: 100m
# -- Memory.
memory: 256Mi
# -- Limits.
limits:
# -- Cpu.
cpu: '1'
# -- Memory.
memory: 1Gi
# -- Non-root pod identity and CSI-assisted ownership for the workspace.
podSecurityContext:
# -- Run As Non Root.
runAsNonRoot: true
# -- Run As User.
runAsUser: 1000
# -- Run As Group.
runAsGroup: 1000
# -- Fs Group.
fsGroup: 1000
# -- Fs Group Change Policy.
fsGroupChangePolicy: OnRootMismatch
# -- Seccomp Profile.
seccompProfile:
# -- Type.
type: RuntimeDefault
# -- Restricted container privileges; all configuration and assets are read-only.
securityContext:
# -- Allow Privilege Escalation.
allowPrivilegeEscalation: false
# -- Read Only Root Filesystem.
readOnlyRootFilesystem: true
# -- Capabilities.
capabilities:
# -- Drop.
drop:
- ALL
# -- Pod labels; immutable selector labels cannot be overridden.
podLabels: {}
# -- Pod annotations, e.g. for an external Secret reloader.
podAnnotations: {}
# -- Node selection constraints.
nodeSelector: {}
# -- Scheduling tolerations.
tolerations: []
# -- Pod affinity or anti-affinity.
affinity: {}
# -- Topology spreading across nodes or zones.
topologySpreadConstraints: []
# -- Scheduling priority class.
priorityClassName: ''
# -- Grace period for HTTP shutdown.
terminationGracePeriodSeconds: 30
# -- Complete workspace storage, including configuration, data, history and encrypted content.
persistence:
# -- Persist the complete workspace. Disable only for disposable tests.
enabled: true
# -- Pre-existing workspace PVC, never created or deleted by this chart.
existingClaim: ''
# -- StorageClass name; empty uses cluster default, dash disables dynamic provisioning.
storageClass: ''
# -- Workspace claim capacity.
size: 10Gi
# -- PVC access mode. Even ReadWriteMany must have exactly one application writer.
accessModes:
- ReadWriteOnce
# -- Keep the generated PVC on Helm uninstall; this does not protect against namespace deletion.
retain: true
# -- Annotations.
annotations: {}
# -- Native OIDC admission to this single administrative workspace. Local access-code login remains available.
oidc:
# -- Enable native OIDC alongside the local administrative access-code login.
enabled: false
# -- Standard custom OIDC provider contract. Provider-specific OAuth adapters are not exposed.
provider: custom
# -- Issuer URL used for discovery, JWKS and token exchange. Permit its destinations in egress policy.
issuerURL: ''
# -- Registered OIDC client identifier.
clientID: ''
# -- Inline client credential stored only in Secret; prefer existingSecret for GitOps.
clientSecret: ''
# -- Existing Secret containing the client credential; cannot be combined with inline clientSecret.
existingSecret: ''
# -- Key containing the OIDC client credential.
clientSecretKey: client-secret
# -- Scopes requested during authorization; include openid.
scopes:
- openid
- profile
- email
# -- Exact HTTPS callback ending in /api/system/oidc/callback. Register the same URL in the provider.
redirectURL: ''
# -- Grant workspace administration to every authenticated identity. Keep false and configure claimRules.
allowAll: false
# -- AND across rules, OR across values. Use equals for exact top-level claim admission.
claimRules: []
# -- Optional companion containers; set their security contexts, probes and resources explicitly.
extraContainers: []
Production example
# SPDX-License-Identifier: Apache-2.0
auth:
existingSecret: siyuan-auth
persistence:
size: 20Gi
ingress:
enabled: true
ingressClassName: traefik
hosts:
- host: notes.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: siyuan-tls
hosts:
- notes.example.com
networkPolicy:
ingressFrom:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-system
Oidc example
# SPDX-License-Identifier: Apache-2.0
# Replace documentation-only 192.0.2.0/24 with your identity-provider destinations.
auth:
existingSecret: siyuan-auth
oidc:
enabled: true
issuerURL: https://identity.example.com/realms/personal
clientID: siyuan
existingSecret: siyuan-oidc
redirectURL: https://notes.example.com/api/system/oidc/callback
claimRules:
- claim: email
operator: equals
values:
- [email protected]
- claim: email_verified
operator: equals
values:
- 'true'
networkPolicy:
webEgress:
- ipBlock:
cidr: 192.0.2.0/24
Gateway-api example
networkPolicy:
ingressFrom:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: gateway-system
gatewayAPI:
enabled: true
httpRoutes:
- parentRefs:
- name: public
namespace: gateway-system
hostnames:
- notes.example.com
External-secrets example
# SPDX-License-Identifier: Apache-2.0
auth:
existingSecret: siyuan-auth
externalSecrets:
enabled: true
items:
- fullnameOverride: siyuan-auth
spec:
secretStoreRef:
name: production
kind: ClusterSecretStore
data:
- secretKey: access-code
remoteRef:
key: applications/siyuan/access-code
Gateway API contract
Use gatewayAPI.enabled and gatewayAPI.httpRoutes[]. Set each route’s parentRefs to a shared Gateway that allows
this namespace, and configure its HTTPS listener and public hostname. Routes accept labels, annotations and rules with
matches, filters and optional backend references; omitted backends target this chart’s application Service. Ingress and
HTTPRoute resources can coexist. Verify controller conditions and public traffic before production use. See the
Gateway API documentation.