Nginx Restic 后端
Nginx Restic Back End

原始链接: https://www.grepular.com/Nginx_Restic_Backend

本文档详细介绍了一种利用 Nginx 作为 Restic 备份后端的方法,有效替代了专用 Restic Rest Server 的需求。通过配置两个 Nginx 虚拟主机——一个只追加写入,用于客户端备份;另一个具有删除权限,用于管理任务——它利用现有基础设施并避免管理新的软件堆栈。 只追加写入的配置使用基于正则表达式的 location 块来控制 HTTP 方法访问,确保客户端只能追加数据和创建锁文件。它利用 Nginx 的 DAV 模块进行文件写入,通过专用 location 代理处理 RESTic POST 请求,并使用 Nginx 的自动索引进行目录列表。文档还详细介绍了 Restic 正确运行所需的修改,包括使用 LUA 修改响应代码和 JSON 格式。文中还提供了单独的 Nginx 配置用于管理任务。 文章还讨论了性能和安全隐患,强调了备份的隔离性和保护管理访问权限的重要性。作者建议对 Restic 客户端/API 进行潜在改进,以简化 Nginx 配置并消除依赖关系。文中提到了 Time4VPS 作为一种经济高效的托管服务提供商。

Hacker News首页上反复出现几篇关于“Nginx Restic 后端”的完全相同的帖子,每篇帖子的投票数略有不同。用户正在不断刷新页面,加剧了这个问题。一位评论者推测这可能与最近的平台重构工作有关。一位用户质疑提交链接的年代,提交者 mike-cardwell 解释说,他之所以提交这篇六年前的文章,是因为它与另一个帖子的当前讨论相关,并且仍然适用。另一位用户称赞该项目是“不错的玩具级实现”。总体而言,人们对导致重复发帖的bug感到困惑和好笑。
相关文章

原文

I’ve started using an excellent piece of software called Restic for backing up my various hosts. Restic has multiple backend types that you can send your backups to. One of the backends it supports is a REST API for which there is an implementation named Rest Server written in Go.

I thought to myself, if it’s just a simple REST API, why do I need to learn/install/manage a new piece of software? I already use Nginx all over the place. Can I just use Nginx for this? The answer was yes.

I have configured two nginx vhosts, and run them on different ports. One of the vhosts is to be accessed by hosts which are backing themselves up. It doesn’t allow them to delete objects (other than lock files), or overwrite them either. Meaning it is an “append-only” backup solution. The other vhost allows deletion, and exists for administrative tasks like pruning old backups.

For demo purposes, I’ve stripped a few things out of this config, e.g TLS. You will need to modify the config for your own use cases. Here is the the append-only config:

server {

    listen 0.0.0.0:80;

    
    client_max_body_size 1000M;

    
    default_type "application/vnd.x.restic.rest.v2";

    
    auth_basic           "Restic Append-Only Backups";
    auth_basic_user_file /opt/backups/auth/.htpasswd;
    root                 /opt/backups/repo/$remote_user;

    
    error_page 470 = @list_objects;
    error_page 471 = @read_object;
    error_page 472 = @write_object;
    error_page 473 = @delete_object;
    error_page 474 = @put_proxy;

    
    location ~ "^/(data|keys|locks|snapshots|index)/$" {
        if ($request_method = 'GET') { return 470; } 
        return 403;
    }

    
    location ~ "^/(config|keys/[a-f0-9]{64})$" {
        if ($request_method = 'HEAD') { return 471; } 
        if ($request_method = 'GET')  { return 471; } 
        return 403;
    }

    
    location ~ "^/locks/[a-f0-9]{64}$" {
        if ($request_method = 'HEAD')   { return 471; } 
        if ($request_method = 'GET')    { return 471; } 
        if ($request_method = 'DELETE') { return 473; } 
        if ($request_method = 'PUT')    { return 472; } 
        if ($request_method = 'POST')   { return 474; } 
        return 403;
    }

    
    location ~ "^/(data|index|snapshots)/[a-f0-9]{64}$" {
        if ($request_method = 'HEAD') { return 471; } 
        if ($request_method = 'GET')  { return 471; } 
        if ($request_method = 'PUT')  { return 472; } 
        if ($request_method = 'POST') { return 474; } 
        return 403;
    }

    
    location ~ "^" {
        return 403;
    }

    
    location @read_object {
    }

    
    location @write_object {

        
        if (-f $request_filename) {
            return 403 'No overwriting files';
        }

        dav_methods PUT;
        create_full_put_path on;
        dav_access user:rw;
    }

    
    location @delete_object {
        dav_methods DELETE;
        header_filter_by_lua_block {
            if ngx.status == ngx.HTTP_NO_CONTENT then
                ngx.status = ngx.HTTP_OK
            end
        }
    }

    
    location @list_objects {
        autoindex            on;
        autoindex_exact_size on;
        autoindex_format     json;

        body_filter_by_lua_block {
            chunk = ngx.arg[1];
            if string.match(chunk, '^<') then
                chunk = '[]'
                ngx.arg[2] = true
            else
                chunk = ngx.arg[1];
                chunk = ngx.re.gsub(chunk, '\\s+', '')
                chunk = ngx.re.gsub(chunk, '"(?!name|size)[^"]+":"[^"]+"', '')
                chunk = ngx.re.gsub(chunk, ',{2,}', ',')
                chunk = ngx.re.gsub(chunk, '{,', '{')
                chunk = ngx.re.gsub(chunk, ',}', '}')
            end
            ngx.arg[1] = chunk
        }
        header_filter_by_lua_block {
            ngx.header["content-type"] = "application/vnd.x.restic.rest.v2";
            if ngx.status == ngx.HTTP_NOT_FOUND then
                ngx.status = ngx.HTTP_OK
                ngx.header["content-length"] = 2
            end
        }
    }

    set_real_ip_from 127.0.0.1;
    location @put_proxy {
        proxy_pass       http://127.0.0.1:80;
        proxy_method     PUT;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        header_filter_by_lua_block {
            if ngx.status == ngx.HTTP_CREATED then
                ngx.status = ngx.HTTP_OK
            end
        }
    }
}

