-
Notifications
You must be signed in to change notification settings - Fork 0
feat(snap): automatic wallet backup on snap removal #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| #!/bin/bash | ||
| # Restore a diode-node backup created by the snap remove hook. | ||
| set -euo pipefail | ||
|
|
||
| usage() { | ||
| cat <<EOF | ||
| Usage: $0 <backup.tar.gz> | ||
|
|
||
| Restores node identity and wallet data from an automatic snap-removal backup. | ||
| Run after reinstalling diode-node and stopping the service: | ||
|
|
||
| sudo snap install diode-node | ||
| sudo snap connect diode-node:backup-dir | ||
| sudo snap stop diode-node.service | ||
| sudo snap run --shell diode-node -c 'bin/restore_snap_backup /var/backups/diode-node/diode_node_backup_YYYY-MM-DD_HHMMSS.tar.gz' | ||
| sudo snap start diode-node.service | ||
| EOF | ||
| } | ||
|
|
||
| if [[ $# -ne 1 ]]; then | ||
| usage | ||
| exit 1 | ||
| fi | ||
|
|
||
| backup_file="$1" | ||
|
|
||
| if [[ ! -f "$backup_file" ]]; then | ||
| echo "Backup file not found: $backup_file" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| if [[ -z "${SNAP:-}" || -z "${SNAP_DATA:-}" || -z "${SNAP_USER_DATA:-}" ]]; then | ||
| echo "Run inside the diode-node snap (connect backup-dir first):" >&2 | ||
| echo " sudo snap connect diode-node:backup-dir" >&2 | ||
| echo " sudo snap run --shell diode-node -c 'bin/restore_snap_backup <backup.tar.gz>'" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| umask 077 | ||
| staging_dir=$(mktemp -d) | ||
| trap 'rm -rf "$staging_dir"' EXIT | ||
|
|
||
| tar -xzf "$backup_file" -C "$staging_dir" | ||
|
|
||
| if [[ -d "$staging_dir/snap_user_data" ]]; then | ||
| mkdir -p "$SNAP_USER_DATA" | ||
| cp -a "$staging_dir/snap_user_data/." "$SNAP_USER_DATA/" | ||
| fi | ||
|
|
||
| if [[ -d "$staging_dir/snap_data" ]]; then | ||
| mkdir -p "$SNAP_DATA" | ||
| cp -a "$staging_dir/snap_data/." "$SNAP_DATA/" | ||
| fi | ||
|
|
||
| echo "Restored backup into $SNAP_DATA and $SNAP_USER_DATA" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,101 @@ | ||||||||||||||||||||||||
| #!/bin/bash | ||||||||||||||||||||||||
| # Automatic wallet backup when the diode-node snap is removed. | ||||||||||||||||||||||||
| # Invoked from snap/hooks/remove; BACKUP_DIR can be overridden for tests. | ||||||||||||||||||||||||
| set -euo pipefail | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| BACKUP_DIR="${BACKUP_DIR:-/var/backups/diode-node}" | ||||||||||||||||||||||||
| SNAP_NAME="${SNAP_NAME:-diode-node}" | ||||||||||||||||||||||||
| staging_dir="" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| log() { | ||||||||||||||||||||||||
| echo "diode-node remove hook: $*" >&2 | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| stage_critical_files() { | ||||||||||||||||||||||||
| local snap_data="$1" | ||||||||||||||||||||||||
| local snap_user_data="$2" | ||||||||||||||||||||||||
| local staging_dir="$3" | ||||||||||||||||||||||||
| local has_data=0 | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if [[ -f "$snap_user_data/node" ]]; then | ||||||||||||||||||||||||
| mkdir -p "$staging_dir/snap_user_data" | ||||||||||||||||||||||||
| cp "$snap_user_data/node" "$staging_dir/snap_user_data/" | ||||||||||||||||||||||||
| has_data=1 | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if [[ -d "$snap_data" ]]; then | ||||||||||||||||||||||||
| for nodedata in "$snap_data"/nodedata_*; do | ||||||||||||||||||||||||
| if [[ -d "$nodedata" ]]; then | ||||||||||||||||||||||||
| mkdir -p "$staging_dir/snap_data" | ||||||||||||||||||||||||
| cp -a "$nodedata" "$staging_dir/snap_data/" | ||||||||||||||||||||||||
| has_data=1 | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
| done | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| echo "$has_data" | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| write_manifest() { | ||||||||||||||||||||||||
| local staging_dir="$1" | ||||||||||||||||||||||||
| local snap_data="$2" | ||||||||||||||||||||||||
| local snap_user_data="$3" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| cat >"$staging_dir/manifest.txt" <<EOF | ||||||||||||||||||||||||
| diode-node automatic backup | ||||||||||||||||||||||||
| created: $(date -u +%Y-%m-%dT%H:%M:%SZ) | ||||||||||||||||||||||||
| snap: ${SNAP_NAME} | ||||||||||||||||||||||||
| snap_data: ${snap_data} | ||||||||||||||||||||||||
| snap_user_data: ${snap_user_data} | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Restore with bin/restore_snap_backup after reinstalling the snap | ||||||||||||||||||||||||
| (connect backup-dir: sudo snap connect diode-node:backup-dir). | ||||||||||||||||||||||||
| If this backup is missing, use "snap saved" within 31 days of removal. | ||||||||||||||||||||||||
| EOF | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| main() { | ||||||||||||||||||||||||
| umask 077 | ||||||||||||||||||||||||
| local snap_data="${SNAP_DATA:-}" | ||||||||||||||||||||||||
| local snap_user_data="${SNAP_USER_DATA:-}" | ||||||||||||||||||||||||
|
dominicletz marked this conversation as resolved.
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if [[ -z "$snap_data" || -z "$snap_user_data" ]]; then | ||||||||||||||||||||||||
| log "SNAP_DATA or SNAP_USER_DATA not set, skipping backup" | ||||||||||||||||||||||||
| exit 0 | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| staging_dir=$(mktemp -d) | ||||||||||||||||||||||||
| trap '[[ -n "$staging_dir" ]] && rm -rf "$staging_dir"' EXIT | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| local has_data | ||||||||||||||||||||||||
| has_data=$(stage_critical_files "$snap_data" "$snap_user_data" "$staging_dir") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if [[ "$has_data" -eq 0 ]]; then | ||||||||||||||||||||||||
| log "no critical node data found, skipping backup" | ||||||||||||||||||||||||
| exit 0 | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| write_manifest "$staging_dir" "$snap_data" "$snap_user_data" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if ! mkdir -p "$BACKUP_DIR" 2>/dev/null; then | ||||||||||||||||||||||||
| log "cannot create backup directory $BACKUP_DIR (is backup-dir plug connected?)" | ||||||||||||||||||||||||
| log "use 'snap saved' within 31 days to recover data via 'snap restore'" | ||||||||||||||||||||||||
| exit 0 | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
|
Comment on lines
+80
to
+84
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For better security hardening, consider applying
Suggested change
|
||||||||||||||||||||||||
| chmod 0700 "$BACKUP_DIR" 2>/dev/null || true | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| local timestamp backup_file | ||||||||||||||||||||||||
| timestamp=$(date -u +%Y-%m-%d_%H%M%S) | ||||||||||||||||||||||||
| backup_file="$BACKUP_DIR/diode_node_backup_${timestamp}.tar.gz" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if ! tar -czf "$backup_file" -C "$staging_dir" .; then | ||||||||||||||||||||||||
| log "failed to write backup archive to $backup_file" | ||||||||||||||||||||||||
| exit 0 | ||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| chmod 0600 "$backup_file" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| log "wallet backup saved to $backup_file" | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| main "$@" | ||||||||||||||||||||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| #!/bin/sh | ||
| # Runs before snap data is deleted. See scripts/snap_backup_on_remove.sh. | ||
| exec "$SNAP/bin/backup_on_remove" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| defmodule SnapBackupOnRemoveTest do | ||
| use ExUnit.Case, async: true | ||
|
|
||
| @script Path.expand("../scripts/snap_backup_on_remove.sh", __DIR__) | ||
|
|
||
| setup do | ||
| root = Path.join(System.tmp_dir!(), "snap_backup_test_#{System.unique_integer([:positive])}") | ||
| snap_data = Path.join(root, "snap_data") | ||
| snap_user_data = Path.join(root, "snap_user_data") | ||
| backup_dir = Path.join(root, "backups") | ||
| extract_dir = Path.join(root, "extract") | ||
|
|
||
| on_exit(fn -> File.rm_rf!(root) end) | ||
|
|
||
| %{ | ||
| root: root, | ||
| snap_data: snap_data, | ||
| snap_user_data: snap_user_data, | ||
| backup_dir: backup_dir, | ||
| extract_dir: extract_dir | ||
| } | ||
| end | ||
|
|
||
| defp run_backup(context) do | ||
| env = [ | ||
| {"SNAP_DATA", context.snap_data}, | ||
| {"SNAP_USER_DATA", context.snap_user_data}, | ||
| {"SNAP_NAME", "diode-node"}, | ||
| {"BACKUP_DIR", context.backup_dir} | ||
| ] | ||
|
|
||
| System.cmd("bash", [@script], env: env, stderr_to_stdout: true) | ||
| end | ||
|
|
||
| test "creates tarball with node identity and nodedata", context do | ||
| File.mkdir_p!(Path.join(context.snap_data, "nodedata_prod")) | ||
| File.mkdir_p!(context.snap_user_data) | ||
|
|
||
| File.write!(Path.join(context.snap_user_data, "node"), "diode_node@host.diode\n") | ||
| File.write!(Path.join(context.snap_data, "nodedata_prod/wallet.dat"), "wallet-secrets") | ||
|
|
||
| {output, 0} = run_backup(context) | ||
| assert output =~ "wallet backup saved" | ||
|
|
||
| [backup_name] = File.ls!(context.backup_dir) | ||
| assert backup_name =~ ~r/^diode_node_backup_.*\.tar\.gz$/ | ||
|
|
||
| backup_path = Path.join(context.backup_dir, backup_name) | ||
| assert File.stat!(backup_path).mode &&& 0o777 == 0o600 | ||
| assert File.stat!(context.backup_dir).mode &&& 0o777 == 0o700 | ||
|
|
||
| File.mkdir_p!(context.extract_dir) | ||
|
|
||
| {_, 0} = | ||
| System.cmd("tar", ["-xzf", backup_path, "-C", context.extract_dir], stderr_to_stdout: true) | ||
|
|
||
| assert File.read!(Path.join(context.extract_dir, "snap_user_data/node")) == | ||
| "diode_node@host.diode\n" | ||
|
|
||
| assert File.read!(Path.join(context.extract_dir, "snap_data/nodedata_prod/wallet.dat")) == | ||
| "wallet-secrets" | ||
|
|
||
| manifest = File.read!(Path.join(context.extract_dir, "manifest.txt")) | ||
| assert manifest =~ "diode-node automatic backup" | ||
| assert manifest =~ context.snap_data | ||
| end | ||
|
|
||
| test "does not backup erl_inetrc even when present", context do | ||
| File.mkdir_p!(Path.join(context.snap_data, "nodedata_prod")) | ||
| File.mkdir_p!(context.snap_user_data) | ||
|
|
||
| File.write!(Path.join(context.snap_user_data, "node"), "diode_node@host.diode\n") | ||
|
|
||
| File.write!( | ||
| Path.join(context.snap_user_data, "erl_inetrc"), | ||
| "{host, {127,0,0,1}, [\"host.diode\", \"diode_node@host.diode\"]}.\n" | ||
| ) | ||
|
|
||
| File.write!(Path.join(context.snap_data, "nodedata_prod/wallet.dat"), "wallet-secrets") | ||
|
|
||
| {output, 0} = run_backup(context) | ||
| assert output =~ "wallet backup saved" | ||
|
|
||
| [backup_name] = File.ls!(context.backup_dir) | ||
| backup_path = Path.join(context.backup_dir, backup_name) | ||
| File.mkdir_p!(context.extract_dir) | ||
|
|
||
| {_, 0} = | ||
| System.cmd("tar", ["-xzf", backup_path, "-C", context.extract_dir], stderr_to_stdout: true) | ||
|
|
||
| refute File.exists?(Path.join(context.extract_dir, "snap_user_data/erl_inetrc")) | ||
| end | ||
|
|
||
| test "skips backup when no critical data exists", context do | ||
| File.mkdir_p!(context.snap_data) | ||
| File.mkdir_p!(context.snap_user_data) | ||
|
|
||
| {output, 0} = run_backup(context) | ||
| assert output =~ "no critical node data found" | ||
| refute File.exists?(context.backup_dir) | ||
| end | ||
|
|
||
| test "does not fail removal when backup directory is not writable", context do | ||
| File.mkdir_p!(context.snap_user_data) | ||
| File.write!(Path.join(context.snap_user_data, "node"), "diode_node@test.diode\n") | ||
|
|
||
| env = [ | ||
| {"SNAP_DATA", context.snap_data}, | ||
| {"SNAP_USER_DATA", context.snap_user_data}, | ||
| {"SNAP_NAME", "diode-node"}, | ||
| {"BACKUP_DIR", "/root/diode-node-backup-should-not-exist"} | ||
| ] | ||
|
|
||
| {output, 0} = | ||
| System.cmd("bash", [@script], env: env, stderr_to_stdout: true) | ||
|
|
||
| assert output =~ "cannot create backup directory" | ||
| assert output =~ "snap saved" | ||
| end | ||
| end |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting a restrictive
umaskis recommended when handling sensitive data like wallet backups to ensure that any temporary files or directories created during the restoration process are not world-readable.