Block desktop client access at the HTTP layer

Server administrators can reject Nextcloud Desktop requests at a web server, reverse proxy, or web application firewall (WAF) by matching the HTTP User-Agent header.

This can be useful as a compatibility guard or an operational policy, for example to temporarily block all desktop clients or require a particular client release.

Warning

This is not a security boundary. The client controls the User-Agent header and can change or omit it. Do not use this rule as the only control when access must be prevented against a deliberately modified client.

Identify desktop client requests

The standard desktop client sends a header with this shape:

Mozilla/5.0 (Macintosh) mirall/34.0.2 (Nextcloud, macos-25.5.0 ClientArchitecture: arm64 OsArchitecture: arm64)

The stable marker is mirall/; the value immediately after it is the desktop client version. The current client constructs this value in Utility::userAgentString().

Use a case-insensitive match for:

mirall/

Apply the rule to every request reaching the Nextcloud virtual host. Blocking only one OCS or WebDAV endpoint does not block desktop access because the client uses multiple OCS, WebDAV, login-flow, status, and provisioning endpoints.

Nextcloud Server’s USER_AGENT_CLIENT_DESKTOP matcher also recognizes the legacy marker csyncoC/. To include those clients, replace mirall/ with (mirall|csyncoC)/ in regular-expression rules, or add a second substring rule where the configuration format does not support regular expressions.

Choose the policy

Block every standard desktop client

Match the case-insensitive substring:

mirall/

Block releases older than 34.0.2

Web-server regular expressions do not compare arbitrary version strings as semantic versions. The following case-insensitive expression is deliberately written for one minimum release, 34.0.2:

mirall/(([0-9]|[12][0-9]|3[0-3])[.]|34[.]0[.](0|1)([^0-9]|$))

It has these results:

Desktop version

Result

3.14.3

blocked

33.0.0

blocked

34.0.0

blocked

34.0.1

blocked

34.0.2

allowed by this rule

34.1.0

allowed by this rule

35.0.0

allowed by this rule

The expression assumes normal release versions with three numeric components. Do not change only the version text in the expression. Build and test a new set of numeric ranges whenever the minimum release changes. A WAF that can extract the version and compare three integer fields is preferable when the minimum changes frequently.

Return 403 Forbidden. Do not return 401 Unauthorized: by HTTP semantics, that status asks the client to authenticate and obscures that this is an administrative policy. The desktop client is not guaranteed to display a custom response body, so the administrator should communicate the policy separately to users.

The examples below assume that Nextcloud has its own virtual host at cloud.example.com. If the host serves other applications, additionally scope the rule to the Nextcloud URL prefix.

nginx

Define one map in the http context. Enable exactly one of the two match entries:

http {
    map $http_user_agent $block_nextcloud_desktop {
        default 0;

        # Policy A: block every standard desktop client.
        ~*mirall/ 1;

        # Policy B: instead block releases older than 34.0.2.
        # ~*mirall/(([0-9]|[12][0-9]|3[0-3])[.]|34[.]0[.](0|1)([^0-9]|$)) 1;
    }

    server {
        server_name cloud.example.com;

        if ($block_nextcloud_desktop) {
            return 403 "Nextcloud desktop client access is disabled.\n";
        }

        # The existing Nextcloud configuration follows here.
    }
}

The map directive is valid only in the http context. The resulting variable is then checked in the Nextcloud server block. A conditional containing only return is sufficient here; no URI rewrite is involved.

Validate and reload the configuration with the commands appropriate to the installation, commonly:

nginx -t
systemctl reload nginx

See the nginx documentation for map and return.

Apache HTTP Server 2.4

Place SetEnvIfNoCase in the Nextcloud virtual host and deny requests carrying the resulting environment variable. Enable exactly one SetEnvIfNoCase line:

<VirtualHost *:443>
    ServerName cloud.example.com

    # Policy A: block every standard desktop client.
    SetEnvIfNoCase User-Agent "mirall/" block_nextcloud_desktop

    # Policy B: instead block releases older than 34.0.2.
    # SetEnvIfNoCase User-Agent "mirall/(([0-9]|[12][0-9]|3[0-3])[.]|34[.]0[.](0|1)([^0-9]|$))" block_nextcloud_desktop

    <Location "/">
        <RequireAll>
            Require all granted
            Require not env block_nextcloud_desktop
        </RequireAll>
    </Location>

    # The existing Nextcloud configuration follows here.