The config is laid out such that there are a handful of regex based location blocks at the top, which return error codes based on HTTP method, which are mapped to other location blocks below, which actually perform the requested action.

I used $remote_user in the “root” so that I could give each host it’s own set of credentials to use for basic auth, which would isolate each host into their own backup directory.

I had to use a few tricks to make Nginx compatible with the Restic REST API. First of all, I used the DAV module, to allow Nginx to write files. I configured it with “create_full_put_path on” so that it would recursively create parent directories. The DAV module expects PUT requests, whilst the restic client uses POST requests. There is no way of forcing the DAV module to work with POST requests, so I set up the “@put_proxy” location block, which proxies requests to localhost whilst modifying the method from POST to PUT. Not ideal, but it works. Unfortunately the DAV module returns a 204 response code, but the restic client fails unless it gets a 200, so I used the LUA Nginx module to modify the response code. I had to do this for deleting objects too.

The other main difficulty was that there are certain end-points that the restic client expects to be able to get directory listings from, in a specific format. To achieve this, I used the autoindex functionality built into Nginx (in the @list_objects location block), and set it to return JSON instead of the default HTML. Luckily, the JSON format supplied by Nginx’s autoindex, is close to the one expected by Restic. It returns an array containing objects with “name” and “size” fields. Unfortunately, it returns a number of other fields, which restic isn’t expecting, and the restic client bombs out because of them. To deal with this, I used the Nginx LUA module to modify the response body to remove the unexpected fields, using regexes. Hacky, but works. I also had to configure it to return a 200 response with an empty array, instead of a 404 if the directory doesn’t exist.

The admin vhost is very similar with a few small differences:

  1. I removed $remote_user from the “root”, meaning the admin user can access all backups at slightly different paths.
  2. I used a separate htpasswd file containing only the admin user credentials.
  3. I prefixed each location regex block to allow for parent paths containing my hostnames. E.g “^/locks/[a-f0-9]{64}$” becomes “^/[a-z0-9]+/locks/[a-f0-9]{64}$”
  4. I added the ability to delete and write to more of the location blocks, e.g to allow writing to the config/keys and deleting from index/data/snapshots.
  5. I added a new location block at the start to allow “restic init”. It doesn’t actually need to do anything, but it needs to return a 200:
location ~ "^/[a-z0-9]+/$" {
    if ($request_method = 'POST') { return 200 'Fake initialised'; }
    return 403;
}

I’m not going to quote the entire admin config above. You should be able to figure it out yourself from my description if you’ve understood the config.

I mentioned the Nginx DAV and LUA modules above. Don’t worry, on Debian at least, DAV is built in, and the LUA module can be used simply by doing an “apt install nginx libnginx-mod-http-lua”

Performance?

I don’t have anything interesting to give you. I did some basic performance tests and didn’t find any noticable differences between the two solutions. For your use case, you may find one better than the other. Nginx is certainly a lot more tunable/configurable/customisable than Rest Server, so you may be able to do some more interesting things there. Especially when it comes to things like rate limiting or proxying to multiple backends etc.

Security?

If one of my hosts is compromised, that sucks. The attacker will be able to access and delete the contents of my server, but because the backups are append-only, at least they wont be able to delete them too.

If my backup host is compromised, that sucks. The attacker will be able to delete my backups. But they wont be able to read them because they’re encrypted client side. And my data should still exist on the hosts that are being backed up.

Administrative commands like pruning backups are run from a separate host. Hopefully that host wont become compromised as it currently has access to read, decrypt and delete all of my backups. I’m not going to say much about that host ;)

Simplifying the config

With a few small backwards compatible changes to the restic client and API, I could remove the LUA dependency, and the need to proxy write requests. I might have a look into doing this and seeing if my changes can be upstreamed, when I find some time.

Hosting

I’m using a Lithuanian VPS Host named Time4VPS (affiliate link) to host these backups. They have a set of very cheap “Storage VPS” plans specifically for things like backups. For example, they do a €5.99/month plan which gives you 1TB of disk and 8TB of bandwidth. They go all the way up to 64TB of disk per server. You can select from a variety of distros (including Debian and Centos), and are given full root access.

Want to leave a tip?BitcoinMoneroZcashPaypalYou can follow this Blog using or Mastodon. To read more, visit my blog index.

联系我们 contact @ memedata.com