ruborg 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +37 -0
- data/SECURITY.md +173 -0
- data/lib/ruborg/backup.rb +56 -7
- data/lib/ruborg/cli.rb +53 -9
- data/lib/ruborg/config.rb +47 -4
- data/lib/ruborg/logger.rb +16 -3
- data/lib/ruborg/passbolt.rb +3 -2
- data/lib/ruborg/repository.rb +26 -4
- data/lib/ruborg/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0bb6c1c5f47b1fbb4538310a929914b0e744bcdb30d5e5635c4246df778e2113
|
|
4
|
+
data.tar.gz: bda5cf063fe587a047dd36fa9c5e948d8130e47501a53eabef9c34a3af7ae361
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8828dacb76519557d51876327133491318bd199e589fb695d08a94af1a73a3c34772f022b6630e199a2c1ab9ab7d78dcce043a402bfa0f418ae51262aad80e8f
|
|
7
|
+
data.tar.gz: 99b7e79eb2e50240862f7ade79291d9a76469385f2b7e8949df8e6ac3251f9adcbe2f605e42c09364616c028f10575593c3e0a1df1d020bcf8fc7291a9cdb558
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.1] - 2025-10-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `borg_options` configuration for controlling Borg environment variables
|
|
14
|
+
- Repository path validation to prevent creation in system directories
|
|
15
|
+
- Backup path validation and normalization
|
|
16
|
+
- Archive name sanitization (alphanumeric, dash, underscore, dot only)
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Borg environment variables now configurable via `borg_options` (backward compatible)
|
|
20
|
+
- All backup paths are now normalized to absolute paths
|
|
21
|
+
- Custom archive names are automatically sanitized
|
|
22
|
+
|
|
23
|
+
### Security
|
|
24
|
+
- Fixed command injection vulnerability in Passbolt CLI execution (now uses Open3.capture3)
|
|
25
|
+
- Added path traversal protection for extract operations
|
|
26
|
+
- Implemented symlink resolution and system path protection for --remove-source
|
|
27
|
+
- Changed to YAML.safe_load_file to prevent arbitrary code execution
|
|
28
|
+
- Added log path validation to prevent writing to system directories
|
|
29
|
+
- Added repository path validation (prevents /bin, /etc, /usr, etc.)
|
|
30
|
+
- Added backup path validation (rejects empty/nil paths)
|
|
31
|
+
- Added archive name sanitization (prevents injection attacks)
|
|
32
|
+
- Made Borg environment options configurable for enhanced security
|
|
33
|
+
- Added SECURITY.md with comprehensive security guidelines and best practices
|
|
34
|
+
- Enhanced test coverage for all security features
|
|
35
|
+
|
|
10
36
|
## [0.3.0] - 2025-10-05
|
|
11
37
|
|
|
12
38
|
### Added
|
data/README.md
CHANGED
|
@@ -18,6 +18,7 @@ A friendly Ruby frontend for [Borg Backup](https://www.borgbackup.org/). Ruborg
|
|
|
18
18
|
- 🗄️ **Multi-Repository** - Manage multiple backup repositories with different sources
|
|
19
19
|
- 🔄 **Auto-initialization** - Automatically initialize repositories on first use
|
|
20
20
|
- ✅ **Well-tested** - Comprehensive test suite with RSpec
|
|
21
|
+
- 🔒 **Security-focused** - Path validation, safe YAML loading, command injection protection
|
|
21
22
|
|
|
22
23
|
## Prerequisites
|
|
23
24
|
|
|
@@ -98,6 +99,11 @@ auto_init: true
|
|
|
98
99
|
|
|
99
100
|
# Log file path (optional, default: ~/.ruborg/logs/ruborg.log)
|
|
100
101
|
log_file: /var/log/ruborg.log
|
|
102
|
+
|
|
103
|
+
# Borg environment options (optional)
|
|
104
|
+
borg_options:
|
|
105
|
+
allow_relocated_repo: true # Allow relocated repositories (default: true)
|
|
106
|
+
allow_unencrypted_repo: true # Allow unencrypted repositories (default: true)
|
|
101
107
|
```
|
|
102
108
|
|
|
103
109
|
### Multi-Repository Configuration
|
|
@@ -111,6 +117,9 @@ encryption: repokey
|
|
|
111
117
|
auto_init: true
|
|
112
118
|
passbolt:
|
|
113
119
|
resource_id: "global-passbolt-id"
|
|
120
|
+
borg_options:
|
|
121
|
+
allow_relocated_repo: false
|
|
122
|
+
allow_unencrypted_repo: false
|
|
114
123
|
|
|
115
124
|
# Multiple repositories
|
|
116
125
|
repositories:
|
|
@@ -282,6 +291,33 @@ backup_paths:
|
|
|
282
291
|
|
|
283
292
|
When enabled, ruborg will automatically run `borg init` if the repository doesn't exist when you run `backup`, `list`, or `info` commands. The passphrase will be retrieved from Passbolt if configured.
|
|
284
293
|
|
|
294
|
+
## Security Configuration
|
|
295
|
+
|
|
296
|
+
Ruborg provides configurable security options via `borg_options`:
|
|
297
|
+
|
|
298
|
+
```yaml
|
|
299
|
+
borg_options:
|
|
300
|
+
# Control whether to allow access to relocated repositories
|
|
301
|
+
# Set to false in production for enhanced security
|
|
302
|
+
allow_relocated_repo: true # default: true
|
|
303
|
+
|
|
304
|
+
# Control whether to allow access to unencrypted repositories
|
|
305
|
+
# Set to false to enforce encryption
|
|
306
|
+
allow_unencrypted_repo: true # default: true
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Security Features:**
|
|
310
|
+
- **Repository Path Validation**: Prevents creation in system directories (`/bin`, `/etc`, `/usr`, etc.)
|
|
311
|
+
- **Backup Path Validation**: Validates and normalizes all backup source paths
|
|
312
|
+
- **Archive Name Sanitization**: Automatically sanitizes custom archive names
|
|
313
|
+
- **Path Traversal Protection**: Prevents extraction to system directories
|
|
314
|
+
- **Symlink Protection**: Resolves and validates symlinks before deletion with `--remove-source`
|
|
315
|
+
- **Safe YAML Loading**: Uses `YAML.safe_load_file` to prevent code execution
|
|
316
|
+
- **Command Injection Protection**: Uses safe command execution methods
|
|
317
|
+
- **Log Path Validation**: Prevents writing logs to system directories
|
|
318
|
+
|
|
319
|
+
See [SECURITY.md](SECURITY.md) for detailed security information and best practices.
|
|
320
|
+
|
|
285
321
|
## Command Reference
|
|
286
322
|
|
|
287
323
|
| Command | Description | Options |
|
|
@@ -341,6 +377,7 @@ The test suite includes:
|
|
|
341
377
|
- Passbolt integration (mocked)
|
|
342
378
|
- CLI commands
|
|
343
379
|
- Logging functionality
|
|
380
|
+
- Comprehensive security tests (path validation, sanitization, etc.)
|
|
344
381
|
|
|
345
382
|
## Contributing
|
|
346
383
|
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Security Features
|
|
4
|
+
|
|
5
|
+
Ruborg implements several security measures to protect your backup operations:
|
|
6
|
+
|
|
7
|
+
### 1. Command Injection Prevention
|
|
8
|
+
- Uses `Open3.capture3` for Passbolt CLI execution (no shell interpolation)
|
|
9
|
+
- Array-based command construction for Borg commands
|
|
10
|
+
- No user input directly interpolated into shell commands
|
|
11
|
+
|
|
12
|
+
### 2. Path Traversal Protection
|
|
13
|
+
- Validates all destination paths during restore operations
|
|
14
|
+
- Prevents extraction to system directories (`/`, `/etc`, `/bin`, `/usr`, etc.)
|
|
15
|
+
- Normalizes paths to prevent `../` traversal attacks
|
|
16
|
+
|
|
17
|
+
### 3. Symlink Protection
|
|
18
|
+
- Resolves symlinks before file deletion with `--remove-source`
|
|
19
|
+
- Refuses to delete system directories even when targeted via symlinks
|
|
20
|
+
- Uses `FileUtils.rm_rf` with `secure: true` option
|
|
21
|
+
|
|
22
|
+
### 4. Safe YAML Loading
|
|
23
|
+
- Uses `YAML.safe_load_file` to prevent arbitrary code execution
|
|
24
|
+
- Rejects YAML files containing Ruby objects or other dangerous constructs
|
|
25
|
+
- Only permits basic data types and Symbol class
|
|
26
|
+
|
|
27
|
+
### 5. Log Path Validation
|
|
28
|
+
- Validates log file paths to prevent writing to system directories
|
|
29
|
+
- Automatically creates log directories with proper permissions
|
|
30
|
+
- Rejects paths in `/bin`, `/etc`, `/usr`, and other sensitive locations
|
|
31
|
+
|
|
32
|
+
### 6. Passphrase Handling
|
|
33
|
+
- Passes passphrases via environment variables (never CLI arguments)
|
|
34
|
+
- Prevents passphrase leakage in process listings
|
|
35
|
+
- Uses Passbolt integration for secure password retrieval
|
|
36
|
+
|
|
37
|
+
### 7. Repository Path Validation
|
|
38
|
+
- Validates repository paths to prevent creation in system directories
|
|
39
|
+
- Rejects empty or nil repository paths
|
|
40
|
+
- Prevents accidental repository creation in `/bin`, `/etc`, `/usr`, etc.
|
|
41
|
+
|
|
42
|
+
### 8. Backup Path Validation
|
|
43
|
+
- Validates all backup source paths before creating archives
|
|
44
|
+
- Rejects empty, nil, or whitespace-only paths
|
|
45
|
+
- Normalizes relative paths to absolute paths for consistency
|
|
46
|
+
|
|
47
|
+
### 9. Archive Name Sanitization
|
|
48
|
+
- Sanitizes user-provided archive names to prevent injection attacks
|
|
49
|
+
- Allows only alphanumeric characters, dashes, underscores, and dots
|
|
50
|
+
- Rejects archive names that would become empty after sanitization
|
|
51
|
+
|
|
52
|
+
### 10. Configurable Borg Environment Options
|
|
53
|
+
- Allows control over relocated repository access via config
|
|
54
|
+
- Allows control over unencrypted repository access via config
|
|
55
|
+
- Defaults to safe settings while maintaining backward compatibility
|
|
56
|
+
|
|
57
|
+
## Security Best Practices
|
|
58
|
+
|
|
59
|
+
### When Using `--remove-source`
|
|
60
|
+
⚠️ **Warning**: The `--remove-source` flag permanently deletes source files after backup.
|
|
61
|
+
|
|
62
|
+
**Recommendations:**
|
|
63
|
+
1. **Test first** without `--remove-source` to verify backups work
|
|
64
|
+
2. **Never use on symlinks** to critical system directories
|
|
65
|
+
3. **Verify backups** before using this flag in production
|
|
66
|
+
4. **Use absolute paths** in configuration to avoid ambiguity
|
|
67
|
+
|
|
68
|
+
### Configuration File Security
|
|
69
|
+
- Store configuration files with restricted permissions: `chmod 600 ruborg.yml`
|
|
70
|
+
- Never commit Passbolt resource IDs to public repositories
|
|
71
|
+
- Use environment variables for sensitive paths when possible
|
|
72
|
+
|
|
73
|
+
### Repository Security
|
|
74
|
+
- Use encrypted repositories (default: `encryption: repokey`)
|
|
75
|
+
- Store passphrases in Passbolt, not in config files
|
|
76
|
+
- Use different encryption keys for different repository types
|
|
77
|
+
- Regularly rotate Passbolt passphrases
|
|
78
|
+
- Avoid creating repositories in system directories
|
|
79
|
+
- Use absolute paths for repository locations
|
|
80
|
+
|
|
81
|
+
### Multi-Repository Considerations
|
|
82
|
+
- Each repository can have its own Passbolt resource ID
|
|
83
|
+
- Validate all source paths before adding to configuration
|
|
84
|
+
- Review exclude patterns to ensure no sensitive files leak
|
|
85
|
+
|
|
86
|
+
### Archive Naming
|
|
87
|
+
- Use default timestamp-based names when possible
|
|
88
|
+
- If providing custom names, use only alphanumeric characters, dashes, and underscores
|
|
89
|
+
- Avoid special characters or path separators in archive names
|
|
90
|
+
|
|
91
|
+
### Borg Environment Options
|
|
92
|
+
- Consider disabling `allow_relocated_repo` for production environments
|
|
93
|
+
- Consider disabling `allow_unencrypted_repo` for sensitive data
|
|
94
|
+
- Configure in `ruborg.yml`:
|
|
95
|
+
```yaml
|
|
96
|
+
borg_options:
|
|
97
|
+
allow_relocated_repo: false # Reject relocated repositories
|
|
98
|
+
allow_unencrypted_repo: false # Reject unencrypted repositories
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Reporting Security Issues
|
|
102
|
+
|
|
103
|
+
If you discover a security vulnerability, please:
|
|
104
|
+
|
|
105
|
+
1. **Do NOT** open a public issue
|
|
106
|
+
2. Email security concerns to: mpantel@aegean.gr
|
|
107
|
+
3. Include:
|
|
108
|
+
- Description of the vulnerability
|
|
109
|
+
- Steps to reproduce
|
|
110
|
+
- Potential impact
|
|
111
|
+
- Suggested fix (if any)
|
|
112
|
+
|
|
113
|
+
We will respond within 48 hours and work with you to address the issue.
|
|
114
|
+
|
|
115
|
+
## Security Audit History
|
|
116
|
+
|
|
117
|
+
- **v0.3.1** (2025-10-05): Comprehensive security hardening
|
|
118
|
+
- Fixed command injection in Passbolt CLI execution (uses Open3.capture3)
|
|
119
|
+
- Added path traversal protection for extract operations
|
|
120
|
+
- Implemented symlink protection for file deletion with --remove-source
|
|
121
|
+
- Switched to safe YAML loading (YAML.safe_load_file)
|
|
122
|
+
- Added log path validation to prevent writing to system directories
|
|
123
|
+
- Added repository path validation to prevent creation in system directories
|
|
124
|
+
- Added backup path validation and normalization
|
|
125
|
+
- Implemented archive name sanitization
|
|
126
|
+
- Made Borg environment variables configurable
|
|
127
|
+
- Enhanced test coverage for all security features
|
|
128
|
+
- Created comprehensive SECURITY.md documentation
|
|
129
|
+
|
|
130
|
+
- **v0.3.0** (2025-10-05): Multi-repository and auto-initialization features
|
|
131
|
+
- Added multi-repository configuration support
|
|
132
|
+
- Added auto-initialization feature
|
|
133
|
+
- Added configurable log file paths
|
|
134
|
+
- No security-specific changes in this version
|
|
135
|
+
|
|
136
|
+
## Dependency Security
|
|
137
|
+
|
|
138
|
+
Ruborg relies on:
|
|
139
|
+
- **Borg Backup**: Industry-standard backup tool with strong encryption
|
|
140
|
+
- **Passbolt CLI**: Secure password management
|
|
141
|
+
- **Ruby stdlib**: No external gems for core functionality (only Thor for CLI)
|
|
142
|
+
|
|
143
|
+
Keep dependencies updated:
|
|
144
|
+
```bash
|
|
145
|
+
# Update Borg
|
|
146
|
+
brew upgrade borgbackup # macOS
|
|
147
|
+
sudo apt update && sudo apt upgrade borgbackup # Ubuntu
|
|
148
|
+
|
|
149
|
+
# Update Passbolt CLI
|
|
150
|
+
# Follow https://github.com/passbolt/go-passbolt-cli
|
|
151
|
+
|
|
152
|
+
# Update Ruby gems
|
|
153
|
+
bundle update
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Security Checklist
|
|
157
|
+
|
|
158
|
+
Before deploying ruborg in production:
|
|
159
|
+
|
|
160
|
+
- [ ] Review all paths in configuration files
|
|
161
|
+
- [ ] Set proper file permissions on config (600) and logs (640)
|
|
162
|
+
- [ ] Use Passbolt for all passphrases (never hardcode)
|
|
163
|
+
- [ ] Test restore operations before relying on backups
|
|
164
|
+
- [ ] Never use `--remove-source` without thorough testing
|
|
165
|
+
- [ ] Keep Borg and Passbolt CLI up to date
|
|
166
|
+
- [ ] Review exclude patterns for sensitive data
|
|
167
|
+
- [ ] Use absolute paths in configuration
|
|
168
|
+
- [ ] Enable auto_init only for trusted repository locations
|
|
169
|
+
- [ ] Regularly audit backup logs for anomalies
|
|
170
|
+
- [ ] Validate repository paths are not in system directories
|
|
171
|
+
- [ ] Configure borg_options for your security requirements
|
|
172
|
+
- [ ] Use default archive names or sanitized custom names only
|
|
173
|
+
- [ ] Ensure backup paths don't contain empty or nil values
|
data/lib/ruborg/backup.rb
CHANGED
|
@@ -28,8 +28,12 @@ module Ruborg
|
|
|
28
28
|
# Change to destination directory if specified
|
|
29
29
|
if destination != "."
|
|
30
30
|
require "fileutils"
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
|
|
32
|
+
# Validate and normalize destination path
|
|
33
|
+
validated_dest = validate_destination_path(destination)
|
|
34
|
+
FileUtils.mkdir_p(validated_dest) unless File.directory?(validated_dest)
|
|
35
|
+
|
|
36
|
+
Dir.chdir(validated_dest) do
|
|
33
37
|
execute_borg_command(cmd)
|
|
34
38
|
end
|
|
35
39
|
else
|
|
@@ -57,7 +61,10 @@ module Ruborg
|
|
|
57
61
|
end
|
|
58
62
|
|
|
59
63
|
cmd << "#{@repository.path}::#{archive_name}"
|
|
60
|
-
|
|
64
|
+
|
|
65
|
+
# Validate and normalize backup paths
|
|
66
|
+
validated_paths = validate_backup_paths(@config.backup_paths)
|
|
67
|
+
cmd += validated_paths
|
|
61
68
|
|
|
62
69
|
cmd
|
|
63
70
|
end
|
|
@@ -79,11 +86,53 @@ module Ruborg
|
|
|
79
86
|
require "fileutils"
|
|
80
87
|
|
|
81
88
|
@config.backup_paths.each do |path|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
89
|
+
# Resolve symlinks and validate path
|
|
90
|
+
begin
|
|
91
|
+
real_path = File.realpath(path)
|
|
92
|
+
rescue Errno::ENOENT
|
|
93
|
+
# Path doesn't exist, skip
|
|
94
|
+
next
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Security check: ensure path hasn't been tampered with
|
|
98
|
+
unless File.exist?(real_path)
|
|
99
|
+
next
|
|
86
100
|
end
|
|
101
|
+
|
|
102
|
+
# Additional safety: don't delete root or system directories
|
|
103
|
+
if real_path == "/" || real_path.start_with?("/bin", "/sbin", "/usr", "/etc", "/sys", "/proc")
|
|
104
|
+
raise BorgError, "Refusing to delete system path: #{real_path}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
if File.directory?(real_path)
|
|
108
|
+
FileUtils.rm_rf(real_path, secure: true)
|
|
109
|
+
elsif File.file?(real_path)
|
|
110
|
+
FileUtils.rm(real_path)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def validate_destination_path(destination)
|
|
116
|
+
# Expand and normalize the path
|
|
117
|
+
normalized_path = File.expand_path(destination)
|
|
118
|
+
|
|
119
|
+
# Security check: prevent path traversal to sensitive directories
|
|
120
|
+
forbidden_paths = ["/", "/bin", "/sbin", "/usr", "/etc", "/sys", "/proc", "/boot"]
|
|
121
|
+
forbidden_paths.each do |forbidden|
|
|
122
|
+
if normalized_path == forbidden || normalized_path.start_with?("#{forbidden}/")
|
|
123
|
+
raise BorgError, "Invalid destination: refusing to extract to system directory #{normalized_path}"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
normalized_path
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def validate_backup_paths(paths)
|
|
131
|
+
raise BorgError, "No backup paths specified" if paths.nil? || paths.empty?
|
|
132
|
+
|
|
133
|
+
paths.map do |path|
|
|
134
|
+
raise BorgError, "Empty backup path specified" if path.nil? || path.to_s.strip.empty?
|
|
135
|
+
File.expand_path(path)
|
|
87
136
|
end
|
|
88
137
|
end
|
|
89
138
|
end
|
data/lib/ruborg/cli.rb
CHANGED
|
@@ -17,10 +17,14 @@ module Ruborg
|
|
|
17
17
|
# Try to load config to get log_file setting
|
|
18
18
|
config_path = options[:config] || "ruborg.yml"
|
|
19
19
|
if File.exist?(config_path)
|
|
20
|
-
config_data = YAML.
|
|
20
|
+
config_data = YAML.safe_load_file(config_path, permitted_classes: [Symbol], aliases: true) rescue {}
|
|
21
21
|
log_path = config_data["log_file"]
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
# Validate log path if provided
|
|
26
|
+
log_path = validate_log_path(log_path) if log_path
|
|
27
|
+
|
|
24
28
|
@logger = RuborgLogger.new(log_file: log_path)
|
|
25
29
|
end
|
|
26
30
|
|
|
@@ -63,7 +67,7 @@ module Ruborg
|
|
|
63
67
|
config = Config.new(options[:config])
|
|
64
68
|
passphrase = fetch_passphrase_from_config(config)
|
|
65
69
|
|
|
66
|
-
repo = Repository.new(config.repository, passphrase: passphrase)
|
|
70
|
+
repo = Repository.new(config.repository, passphrase: passphrase, borg_options: config.borg_options)
|
|
67
71
|
|
|
68
72
|
# Auto-initialize repository if configured
|
|
69
73
|
if config.auto_init? && !repo.exists?
|
|
@@ -88,7 +92,7 @@ module Ruborg
|
|
|
88
92
|
config = Config.new(options[:config])
|
|
89
93
|
passphrase = fetch_passphrase_from_config(config)
|
|
90
94
|
|
|
91
|
-
repo = Repository.new(config.repository, passphrase: passphrase)
|
|
95
|
+
repo = Repository.new(config.repository, passphrase: passphrase, borg_options: config.borg_options)
|
|
92
96
|
backup = Backup.new(repo, config: config)
|
|
93
97
|
|
|
94
98
|
backup.extract(archive_name, destination: options[:destination], path: options[:path])
|
|
@@ -110,7 +114,7 @@ module Ruborg
|
|
|
110
114
|
config = Config.new(options[:config])
|
|
111
115
|
passphrase = fetch_passphrase_from_config(config)
|
|
112
116
|
|
|
113
|
-
repo = Repository.new(config.repository, passphrase: passphrase)
|
|
117
|
+
repo = Repository.new(config.repository, passphrase: passphrase, borg_options: config.borg_options)
|
|
114
118
|
|
|
115
119
|
# Auto-initialize repository if configured
|
|
116
120
|
if config.auto_init? && !repo.exists?
|
|
@@ -147,12 +151,37 @@ module Ruborg
|
|
|
147
151
|
exit 1
|
|
148
152
|
end
|
|
149
153
|
|
|
154
|
+
def validate_log_path(log_path)
|
|
155
|
+
# Expand to absolute path
|
|
156
|
+
normalized_path = File.expand_path(log_path)
|
|
157
|
+
|
|
158
|
+
# Prevent writing to sensitive system directories
|
|
159
|
+
forbidden_paths = ["/bin", "/sbin", "/usr/bin", "/usr/sbin", "/etc", "/sys", "/proc", "/boot"]
|
|
160
|
+
forbidden_paths.each do |forbidden|
|
|
161
|
+
if normalized_path.start_with?("#{forbidden}/")
|
|
162
|
+
raise ConfigError, "Invalid log path: refusing to write to system directory #{normalized_path}"
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Ensure parent directory exists or can be created
|
|
167
|
+
log_dir = File.dirname(normalized_path)
|
|
168
|
+
unless File.directory?(log_dir)
|
|
169
|
+
begin
|
|
170
|
+
FileUtils.mkdir_p(log_dir)
|
|
171
|
+
rescue => e
|
|
172
|
+
raise ConfigError, "Cannot create log directory #{log_dir}: #{e.message}"
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
normalized_path
|
|
177
|
+
end
|
|
178
|
+
|
|
150
179
|
# Single repository backup (legacy)
|
|
151
180
|
def backup_single_repo(config)
|
|
152
181
|
@logger.info("Backing up paths: #{config.backup_paths.join(', ')}")
|
|
153
182
|
passphrase = fetch_passphrase_from_config(config)
|
|
154
183
|
|
|
155
|
-
repo = Repository.new(config.repository, passphrase: passphrase)
|
|
184
|
+
repo = Repository.new(config.repository, passphrase: passphrase, borg_options: config.borg_options)
|
|
156
185
|
|
|
157
186
|
# Auto-initialize repository if configured
|
|
158
187
|
if config.auto_init? && !repo.exists?
|
|
@@ -163,9 +192,9 @@ module Ruborg
|
|
|
163
192
|
|
|
164
193
|
backup = Backup.new(repo, config: config)
|
|
165
194
|
|
|
166
|
-
archive_name = options[:name]
|
|
195
|
+
archive_name = options[:name] ? sanitize_archive_name(options[:name]) : Time.now.strftime("%Y-%m-%d_%H-%M-%S")
|
|
167
196
|
@logger.info("Creating archive: #{archive_name}")
|
|
168
|
-
backup.create(name:
|
|
197
|
+
backup.create(name: archive_name, remove_source: options[:remove_source])
|
|
169
198
|
@logger.info("Backup created successfully: #{archive_name}")
|
|
170
199
|
|
|
171
200
|
if options[:remove_source]
|
|
@@ -203,7 +232,8 @@ module Ruborg
|
|
|
203
232
|
merged_config = global_settings.merge(repo_config)
|
|
204
233
|
|
|
205
234
|
passphrase = fetch_passphrase_for_repo(merged_config)
|
|
206
|
-
|
|
235
|
+
borg_opts = merged_config["borg_options"] || {}
|
|
236
|
+
repo = Repository.new(repo_config["path"], passphrase: passphrase, borg_options: borg_opts)
|
|
207
237
|
|
|
208
238
|
# Auto-initialize if configured
|
|
209
239
|
auto_init = merged_config["auto_init"] || false
|
|
@@ -217,7 +247,7 @@ module Ruborg
|
|
|
217
247
|
backup_config = BackupConfig.new(repo_config, merged_config)
|
|
218
248
|
backup = Backup.new(repo, config: backup_config)
|
|
219
249
|
|
|
220
|
-
archive_name = options[:name]
|
|
250
|
+
archive_name = options[:name] ? sanitize_archive_name(options[:name]) : "#{repo_name}-#{Time.now.strftime('%Y-%m-%d_%H-%M-%S')}"
|
|
221
251
|
@logger.info("Creating archive: #{archive_name}")
|
|
222
252
|
|
|
223
253
|
sources = repo_config["sources"] || []
|
|
@@ -237,6 +267,20 @@ module Ruborg
|
|
|
237
267
|
Passbolt.new(resource_id: passbolt_config["resource_id"]).get_password
|
|
238
268
|
end
|
|
239
269
|
|
|
270
|
+
def sanitize_archive_name(name)
|
|
271
|
+
raise ConfigError, "Archive name cannot be empty" if name.nil? || name.strip.empty?
|
|
272
|
+
|
|
273
|
+
# Check if name contains at least one valid character before sanitization
|
|
274
|
+
unless name =~ /[a-zA-Z0-9._-]/
|
|
275
|
+
raise ConfigError, "Invalid archive name: must contain at least one valid character (alphanumeric, dot, dash, or underscore)"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Allow only alphanumeric, dash, underscore, and dot
|
|
279
|
+
sanitized = name.gsub(/[^a-zA-Z0-9._-]/, '_')
|
|
280
|
+
|
|
281
|
+
sanitized
|
|
282
|
+
end
|
|
283
|
+
|
|
240
284
|
# Wrapper class to adapt multi-repo config to existing Backup class
|
|
241
285
|
class BackupConfig
|
|
242
286
|
def initialize(repo_config, merged_settings)
|
data/lib/ruborg/config.rb
CHANGED
|
@@ -17,9 +17,11 @@ module Ruborg
|
|
|
17
17
|
def load_config
|
|
18
18
|
raise ConfigError, "Configuration file not found: #{@config_path}" unless File.exist?(@config_path)
|
|
19
19
|
|
|
20
|
-
@data = YAML.
|
|
20
|
+
@data = YAML.safe_load_file(@config_path, permitted_classes: [Symbol], aliases: true)
|
|
21
21
|
rescue Psych::SyntaxError => e
|
|
22
22
|
raise ConfigError, "Invalid YAML syntax: #{e.message}"
|
|
23
|
+
rescue Psych::DisallowedClass => e
|
|
24
|
+
raise ConfigError, "Invalid YAML content: #{e.message}"
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
# Legacy single-repo accessors (for backward compatibility)
|
|
@@ -32,15 +34,18 @@ module Ruborg
|
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
def exclude_patterns
|
|
35
|
-
@data["exclude_patterns"] || []
|
|
37
|
+
patterns = @data["exclude_patterns"] || []
|
|
38
|
+
validate_exclude_patterns(patterns)
|
|
36
39
|
end
|
|
37
40
|
|
|
38
41
|
def compression
|
|
39
|
-
@data["compression"] || "lz4"
|
|
42
|
+
value = @data["compression"] || "lz4"
|
|
43
|
+
validate_compression(value)
|
|
40
44
|
end
|
|
41
45
|
|
|
42
46
|
def encryption_mode
|
|
43
|
-
@data["encryption"] || "repokey"
|
|
47
|
+
value = @data["encryption"] || "repokey"
|
|
48
|
+
validate_encryption(value)
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
def passbolt_integration
|
|
@@ -55,6 +60,10 @@ module Ruborg
|
|
|
55
60
|
@data["log_file"]
|
|
56
61
|
end
|
|
57
62
|
|
|
63
|
+
def borg_options
|
|
64
|
+
@data["borg_options"] || {}
|
|
65
|
+
end
|
|
66
|
+
|
|
58
67
|
# New multi-repo support
|
|
59
68
|
def multi_repo?
|
|
60
69
|
@multi_repo
|
|
@@ -81,8 +90,42 @@ module Ruborg
|
|
|
81
90
|
|
|
82
91
|
private
|
|
83
92
|
|
|
93
|
+
VALID_COMPRESSION = ["lz4", "zstd", "zlib", "lzma", "none"].freeze
|
|
94
|
+
VALID_ENCRYPTION = ["repokey", "keyfile", "none", "authenticated", "repokey-blake2",
|
|
95
|
+
"keyfile-blake2", "authenticated-blake2"].freeze
|
|
96
|
+
|
|
84
97
|
def detect_format
|
|
85
98
|
@multi_repo = @data.key?("repositories")
|
|
86
99
|
end
|
|
100
|
+
|
|
101
|
+
def validate_compression(compression)
|
|
102
|
+
unless VALID_COMPRESSION.include?(compression)
|
|
103
|
+
raise ConfigError, "Invalid compression '#{compression}'. Must be one of: #{VALID_COMPRESSION.join(', ')}"
|
|
104
|
+
end
|
|
105
|
+
compression
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def validate_encryption(encryption)
|
|
109
|
+
unless VALID_ENCRYPTION.include?(encryption)
|
|
110
|
+
raise ConfigError, "Invalid encryption mode '#{encryption}'. Must be one of: #{VALID_ENCRYPTION.join(', ')}"
|
|
111
|
+
end
|
|
112
|
+
encryption
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def validate_exclude_patterns(patterns)
|
|
116
|
+
return patterns if patterns.empty?
|
|
117
|
+
|
|
118
|
+
patterns.each do |pattern|
|
|
119
|
+
if pattern.nil? || pattern.to_s.strip.empty?
|
|
120
|
+
raise ConfigError, "Exclude pattern cannot be empty or nil"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
if pattern.length > 1000
|
|
124
|
+
raise ConfigError, "Exclude pattern too long (max 1000 characters): #{pattern[0..50]}..."
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
patterns
|
|
129
|
+
end
|
|
87
130
|
end
|
|
88
131
|
end
|
data/lib/ruborg/logger.rb
CHANGED
|
@@ -10,7 +10,7 @@ module Ruborg
|
|
|
10
10
|
|
|
11
11
|
def initialize(log_file: nil)
|
|
12
12
|
@log_file = log_file || default_log_file
|
|
13
|
-
|
|
13
|
+
validate_and_ensure_log_directory
|
|
14
14
|
@logger = Logger.new(@log_file, "daily")
|
|
15
15
|
@logger.level = Logger::INFO
|
|
16
16
|
@logger.formatter = proc do |severity, datetime, progname, msg|
|
|
@@ -45,8 +45,21 @@ module Ruborg
|
|
|
45
45
|
dir
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
def
|
|
49
|
-
|
|
48
|
+
def validate_and_ensure_log_directory
|
|
49
|
+
# Validate log file path for security
|
|
50
|
+
normalized_path = File.expand_path(@log_file)
|
|
51
|
+
|
|
52
|
+
# Prevent writing to sensitive system directories
|
|
53
|
+
forbidden_paths = ["/bin", "/sbin", "/usr/bin", "/usr/sbin", "/etc", "/sys", "/proc", "/boot"]
|
|
54
|
+
forbidden_paths.each do |forbidden|
|
|
55
|
+
if normalized_path.start_with?("#{forbidden}/")
|
|
56
|
+
raise ConfigError, "Invalid log path: refusing to write to system directory #{normalized_path}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Ensure log directory exists
|
|
61
|
+
log_dir = File.dirname(normalized_path)
|
|
62
|
+
FileUtils.mkdir_p(log_dir) unless File.directory?(log_dir)
|
|
50
63
|
end
|
|
51
64
|
end
|
|
52
65
|
end
|
data/lib/ruborg/passbolt.rb
CHANGED
data/lib/ruborg/repository.rb
CHANGED
|
@@ -5,9 +5,10 @@ module Ruborg
|
|
|
5
5
|
class Repository
|
|
6
6
|
attr_reader :path
|
|
7
7
|
|
|
8
|
-
def initialize(path, passphrase: nil)
|
|
9
|
-
@path = path
|
|
8
|
+
def initialize(path, passphrase: nil, borg_options: {})
|
|
9
|
+
@path = validate_repo_path(path)
|
|
10
10
|
@passphrase = passphrase
|
|
11
|
+
@borg_options = borg_options
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def exists?
|
|
@@ -37,11 +38,32 @@ module Ruborg
|
|
|
37
38
|
|
|
38
39
|
private
|
|
39
40
|
|
|
41
|
+
def validate_repo_path(path)
|
|
42
|
+
raise BorgError, "Repository path cannot be empty" if path.nil? || path.empty?
|
|
43
|
+
|
|
44
|
+
normalized = File.expand_path(path)
|
|
45
|
+
|
|
46
|
+
# Prevent repository creation in critical system directories
|
|
47
|
+
forbidden = ["/bin", "/sbin", "/usr/bin", "/usr/sbin", "/etc", "/sys", "/proc", "/boot", "/dev"]
|
|
48
|
+
forbidden.each do |forbidden_path|
|
|
49
|
+
if normalized == forbidden_path || normalized.start_with?("#{forbidden_path}/")
|
|
50
|
+
raise BorgError, "Invalid repository path: refusing to use system directory #{normalized}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
normalized
|
|
55
|
+
end
|
|
56
|
+
|
|
40
57
|
def execute_borg_command(cmd)
|
|
41
58
|
env = {}
|
|
42
59
|
env["BORG_PASSPHRASE"] = @passphrase if @passphrase
|
|
43
|
-
|
|
44
|
-
|
|
60
|
+
|
|
61
|
+
# Apply Borg environment options from config (defaults to yes for backward compatibility)
|
|
62
|
+
allow_relocated = @borg_options.fetch("allow_relocated_repo", true)
|
|
63
|
+
allow_unencrypted = @borg_options.fetch("allow_unencrypted_repo", true)
|
|
64
|
+
|
|
65
|
+
env["BORG_RELOCATED_REPO_ACCESS_IS_OK"] = allow_relocated ? "yes" : "no"
|
|
66
|
+
env["BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK"] = allow_unencrypted ? "yes" : "no"
|
|
45
67
|
|
|
46
68
|
# Redirect stdin from /dev/null to prevent interactive prompts
|
|
47
69
|
result = system(env, *cmd, in: "/dev/null")
|
data/lib/ruborg/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruborg
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michail Pantelelis
|
|
@@ -110,6 +110,7 @@ files:
|
|
|
110
110
|
- LICENSE
|
|
111
111
|
- README.md
|
|
112
112
|
- Rakefile
|
|
113
|
+
- SECURITY.md
|
|
113
114
|
- exe/ruborg
|
|
114
115
|
- lib/ruborg.rb
|
|
115
116
|
- lib/ruborg/backup.rb
|