</VirtualHost>

For a subdirectory installation, such as https://cloud.example.com/nextcloud, use <Location "/nextcloud"> instead. The modules mod_setenvif and mod_authz_core must be enabled.

Validate and reload the configuration with the commands appropriate to the installation, commonly:

apachectl configtest
systemctl reload apache2

The Apache documentation describes this SetEnvIfNoCase and Require blocking pattern and also notes that a client can circumvent a User-Agent rule.

Caddy 2

Place the matcher and response before the existing Nextcloud handler. Enable exactly one matcher line:

cloud.example.com {
    # Policy A: block every standard desktop client.
    @blockedNextcloudDesktop header_regexp User-Agent (?i)mirall/

    # Policy B: instead block releases older than 34.0.2.
    # @blockedNextcloudDesktop header_regexp User-Agent (?i)mirall/(([0-9]|[12][0-9]|3[0-3])[.]|34[.]0[.](0|1)([^0-9]|$))

    respond @blockedNextcloudDesktop "Nextcloud desktop client access is disabled." 403

    # The existing Nextcloud file_server, php_fastcgi, or reverse_proxy
    # directives follow here.
}

Caddy uses RE2 regular expressions. The example therefore uses capturing groups rather than PCRE-only constructs.

Validate and reload the configuration with the commands appropriate to the installation, commonly:

caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy

See the Caddy documentation for header_regexp and respond.

HAProxy

Add the ACL and deny action to the HTTP frontend that receives Nextcloud traffic. Enable exactly one blocked_nextcloud_desktop ACL:

frontend https_frontend
    bind :443 ssl crt /etc/haproxy/certs/cloud.example.com.pem

    acl nextcloud_host hdr(host) -i cloud.example.com cloud.example.com:443

    # Policy A: block every standard desktop client.
    acl blocked_nextcloud_desktop req.hdr(User-Agent) -m sub -i mirall/

    # Policy B: instead block releases older than 34.0.2.
    # acl blocked_nextcloud_desktop req.hdr(User-Agent) -m reg -i mirall/(([0-9]|[12][0-9]|3[0-3])[.]|34[.]0[.](0|1)([^0-9]|$))

    http-request deny deny_status 403 if nextcloud_host blocked_nextcloud_desktop

    # The existing backend selection follows here.

Keep the host ACL when this frontend serves more than Nextcloud. Validate and reload the configuration with the commands appropriate to the installation, commonly:

haproxy -c -f /etc/haproxy/haproxy.cfg
systemctl reload haproxy

See the HAProxy documentation for ACLs and http-request deny.

Verify the rule

Test from a system that reaches the same proxy or WAF path as real users. The following probes do not require Nextcloud credentials:

# Must be 403 for both policies.
curl -sS -o /dev/null -w '%{http_code}\n' \
    -A 'Mozilla/5.0 (Linux) mirall/34.0.1 (Nextcloud, linux)' \
    https://cloud.example.com/status.php

# Must not be blocked by the minimum-version policy. It is still 403 when
# the block-all policy is active.
curl -sS -o /dev/null -w '%{http_code}\n' \
    -A 'Mozilla/5.0 (Linux) mirall/34.0.2 (Nextcloud, linux)' \
    https://cloud.example.com/status.php

# Must not be blocked by either desktop rule.
curl -sS -o /dev/null -w '%{http_code}\n' \
    -A 'Mozilla/5.0 (compatible; policy-check)' \
    https://cloud.example.com/status.php

Also verify these operational details:

  1. The rule covers all Nextcloud paths and HTTP methods, including WebDAV methods such as PROPFIND, PUT, and DELETE.

  2. The origin server cannot be reached through another hostname or IP address that bypasses the proxy or WAF.

  3. Proxy layers preserve the original User-Agent header until the component enforcing the rule has evaluated it.

  4. Access and WAF logs record blocked requests, the matched policy, and the returned status.

  5. A rollback consists of disabling the match rule and reloading the configuration; it does not require changing Nextcloud itself.