How-To: Auth with LDAP + LemonLDAP + Nextcloud + SSO (environment variable)

While i have been working quite some time with nextcloud I had to master setting it up with environment variable based authentication in the last week. Since I could not find a lot of of examples how to configure my specific setup and I have to write an internal documentation anyway, I’d like to share my findings.

Just a heads up, if you are not relying on environmental variable based auth and use lemonldap or any other backend which provides SAML or some other modern SSO Service, those are easier to integrate, at least in my opinion.

Requirements:
• Working lemonldap setup
• lemon ldap handler running on server
• nginx
• ssl certificates for domain(s)
• ldap backend (could be configured without)
• php-fpm (could be configured without)

I work at a german student union and we already have a working it infrastructure. We were (atm still are) using seafile as a file sharing service, but we are missing a groupware solution for some time (calendar, collaborative working, and so on). After some discussion the decision was made to switch to nextcloud. Especially, because it allows us to integrate it into our current setup.

Our users are managed by FreeIPA (basically OpenLDAP with kerberos and some more features). We use lemonldap as authentication and authorization backend. Most of our infrastructure is debian based and we have some workstations which are integrated with kerberos. This means users have to type their password once when logging in and never again while using this workstation (at least that’s the idea – works mostly :D).

We use nginx in combination with lemonldap to authenticate based on environment variables. While this allows us to share authentication and authorization, at least in theory, we decided only to use the authentication and provide authorization parameters via the ldap module.

Configuring ldap is well documented and was set up pretty quickly. The SSO & SAML authentication app allows to authenticate via SAML or an environment variable.

What was unclear is how to set nginx up to provide auth.
We ended up with two nginx files. One for the lemonldap-handler-reload using the fqdn name of the server and the nextcloud config using the fqdn of the service for nextcloud. (cloud.domain in our case). It is possible to use only one domain.

Our handler-reload.conf looks like this. It allows access only from our lemonldap-ng main server. Everything else is redirected to our cloudserver.

handler-reload.conf

server {
    listen 443 ssl;
    server_name servername.domain;
    include /etc/nginx/conf.d/ssl.conf;

    ssl_certificate     /path/to/ssl/cert/servername.domain/fullchain.pem;
    ssl_certificate_key /path/to/ssl/cert/servername.domain/privkey.pem;

    # lemonldap-ng reload
    location = /reload {
        allow $lemonldap-ip;
        deny all;

        # FastCGI configuration
        include /etc/nginx/fastcgi_params;
        fastcgi_pass unix:/var/run/llng-fastcgi-server/llng-fastcgi.sock;
        fastcgi_param LLTYPE reload;
    }

    # redirect to cloud in all other cases
    location / {
        return 301 https://servicename.domain;
    }
}

The nginx nextcloud config is based on the usual nextcloud-nginx config file.
We have to add a = /lmauth location for the authentication, but we also have to add an location at which point nextcloud uses this authentication. For this we add the location ~ ^/apps/user_saml/:

location = /lmauth {
        internal;
        include /etc/nginx/fastcgi_params;
    
        # increase fcgi response header buffer size, because all ldap groups
        # are included here, which can get quite large.
        fastcgi_buffers 16 16k; 
        fastcgi_buffer_size 32k;
        fastcgi_pass unix:/var/run/llng-fastcgi-server/llng-fastcgi.sock;

        # Drop post datas
        fastcgi_pass_request_body  off;
        fastcgi_param CONTENT_LENGTH "";

        # Keep original hostname
        fastcgi_param HOST $http_host;

        # Keep original request (LLNG server will received /llauth)
        fastcgi_param X_ORIGINAL_URI  $request_uri;
    }

and

location ~ ^/apps/user_saml/ {
        # lemonldap protection
        auth_request /lmauth;
        auth_request_set $lmremote_user $upstream_http_lm_remote_user;
        auth_request_set $lmlocation $upstream_http_location;
        error_page 401 $lmlocation;

        include /etc/nginx/fastcgi_params;
        fastcgi_param SCRIPT_NAME "";
        fastcgi_param PATH_INFO $uri;
        include /etc/lemonldap-ng/nginx-lua-headers.conf;
        fastcgi_param REMOTE_USER $http_remote_user;
        try_files $uri $uri/ /index.php$request_uri;
    }

