Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,12 +460,12 @@ Export your configuration (servers + apps + secrets + config files) as YAML and

Backups can be protected with a passphrase using **AES-GCM encryption** (PBKDF2 key derivation, 600,000 iterations):

- Secrets are encrypted before export with prefix `enc:`
- Auth passwords and config file contents are encrypted
- Secrets, SSH keys, auth passwords, and config file contents are encrypted before export
- Encrypted values use the prefix `enc:` with base64 salt + nonce + ciphertext
- The export page includes a client-side passphrase generator (32-character hex, via `crypto.getRandomValues`)
- Without a passphrase, secrets are exported as plaintext (still safe for offline backups)
- Without a passphrase, sensitive data is exported as plaintext (still useful for offline/internal backups)

SSH keys are **never** exported — you must re-enter them after import.
When importing on a new instance, all servers and their SSH keys are restored automatically — no need to re-enter keys one by one, as long as the target VMs are reachable or use the same key pair.

### Import

Expand Down
8 changes: 4 additions & 4 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,20 +350,20 @@ Init is idempotent — re-running skips components that already exist (Caddy con

### Export

1. Query all servers (name, host, port, user — **no SSH keys**)
1. Query all servers (name, host, port, user, SSH key)
2. Query all apps (name, domain, port, compose, git_repo, git_branch, auth_user, auth_pass, compose_mode, server name mapping)
3. Query all app secrets and config files
4. If passphrase provided: encrypt secrets, auth_pass, and file contents with AES-GCM (PBKDF2 key derivation, 600,000 iterations, `enc:` prefix)
4. If passphrase provided: encrypt SSH keys, secrets, auth_pass, and file contents with AES-GCM (PBKDF2 key derivation, 600,000 iterations, `enc:` prefix)
5. Generate YAML document
6. Download as `dockify-config.yaml`

### Import

1. Parse YAML file
2. If passphrase provided: attempt decryption of all `enc:` values to validate passphrase
2. If passphrase provided: attempt decryption of all `enc:` values to validate passphrase (including SSH keys)
3. In **merge** mode (default): skip entries that already exist by same name
4. In **replace** mode: delete all existing servers and apps, then import
5. Create servers (user must re-enter SSH keys after import)
5. Create servers with SSH key from export (fallback to `"pending"` if key is empty — user must enter key manually)
6. Create apps (status = created, ready for deploy)
7. Import secrets and config files
8. Redirect to servers page
Expand Down
45 changes: 36 additions & 9 deletions internal/backup/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ type ExportData struct {
}

type ExportServer struct {
Name string `yaml:"name"`
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
Name string `yaml:"name"`
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
SSHKey string `yaml:"ssh_key,omitempty"`
}

type ExportSecret struct {
Expand Down Expand Up @@ -157,12 +158,23 @@ func (s *Service) Export(passphrase string) (string, error) {
serverNameMap := map[int64]string{}
for _, svr := range servers {
serverNameMap[svr.ID] = svr.Name
data.Servers = append(data.Servers, ExportServer{
es := ExportServer{
Name: svr.Name,
Host: svr.Host,
Port: svr.Port,
User: svr.User,
})
}
if svr.SSHKey != "" {
if passphrase != "" {
es.SSHKey, err = encrypt(svr.SSHKey, passphrase)
if err != nil {
return "", fmt.Errorf("encrypt ssh_key for %q: %w", svr.Name, err)
}
} else {
es.SSHKey = svr.SSHKey
}
}
data.Servers = append(data.Servers, es)
}

for _, a := range apps {
Expand Down Expand Up @@ -229,9 +241,8 @@ func (s *Service) Export(passphrase string) (string, error) {

sb := strings.Builder{}
sb.WriteString("# Dockify Configuration Export\n")
sb.WriteString("# Auth passwords (if present) are included as saved. SSH keys are not exported.\n")
if passphrase != "" {
sb.WriteString("# Secret values and config file contents are encrypted with the provided passphrase.\n")
sb.WriteString("# All sensitive values (secrets, auth passwords, config files, SSH keys) are encrypted.\n")
}
sb.WriteString("# Remove or edit entries before import as needed.\n")
sb.Write(out)
Expand All @@ -250,6 +261,11 @@ func (s *Service) Import(yamlData, passphrase, mode string) (string, error) {

var logLines []string

for _, es := range data.Servers {
if err := validateDecrypt(es.SSHKey, passphrase); err != nil {
return "", fmt.Errorf("server %q ssh_key: %w", es.Name, err)
}
}
for _, ea := range data.Apps {
if err := validateDecrypt(ea.AuthPass, passphrase); err != nil {
return "", fmt.Errorf("%q: %w", ea.Name, err)
Expand Down Expand Up @@ -289,12 +305,23 @@ func (s *Service) Import(yamlData, passphrase, mode string) (string, error) {
serverIDs[es.Name] = existing.ID
continue
}
sshKey := es.SSHKey
if passphrase != "" && es.SSHKey != "" {
sshKey, err = decrypt(es.SSHKey, passphrase)
if err != nil {
return strings.Join(logLines, "\n"), fmt.Errorf("server %q: wrong passphrase or corrupted data", es.Name)
}
}
if sshKey == "" {
sshKey = "pending"
}

svr := &server.Server{
Name: es.Name,
Host: es.Host,
Port: es.Port,
User: es.User,
SSHKey: "pending",
SSHKey: sshKey,
Status: "pending",
}
if svr.Port == 0 {
Expand Down
8 changes: 4 additions & 4 deletions internal/http/templates/export.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<h3>Download Backup</h3>
<p style="color:var(--text-muted)">
Export all servers and apps configuration to a YAML file.
SSH keys are <strong>not</strong> exported — you must re-add them after import.
SSH keys are included and encrypted with your passphrase.
</p>

<label style="display:block;margin-bottom:0.5em">
Expand All @@ -31,8 +31,8 @@ <h3>Download Backup</h3>
</label>

<p style="color:var(--orange);font-size:0.85em">
⚠ Secret values will be encrypted with this passphrase.
Store it safely — without it you cannot import this backup.
⚠ All sensitive values (secrets, auth passwords, SSH keys, config files) will be encrypted.
Store the passphrase safely — without it you cannot import this backup.
</p>

<button type="submit" class="btn btn-primary">Download YAML</button>
Expand All @@ -42,7 +42,7 @@ <h3>Download Backup</h3>
<div class="card" style="margin-top:1em">
<h3>What's exported</h3>
<ul style="color:var(--text-muted);font-size:0.9em">
<li>Server: name, host, port, user (no SSH key)</li>
<li>Server: name, host, port, user, SSH key (encrypted with your passphrase)</li>
<li>Apps: name, domain, port, compose, git repo/branch, auth credentials, secrets, config files</li>
<li>Secrets are encrypted with your passphrase</li>
<li>You can edit the YAML file to remove entries before importing</li>
Expand Down
Loading