An external scan covers only part of operating OpenCloud securely. This guide covers the remaining work: identity-provider policies, audit logging, firewall rules, host maintenance, backups and user guidance.
Use these controls alongside scheduled scanning. The final section explains what regular scans can detect after the deployment is in place.
Every setting below is quoted from OpenCloud's own documentation and links to it. OpenCloud moves fast; when a variable here disagrees with the linked page, the linked page is right, and an issue is welcome.
- Running OpenCloud in a secure infrastructure
- The shape of a defensible deployment
- 1. Put a real identity provider in front
- 2. Turn the audit log on, then read it
- 3. Firewall it properly
- 4. Underneath it all: the host and the data
- 5. What the people using it should know
- 6. Where this scanner fits: continuous monitoring
- Checklist
- Where to go next
- Trademarks and affiliation
The shape of a defensible deployment
internet
│
443/tcp only
│
┌─────────▼─────────┐
│ reverse proxy │ TLS, HSTS, security headers,
│ (nginx/Caddy/…) │ rate limits, TRACE refused
└─────────┬─────────┘
│ private network, no published ports
┌──────────────┼──────────────┐
│ │ │
┌──────▼─────┐ ┌──────▼─────┐ ┌──────▼──────┐
│ OpenCloud │ │ identity │ │ audit │
│ :9200 │ │ provider │ │ service │
└──────┬─────┘ └────────────┘ └──────┬──────┘
│ │
┌─────▼──────┐ ┌──────▼──────┐
│ storage │ │ log sink │ off-host, append-only
└────────────┘ └─────────────┘
Expose the intended public entry points, enforce your sign-in policy at the identity provider and send audit records to a separate system. The sections below describe each part.
1. Put a real identity provider in front
Why, before how
OpenCloud includes an identity provider (idp) and identity management (idm) for a
working initial installation. An external provider is useful when an organization needs
shared account lifecycle, multifactor authentication and consistent policies across
services:
- Second factors. An external provider gives you TOTP, WebAuthn or passkeys across every application you run, configured once.
- Lifecycle. Somebody leaves and you disable one account, not one account per service.
- Session policy. Lockout after failed attempts, session lifetime, device trust, conditional access - all of it belongs in the provider.
- Audit. Sign-in attempts are recorded in the place that handles sign-ins, which is where an investigator will look for them.
This scanner reports which provider it found under identityProvider, and
softens the HTTP Basic authentication finding from medium to low when an
external one is detected - see
Authentication.
Step by step, for each of the three: this section is the summary and the reasoning. Putting an identity provider in front of OpenCloud, step by step is the tutorial - installing each provider, the four OpenCloud clients every one of them needs, verifying it worked, and moving an instance that already has accounts.
What OpenCloud needs, whichever provider you pick
The variables are the same for all three; only the issuer URL and the way you create the client differ. From OpenCloud's external IdP guide:
| Variable | What it does |
|---|---|
OC_OIDC_ISSUER |
The provider's issuer URL, e.g. https://id.example.com/realms/opencloud |
OC_EXCLUDE_RUN_SERVICES |
Add idp so the built-in provider does not start |
PROXY_OIDC_ACCESS_TOKEN_VERIFY_METHOD |
jwt, so tokens are verified against the provider's published keys rather than by asking it on every request |
PROXY_OIDC_REWRITE_WELLKNOWN |
true, so clients discovering /.well-known/openid-configuration on the OpenCloud host are pointed at the real provider |
PROXY_USER_OIDC_CLAIM |
The claim that identifies a user, commonly preferred_username |
PROXY_USER_CS3_CLAIM |
The OpenCloud attribute it is matched against, commonly username |
PROXY_AUTOPROVISION_ACCOUNTS |
true creates an account on first sign-in |
PROXY_ROLE_ASSIGNMENT_DRIVER |
oidc to take roles from a claim, default to give everybody the same role |
PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM |
Which claim carries them; roles by default |
GRAPH_ASSIGN_DEFAULT_USER_ROLE |
false when roles come from the provider, or every user quietly gets the default one as well |
Review account provisioning and role assignment together:
Autoprovisioning is an access-control decision. With
PROXY_AUTOPROVISION_ACCOUNTS=true, anybody your provider will authenticate
gets an OpenCloud account the first time they visit. That is right when the
provider's OpenCloud application is restricted to a group, and wrong when the
provider authenticates your whole organisation - restrict it on the provider
side, not by leaving autoprovisioning off and creating accounts by hand.
Role assignment from a claim needs the default role switched off. Setting
PROXY_ROLE_ASSIGNMENT_DRIVER=oidc while leaving
GRAPH_ASSIGN_DEFAULT_USER_ROLE=true is the misconfiguration that gives
everyone a role you did not intend.
Keycloak
The step-by-step Keycloak tutorial is the whole job; what follows is the shape of it.
The most common choice where an organisation already runs one. Create a realm (or reuse yours), then a client:
- Client type OpenID Connect. Register the web, desktop, Android and iOS clients separately, using the client IDs in the linked provider tutorial.
- Public client with PKCE - OpenCloud's clients are public clients and
cannot keep a secret. Set Proof Key for Code Exchange to
S256. - Valid redirect URIs must include the desktop client's loopback
(
http://127.0.0.1:*andhttp://localhost:*) and your web address. - Issuer:
https://id.example.com/realms/opencloud.
For roles, add a User Client Role mapper putting the client roles into a
roles claim, then set PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM=roles. Give the
realm a password policy and require OTP for the administrator role at minimum.
Authentik
The step-by-step Authentik tutorial is the whole job; what follows is the shape of it.
This repository already ships an Authentik stack, though for a different purpose - it protects the scan service's own MCP endpoint, not OpenCloud. The provider configuration is the same shape:
- Create an OAuth2/OpenID Provider, authorization flow
implicit consentfor a trusted internal application. - Client type public, with PKCE required.
- Set the redirect URIs as above, using regex for the loopback range.
- The issuer is
https://id.example.com/application/o/<application-slug>/. The trailing slash matters. - Bind the application to a group so that not every Authentik user gets an
OpenCloud account, then turn
PROXY_AUTOPROVISION_ACCOUNTSon.
authentik/blueprints/ in this repository is a
worked example of provisioning a provider from a file rather than by clicking,
which is worth copying whatever you are configuring.
Authelia
The step-by-step Authelia tutorial is the whole job; what follows is the shape of it.
The lightest of the three, and a good fit where the reverse proxy is already
doing forward authentication. Authelia's OpenID Connect provider is configured
in configuration.yml rather than a UI:
- Register a client under
identity_providers.oidc.clientswithpublic: true,require_pkce: trueandpkce_challenge_method: S256. - Scopes
openid,profile,email,groups. - The issuer is
https://auth.example.com. - Map groups to OpenCloud roles with
PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM=groups.
Authelia's access control rules are the natural place to require two factors for OpenCloud specifically:
access_control:
rules:
- domain: opencloud.example.com
policy: two_factor
Basic authentication is the hole in all of this
None of the above applies to a client that cannot speak OpenID Connect -
CalDAV and CardDAV calendars, WebDAV mounts, backup jobs. Those authenticate
with HTTP Basic, and PROXY_ENABLE_BASIC_AUTH=true re-opens a path that
bypasses your provider and every second factor on it.
Leave it false if nothing needs it. If something does, the answer is app
tokens, not account passwords: what can be replayed is then revocable and is
never the credential your identity provider protects. This scanner reports
basicAuthDisabled as medium, or low when it can see an external provider,
precisely because the trade is sometimes deliberate - see
Authentication.
2. Turn the audit log on, then read it
The audit service does not run by default
OpenCloud has an audit service, and it is not in the default run set. Nothing is recording who shared what until you start it:
# Add it to the services that run, alongside the default set.
OC_ADD_RUN_SERVICES=audit
It records three things worth having:
- File system operations - create, delete, move, including the trash bin and versioning.
- User management - accounts created and deleted.
- Sharing - user and group shares, public links, permission changes, and calls to the sharing API from clients.
That third category is the one that matters most here. This scanner can tell
you that public links may be created without a password
(publicLinkPasswordEnforced); only the audit log can tell you
that somebody created 4,000 of them last Tuesday.
Configure it with the variables from the audit service reference:
| Variable | Default | What to set it to |
|---|---|---|
AUDIT_LOG_TO_CONSOLE |
true |
Leave on when a container log driver ships stdout somewhere |
AUDIT_LOG_TO_FILE |
false |
true if you would rather write a file |
AUDIT_FILEPATH |
(empty) | Required when logging to a file |
AUDIT_FORMAT |
json |
Keep json; the minimal format is for reading by eye, not by a collector |
AUDIT_LOG_LEVEL |
error |
Raise it, or you will record almost nothing |
OC_EVENTS_ENDPOINT |
127.0.0.1:9233 |
The event broker the service reads from |
AUDIT_EVENTS_AUTH_USERNAME / _PASSWORD |
(empty) | Set both once the broker is not on loopback |
AUDIT_EVENTS_ENABLE_TLS |
false |
true when the broker is reached over a network |
AUDIT_LOG_LEVEL defaulting to error is the detail that catches people out:
starting the service and leaving the level alone produces a log that is
technically running and practically empty.
Getting the log off the box
An audit log stored only on the machine being audited is evidence an attacker can edit. Ship it:
# docker-compose fragment: hand stdout to the host's journal, which a
# collector then forwards off the machine.
services:
opencloud:
logging:
driver: journald
options:
tag: opencloud
Whatever collector you use - Loki, Elasticsearch, a syslog server, a managed service - the properties to insist on are the same: append-only from the sender's point of view, on a different trust domain from the instance, with its own retention. A collector that OpenCloud's own credentials can delete from is not much better than a local file.
What to actually alert on
Alerting on everything means alerting on nothing. A short list that has earned its place:
- A public link created with no password or no expiry, especially on a space that is not usually shared.
- Share permissions widened on anything, particularly to a group.
- An account created or given an administrative role outside your normal provisioning process.
- Bulk download or deletion - a volume of file operations from one account well above its own baseline.
- Sign-in anomalies, which come from your identity provider rather than from OpenCloud: impossible travel, a spike in failures, a first sign-in from a new country.
Retention, and the law
An audit log of a file service is a record of who accessed which documents, which in most jurisdictions is personal data with a retention limit rather than something to keep forever. Decide the period deliberately, write it down, and make the collector enforce it. If you are subject to GDPR, this log is in scope for your record of processing activities.
3. Firewall it properly
The ports, and which of them belong on the internet
| Port | What it is | Exposed to the internet? |
|---|---|---|
| 443 | The reverse proxy | Yes - this one, and only this one |
| 80 | Plain HTTP | Only to redirect to 443, or not at all |
| 9200 | OpenCloud's own proxy service | No. Publishing it lets clients bypass your TLS and header policy entirely |
| 9233 | The events broker (NATS) | No |
| 9205, 9141, 9124, 9134, 9239 | Per-service debug listeners - metrics, a config dump, optionally pprof | No. They bind to 127.0.0.1 by default; reaching one from outside means a container port mapping published it |
| 22 | SSH | Management network or VPN only, never the open internet |
This scanner checks the bottom three rows from the outside -
backendPortClosed, debugPort:* and debugEndpoint:*, described in
Exposed paths and debug endpoints. It is checking your firewall
for you, from the one vantage point that counts.
A host firewall that works with Docker
The usual mistake is worth stating plainly: Docker writes its own iptables
rules and they are evaluated before UFW's. A container started with
-p 9200:9200 is reachable from the internet no matter what ufw status
says. Two ways out, and you want one of them:
Publish to loopback only. The simplest fix, and it needs no firewall at all:
services:
opencloud:
ports:
# Not "9200:9200" - that binds 0.0.0.0.
- "127.0.0.1:9200:9200"
Better still, publish nothing and let the reverse proxy reach OpenCloud over a Docker network by service name. A port that is not published cannot be misconfigured.
Or make Docker respect the host firewall. In /etc/docker/daemon.json:
{
"iptables": true,
"ip-forward": true
}
and then filter in DOCKER-USER, which is the one chain Docker leaves for
you:
# Everything reaching a container from outside must come via the proxy.
iptables -I DOCKER-USER -i eth0 -p tcp --dport 9200 -j DROP
The nftables equivalent, if that is your generation:
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iif lo accept
tcp dport { 80, 443 } accept
tcp dport 22 ip saddr 10.0.0.0/8 accept
}
}
Whichever you use, verify from somewhere else rather than believing the
config. nmap -Pn -p 9200,9205,9233 opencloud.example.com from off the host,
or simply run this scanner, which probes exactly those ports:
check-opencloud-security --host opencloud.example.com --check-hardening --debug
Egress matters too
Inbound rules are the ones people write. Outbound rules are the ones that limit what a compromise can do - exfiltration, a reverse shell, joining a botnet. An OpenCloud host needs remarkably little: DNS, NTP, the ACME directory if it issues its own certificates, your package mirror, and whatever storage or mail backend you have deliberately configured. Default to denying the rest.
4. Underneath it all: the host and the data
Briefly, because none of it is OpenCloud-specific and all of it is load-bearing:
- Unattended security updates on the host, and a real update process for the OpenCloud release itself. This scanner grades the release you are on (lifecycle); it cannot install anything.
- Full-disk encryption on whatever the storage lives on, so that a decommissioned or stolen disk is not a data breach.
- Backups you have restored from. A backup nobody has tested is a hypothesis. Keep one copy offline or on write-once storage - ransomware looks for the backup first.
- Least privilege for the service account. The systemd units in
contrib/systemd/show the pattern:DynamicUser=yes,ProtectSystem=strict,NoNewPrivileges=yes, an emptyCapabilityBoundingSet=. Runsystemd-analyze security <unit>on yours. - Separate the reverse proxy from OpenCloud, on different hosts or at least different containers, so that a proxy compromise is not immediately a storage compromise.
5. What the people using it should know
Most real incidents at a file service are not exploits. Somebody shares the wrong folder with a public link, or reuses a password that was in a breach dump. That is a documentation and defaults problem, not a patching problem.
For everybody with an account
- A public link is a password. Anyone who has the URL has the data - forwarded, pasted into a ticket, or sitting in a mail archive. Put a password on it and set an expiry.
- Check what you are sharing before you share it. Sharing a parent folder shares everything below it, including what gets added later.
- Enrol a second factor, and prefer a passkey or a hardware key over TOTP.
- App passwords are for apps. Your calendar client gets its own revocable token; it never gets your account password.
- Removing a share is not the same as un-sending a file. Assume anything shared has been downloaded.
- Report a mistaken share immediately. The window in which an administrator can revoke a link and read the audit log is short, and nobody is in trouble for reporting it fast.
For administrators
- Review shares periodically. Public links accumulate; almost none of them are ever deliberately deleted.
- Have an offboarding runbook that covers the identity provider, the app tokens, and the shares that person created.
- Know your instance's normal. The alert list above only works against a baseline.
- Write down who to call. An incident at 03:00 is not the time to discover nobody knows who owns the storage.
6. Where this scanner fits: continuous monitoring
What a scheduled scan catches that a one-off audit does not
A security review is a photograph. Infrastructure is a film. Everything on this page can be true on Monday and false on Thursday, and the ways that happens are mundane rather than dramatic:
- A certificate expires, or renews to one that does not cover every name.
- A reverse proxy is reconfigured for an unrelated service and stops
sending
Strict-Transport-Security, or starts answeringTRACE. - Somebody publishes a debug port while chasing a performance problem and does not unpublish it.
- A release goes end of life, which is a change in the world rather than a change in your deployment - the instance that was fully supported last month now receives no security fixes, and nothing on your host changed to tell you.
- An advisory is published for the version you are running.
- A new deployment is stood up from a copied compose file that still publishes 9200.
Running this plugin on a schedule turns each of those into an alert on the day it happens, from outside the instance, which is the same vantage point an attacker has. That is the argument for continuous monitoring in one sentence: the gap between a deployment breaking and somebody noticing is where incidents live, and the only thing that shortens it is something that looks every few minutes.
A monitoring setup that is worth having
Start here, then read Scheduling or Icinga2 / Nagios for your platform:
check-opencloud-security \
--host opencloud.example.com \
--check-hardening \
--baseline /var/lib/check-opencloud-security/baseline.json \
--warn-on-new \
--webhook-url https://hooks.example.com/opencloud \
--webhook-on warning
Four choices in there, each earning its place:
--check-hardeningincludes the headers and hardening measures, not only the rating.--baselinewith--warn-on-newalerts on what changed rather than on the accepted state of the world. An instance with one finding you have consciously decided to live with stays quiet until a second one appears - see Reporting only what changed.- A webhook so the alert reaches a human rather than a dashboard nobody opens.
- Findings you accept are waived explicitly, with
--ignore-hardening, which keeps them in the result document and in the report while taking them out of the alert. A waiver is a decision with a name on it, not a silenced check - see Accepting a finding you are not going to fix.
For a fleet, Checking a fleet of instances covers one configuration file per instance and keeping the waivers honest across all of them. For graphs and long-run trends, Prometheus and Grafana - the rating as a time series is a surprisingly good summary to put in front of people who do not read alerts.
What it deliberately will not tell you
Being clear about this is what makes the rest of the report trustworthy:
- Nothing behind a login. The scan never authenticates, so it sees what an anonymous visitor sees and nothing more. Your permission model, your space layout and the contents of your shares are all invisible to it.
- Nothing about your identity provider's configuration. It detects that one is there and names the vendor; whether you require a second factor is between you and the provider.
- Nothing about your audit log. Whether the service is running, whether anyone reads it, and whether it leaves the host are all outside what an HTTP scan can observe.
- Nothing about your firewall's rules, only about their effect on the handful of ports it probes.
- No exploitation. It never tries a payload, never guesses a password, and the one credential probe it does make uses only the demo passwords OpenCloud publishes in its own documentation.
What the scan deliberately does not answer is the full version of this list.
Checklist
Print it, argue with it, cross off what does not apply:
- [ ] Only 443 (and 80, redirecting) reachable from the internet
- [ ] 9200 and every
92xxdebug port unreachable from outside - verified from another host, not from the config - [ ] Outbound traffic restricted to what the instance actually needs
- [ ] An external identity provider handles sign-in
- [ ] Second factor required, at minimum for administrators
- [ ]
PROXY_ENABLE_BASIC_AUTH=false, or app tokens issued for the clients that need it - [ ]
GRAPH_ASSIGN_DEFAULT_USER_ROLE=falseif roles come from a claim - [ ] Autoprovisioning scoped by a group on the provider side
- [ ]
OC_ADD_RUN_SERVICES=auditandAUDIT_LOG_LEVELraised aboveerror - [ ] Audit log shipped off the host, with a deliberate retention period
- [ ] Alerts defined for public links, share widening and role changes
- [ ] TLS from a public CA, renewing automatically, with a CAA record
- [ ] Security headers set at the proxy - see reverse proxies
- [ ]
OC_CORS_ALLOW_ORIGINSnarrowed from its*default - [ ] Public links require a password and an expiry
- [ ] Backups exist, leave the host, and have been restored from
- [ ] Host patched automatically; OpenCloud release inside its support window
- [ ] This check runs on a schedule, with a baseline, alerting a human
Where to go next
| Page | Why |
|---|---|
| Reverse proxies | The nginx, Apache, Caddy, Traefik and HAProxy configuration behind most of section 3 |
| TLS and certificates | Every transport check, and what a good certificate looks like |
| Exposed paths and debug endpoints | What the firewall section is checked against |
| Authentication | The identity-provider and Basic-auth findings in detail |
| Public link sharing | The sharing policy your users are working within |
| Scheduling | systemd timers and cron for the monitoring in section 6 |
| Checking a fleet of instances | Once there is more than one |
| Prometheus and Grafana | The rating as a time series |
| What OpenCloud is | Background, if you arrived here from ownCloud or Nextcloud |