After this has been set up, you need to reload nginx.
If not already configured the server has to be added as a handler in lemonldap (GP → Config Reload → Reload URLs).
And the service has to be added to virtual hosts with an access rule and the exported headers defined.
In our case the access rule is just one rule “$ldapgroups =~ /\b(ldapgroup1|group2|and so on)\b/”
The exported header: “key = REMOTE_USER, value = $uid” (should match with your usersettings in ldap module).

Last but not least configure your SSO&SAML app in Nextcloud to (only) allow login via enviroment variable which is “REMOTE_USER” in our case.

Basically that’s it.

In case you do not want to use the ldap app for authorization (or don’t use ldap) you have to add your authorization parameters to exported headers and configure them in your SSO&SAML authentication app.

Final result for nginx nextcloud.conf

upstream php-handler {
    #server 127.0.0.1:9000;
    server unix:/var/run/php/php8.2-fpm.sock;
}

server {
    listen 80;
    listen [::]:80;
    server_name servicename.domain;

    # Enforce HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443      ssl http2;
    listen [::]:443 ssl http2;
    server_name servicename.domain;

    # Use Mozilla's guidelines for SSL/TLS settings
    # https://mozilla.github.io/server-side-tls/ssl-config-generator/
    ssl_certificate     /path/to/ssl/cert/servicename.domain/fullchain.pem;
    ssl_certificate_key /path/to/ssl/cert/servicename.domain/privkey.pem;

    # HSTS settings
    # WARNING: Only add the preload option once you read about
    # the consequences in https://hstspreload.org/. This option
    # will add the domain to a hardcoded list that is shipped
    # in all major browsers and getting removed from this list
    # could take several months.
    add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;" always;

    # set max upload size and increase upload timeout:
    client_max_body_size 512M;
    client_body_timeout 300s;
    fastcgi_buffers 64 4K;

    # Enable gzip but do not remove ETag headers
    gzip on;
    gzip_vary on;
    gzip_comp_level 4;
    gzip_min_length 256;
    gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
    gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/wasm application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;

    # Pagespeed is not supported by Nextcloud, so if your server is built
    # with the `ngx_pagespeed` module, uncomment this line to disable it.
    #pagespeed off;

    # HTTP response headers borrowed from Nextcloud `.htaccess`
    add_header Referrer-Policy                      "no-referrer"       always;
    add_header X-Content-Type-Options               "nosniff"           always;
    add_header X-Download-Options                   "noopen"            always;
    add_header X-Frame-Options                      "SAMEORIGIN"        always;
    add_header X-Permitted-Cross-Domain-Policies    "none"              always;
    add_header X-Robots-Tag                         "noindex, nofollow" always;
    add_header X-XSS-Protection                     "1; mode=block"     always;

    # Remove X-Powered-By, which is an information leak
    fastcgi_hide_header X-Powered-By;

    # Path to the root of your installation
    root /var/www/nextcloud;

    # Specify how to handle directories -- specifying `/index.php$request_uri`
    # here as the fallback means that Nginx always exhibits the desired behaviour
    # when a client requests a path that corresponds to a directory that exists
    # on the server. In particular, if that directory contains an index.php file,
    # that file is correctly served; if it doesn't, then the request is passed to
    # the front-end controller. This consistent behaviour means that we don't need
    # to specify custom rules for certain paths (e.g. images and other assets,
    # `/updater`, `/ocm-provider`, `/ocs-provider`), and thus
    # `try_files $uri $uri/ /index.php$request_uri`
    # always provides the desired behaviour.
    index index.php index.html /index.php$request_uri;

    # lemonldap-ng auth
    location = /lmauth {
        internal;
        include /etc/nginx/fastcgi_params;
    
        # increase fcgi response header buffer size, because all ldap groups
        # are included here, which can get quite large.
        fastcgi_buffers 16 16k; 
        fastcgi_buffer_size 32k;
        fastcgi_pass unix:/var/run/llng-fastcgi-server/llng-fastcgi.sock;

        # Drop post datas
        fastcgi_pass_request_body  off;
        fastcgi_param CONTENT_LENGTH "";

        # Keep original hostname
        fastcgi_param HOST $http_host;

        # Keep original request (LLNG server will received /llauth)
        fastcgi_param X_ORIGINAL_URI  $request_uri;
    }

    # Rule borrowed from `.htaccess` to handle Microsoft DAV clients
    location = / {
        if ( $http_user_agent ~ ^DavClnt ) {
            return 302 /remote.php/webdav/$is_args$args;
        }
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    # Make a regex exception for `/.well-known` so that clients can still
    # access it despite the existence of the regex rule
    # `location ~ /(\.|autotest|...)` which would otherwise handle requests
    # for `/.well-known`.
    location ^~ /.well-known {
        # The rules in this block are an adaptation of the rules
        # in `.htaccess` that concern `/.well-known`.

        location = /.well-known/carddav { return 301 /remote.php/dav/; }
        location = /.well-known/caldav  { return 301 /remote.php/dav/; }

        location /.well-known/acme-challenge    { try_files $uri $uri/ =404; }
        location /.well-known/pki-validation    { try_files $uri $uri/ =404; }

        # Let Nextcloud's API for `/.well-known` URIs handle all other
        # requests by passing them to the front-end controller.
        return 301 /index.php$request_uri;
    }

    # Rules borrowed from `.htaccess` to hide certain paths from clients
    location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)  { return 404; }
    location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console)                { return 404; }


    # nginx protection with lemonldap and environment variable
    location ~ ^/apps/user_saml/ {
        # lemonldap protection
        auth_request /lmauth;
        auth_request_set $lmremote_user $upstream_http_lm_remote_user;
        auth_request_set $lmlocation $upstream_http_location;
        error_page 401 $lmlocation;

        include /etc/nginx/fastcgi_params;
        fastcgi_param SCRIPT_NAME "";
        fastcgi_param PATH_INFO $uri;
        include /etc/lemonldap-ng/nginx-lua-headers.conf;
        fastcgi_param REMOTE_USER $http_remote_user;
        try_files $uri $uri/ /index.php$request_uri;
    }
    

    # Ensure this block, which passes PHP files to the PHP process, is above the blocks
    # which handle static assets (as seen below). If this block is not declared first,
    # then Nginx will encounter an infinite rewriting loop when it prepends `/index.php`
    # to the URI, resulting in a HTTP 500 error response.
    location ~ \.php(?:$|/) {
        # Required for legacy support
        rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[ms]-provider\/.+|.+\/richdocumentscode\/proxy) /index.php$request_uri;
        fastcgi_split_path_info ^(.+?\.php)(/.*)$;
        set $path_info $fastcgi_path_info;
        try_files $fastcgi_script_name =404;

        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $path_info;
        fastcgi_param HTTPS on;
        fastcgi_param REMOTE_USER $http_remote_user;

        fastcgi_param modHeadersAvailable true;         # Avoid sending the security headers twice
        fastcgi_param front_controller_active true;     # Enable pretty urls
        fastcgi_pass php-handler;

        fastcgi_intercept_errors on;
        fastcgi_request_buffering off;

        fastcgi_max_temp_file_size 0;
    }

    location ~ \.(?:css|js|svg|gif|png|jpg|ico|wasm|tflite|map)$ {
        try_files $uri /index.php$request_uri;
        expires 6M;         # Cache-Control policy borrowed from `.htaccess`
        access_log off;     # Optional: Don't log access to assets

        location ~ \.wasm$ {
            default_type application/wasm;
        }
    }

    location ~ \.woff2?$ {
        try_files $uri /index.php$request_uri;
        expires 7d;         # Cache-Control policy borrowed from `.htaccess`
        access_log off;     # Optional: Don't log access to assets
    }

    # Rule borrowed from `.htaccess`
    location /remote {
        return 301 /remote.php$request_uri;
    }

    location / {
        try_files $uri $uri/ /index.php$request_uri;
    }
}
1 Like