POSTS
Using Nginx as a REST Backend for Restic Backups
Using Nginx as a REST Backend for Restic Backups
I recently switched to Restic for backing up my servers due to its simplicity and encryption features. While Restic supports various backends, I was particularly interested in its REST API compatibility. Instead of deploying a dedicated REST server like Rest Server (the Go-based implementation), I wondered if Nginx—already running on my systems—could handle the task. Turns out, it can!
The Setup: Append-Only vs. Admin Access
I configured two Nginx virtual hosts on different ports:
- Append-Only Backups – Hosts can upload backups but cannot delete or overwrite files (except lock files)
- Admin Access – Allows deletions and administrative tasks (e.g., pruning old backups)
Below is a simplified version of the append-only configuration (TLS and other optimizations removed for clarity):
server {
listen 0.0.0.0:80;
client_max_body_size 1000M;
default_type "application/vnd.x.restic.rest.v2";
# Authentication
auth_basic "Restic Append-Only Backups";
auth_basic_user_file /opt/backups/auth/.htpasswd;
root /opt/backups/repo/$remote_user; # Isolates backups per host
# Routing rules
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;
# Allow listing directories (e.g., /data/, /locks/)
location ~ "^/(data|keys|locks|snapshots|index)/$" {
if ($request_method = 'GET') { return 470; }
return 403;
}
# Allow reading config and keys
location ~ "^/(config|keys/[a-f0-9]{64})$" {
if ($request_method ~ ^(HEAD|GET)$) { return 471; }
return 403;
}
# Lock file management (read/write/delete)
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;
}
# Data/index/snapshot handling (read/write)
location ~ "^/(data|index|snapshots)/[a-f0-9]{64}$" {
if ($request_method ~ ^(HEAD|GET)$) { return 471; }
if ($request_method = 'PUT') { return 472; }
if ($request_method = 'POST') { return 474; }
return 403;
}
# Block all other requests
location / { return 403; }
# --- Handler Locations --- #
location @read_object { ... }
location @write_object {
if (-f $request_filename) { return 403 'No overwrites'; }
dav_methods PUT;
create_full_put_path on;
dav_access user:rw;
}
location @delete_object {
dav_methods DELETE;
# Convert 204 → 200 for Restic compatibility
header_filter_by_lua_block { ngx.status = ngx.status == 204 and 200 or ngx.status; }
}
location @list_objects {
autoindex on;
autoindex_format json;
# Lua cleans up JSON to match Restic's expected format
body_filter_by_lua_block { ... }
}
# Proxy POST→PUT for DAV compatibility
location @put_proxy {
proxy_pass http://127.0.0.1:80;
proxy_method PUT;
header_filter_by_lua_block { ngx.status = ngx.status == 201 and 200 or ngx.status; }
}
}
Key Challenges and Workarounds
- POST vs. PUT: Restic uses
POST
, but Nginx’s WebDAV module requiresPUT
. Solved with a local proxy - Response Codes: Restic expects
200
for deletions, but WebDAV returns204
. Fixed with Lua header filtering - Directory Listings: Nginx’s
autoindex
JSON includes extra fields. Stripped via Lua regex
Admin Configuration Differences
- No
$remote_user
isolation – Admin accesses all backups - Expanded permissions – Allows deletions in
/data/
,/snapshots/
, etc - Added
restic init
endpoint – Returns a dummy200
response
Security Considerations
- Compromised host: Attackers can’t delete backups (append-only)
- Compromised backup server: Data remains encrypted; attackers can’t read backups without keys
- Admin host: Highly restricted—holds decryption keys and delete permissions
Performance and Alternatives
Benchmarks showed negligible differences between Nginx and Rest Server. However, Nginx offers more flexibility (e.g., rate limiting, load balancing).
Potential Improvements
- Upstream Restic changes to natively support
PUT
and200
responses, eliminating Lua dependencies