CSRF verification failed when using Seatable 2.1.0 without LetsEncrypt

Hi! I’ve been wanting to give SeaTable a try but unfortunately I can’t get it to work.

I want to use SeaTable without the automatic LetsEncrypt certificate feature because I will be proxying it through my frontend Nginx instance which already has a certificate. After following the steps in the documentation (Developer Edition - SeaTable Admin Manual) and setting up my reverse proxy configuration (see below), I get a Django CSRF validation error.

  • I’ve tried to replace http:// with https:// in dtable_web_settings.py and ccnet.conf (and restarting Seatable service or container, both ways) but it did not help.
  • I’ve tried adding mydomain in dtable_web_settings.py under a new CSRF_TRUSTED_ORIGINS option, but it did not succeed either. The Django DEBUG option also didn’t give me any substantial hints (just saying that mydomain isn’t a trusted origin).
  • I’ve also tried to use it with SEATABLE_SERVER_LETSENCRYPT=True (after removing all containers and bind mounted directories), but I hit the same issue as in Seatable 1.8 developer edition docker fresh installation broken? (where you get [...] open() "/etc/nginx/sites-enabled/default" failed [...] in the seatable logs) and I could not get it to work as per the answers (that is, making sure that it is set to True when the containers are initially created).

Thanks for any help,

Niklas

My docker-compose.yml:

version: '2.0'
services:

  memcached:
    image: memcached:1.5.6
    entrypoint: memcached -m 256
    restart: unless-stopped

  db:
    image: mariadb:10.5
    environment:
      - MYSQL_ROOT_PASSWORD=REDACTED
      - MYSQL_LOG_CONSOLE=false
    volumes:
      - ./seatable-db:/var/lib/mysql
    restart: unless-stopped

  redis:
    image: redis:5.0.7
    restart: unless-stopped

  seatable:
    image: seatable/seatable:2.1.0
    ports:
      - "127.0.0.1:12001:80"
    volumes:
      - ./seatable-data:/shared
    environment:
      - DB_HOST=db
      - DB_ROOT_PASSWD=REDACTED
      - SEATABLE_SERVER_LETSENCRYPT=False
      - SEATABLE_SERVER_HOSTNAME=mydomain
      - TIME_ZONE=Europe/Berlin
    links:
      - db
      - memcached
      - redis
    restart: unless-stopped

My reverse proxy configuration:

server {
  server_name mydomain;
  listen 80;
  listen 443 ssl http2;
  include i.d/acme.conf;

  if ($scheme = http) {
    return 302 https://$server_name$request_uri;
  }

  client_max_body_size 512M;

  location / {
    proxy_pass http://localhost:12001;
    proxy_set_header        X-Real-IP         $remote_addr;
    proxy_set_header        X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header        X-Forwarded-Host  $server_name;
    proxy_set_header        X-Forwarded-Proto $scheme;
  }
}

Hi, welcome to the SeaTable Forum.

Quick question before diving into it: Are you using “mydomain” or is it just a placeholder for the forum?

Correct this to “12001:80” I have never seen this notation and I am pretty sure docker-compose cannot interpret it. In fact, how should it? You are separating IP address and port the same way you separate two different ports.

Also have a look at this article: Install SeaTable Enterprise on your own server behind a web server

Now, upon closer inspection, I also realize that you use a custom docker-compose.yml

If you start from the official YAML file and adjust that to your needs, you stand a much better chance of succeeding.

@Karlheinz mydomain is a placeholder for my actual domain. :slight_smile: Sorry for not clarifying that earlier.

Also in reply to @rdb, I am able to visit the SeaTable login page so I don’t think the ports configuration is an issue either. (What I get with 127.0.0.1:12001:80 is that the port 12001 is only addressable in the local network and not from the outside, e.g. via mydomain:12001).

I did actually start from the official Docker Compose file, but made modifications to fit my needs, which there are

  • Updated MYSQL_ROOT_PASSWORD
  • Update db and seatable volume sto use bind a relative host directory (./seatable-db and ./seatable-data respectively)
  • Remove container_name (to use default container names)
  • Add restart: unless-stopped
  • Update the 80:80 port mapping and remove the 443:443 port mapping (since I did not want to use the images’ LetsEncrypt feature, that means I also won’t be proxying the HTTPS port but the HTTP port via NGinx)
  • Use links instead of network (although I might revert this, I was only using it because Portainer.io did not support it properly, but I tested it also with the network method and it doesn’t change the behaviour I’m observng)
  • Update the TIME_ZONE, DB_ROOT_PASSWORD and SEATABLE_SERVER_HOSTNAME\

However, I did just try it again from scratch with the official docker file (just changing the volumes and port mapping this time) and this time tried it without using a reverse proxy (so I mapped the port as 12001:80 instead of 127.0.0.1:12001:80) and accessing it as mydomain:12001 works (I can log in as the superuser I created before).

However, reverse-proxying that very same running instance results in the same CSRF issue (I’ve enabled DEBUG = True in dtable_web_settings.py to enable this detailed error output):

So I believe that pins it down to an issue related to the reverse proxy. I’m already configuring Nginx to pass the usual headers for reverse proxying along to the SeaTable upstream (e.g. X-Forwarded-For etc.). The CSRF token is also set as expected.

image

I can’t seem to find a reliable source for this same issue (i.e., “CSRF verification failed in Django behind reverse proxy”), so I wonder if it is something that needs to be fixed in SeaTable itself :thinking:

… Aaaaand while I write this, I try again to set CSRF_TRUSTED_ORIGINS in dtable_web_settings.py and now it works :laughing: So I must have done something wrong when I tried this solution for the first time.

CSRF_TRUSTED_ORIGINS = ['mydomain']
# .. rest of dtable_web_settings.py

More details about CSRF_TRUSTED_ORIGINS in the Django documentation:


Edit: To complete the solution, to make Websockets work, the Nginx reverse proxy needs to be configured accordingly, so here the full Nginx reverse proxy config I’m using


server {
  server_name mydomain;
  listen 80;
  listen 443 ssl http2;
  include i.d/acme.conf;

  if ($scheme = http) {
    return 302 https://$server_name$request_uri;
  }

  client_max_body_size 16M;

  location / {
    proxy_pass http://localhost:12002;
    proxy_set_header        X-Real-IP         $remote_addr;
    proxy_set_header        X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header        X-Forwarded-Host  $server_name;
    proxy_set_header        X-Forwarded-Proto $scheme;
  }

  location /socket.io {
    proxy_pass http://localhost:12002;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header Host $host;
  }
}
1 Like

Great! Pleased to hear it works now!

Here is another small useful tip that could help to overcome problems with CSRF. I had to use this when I tried to install a self hosted SeaTable Enterprise 2.7 without let’s encrypt - so with http only.

A possible solution could be to add the following to the dtable_web_settings.py. This should disable the security protection of the CSRF Token.

SESSION_COOKIE_SAMESITE = None
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SAMESITE = None
CSRF_COOKIE_SECURE = False

Important: this tip is no general recommendation. Most of the time a CSRF Error (403 Error) is the result of a misconfiguration that could be fixed. Use this approach only temporary and try to find the root cause of the problem.

Best regards
Christoph