Skip to content

Commit 21005a3

Browse files
committed
feat(backup): add support for asymmetric encryption
1 parent 4d11d76 commit 21005a3

File tree

10 files changed

+313
-88
lines changed

10 files changed

+313
-88
lines changed

k8s/backup-key.secret.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: v1
2+
kind: Secret
3+
metadata:
4+
name: lifemonitor-api-backup-key
5+
type: Opaque
6+
data:
7+
encryptionKey: <base64-encoded-encryption-key>

k8s/templates/_helpers.tpl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ Define lifemonitor TLS secret name
6767
{{- printf "%s-tls" .Release.Name }}
6868
{{- end }}
6969

70+
{{/*
71+
Define lifemonitor secret name for backup key
72+
*/}}
73+
{{- define "chart.lifemonitor.backup.key" -}}
74+
{{- printf "%s-backup-key" .Release.Name }}
75+
{{- end }}
76+
7077

7178
{{/*
7279
Define volume name of LifeMonitor backup data

k8s/templates/backup.job.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,22 @@ spec:
2929
{{- include "lifemonitor.common-volume-mounts" . | nindent 12 }}
3030
- name: lifemonitor-backup
3131
mountPath: "/var/data/backup"
32+
{{- if .Values.backup.encryptionKeySecret }}
33+
- name: lifemonitor-backup-encryption-key
34+
mountPath: "/lm/backup/encryption.key"
35+
subPath: encryptionKey
36+
{{- end }}
3237
restartPolicy: OnFailure
3338
volumes:
3439
{{- include "lifemonitor.common-volume" . | nindent 10 }}
3540
- name: lifemonitor-backup
3641
persistentVolumeClaim:
3742
claimName: {{ .Values.backup.existingClaim }}
43+
{{- if .Values.backup.encryptionKeySecret }}
44+
- name: lifemonitor-backup-encryption-key
45+
secret:
46+
secretName: {{ .Values.backup.encryptionKeySecret }}
47+
{{- end }}
3848
{{- with .Values.lifemonitor.nodeSelector }}
3949
nodeSelector:
4050
{{- toYaml . | nindent 10 }}

k8s/templates/settings.secret.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ stringData:
7878
{{- if .Values.backup.retain_days }}
7979
BACKUP_RETAIN_DAYS={{ .Values.backup.retain_days }}
8080
{{- end }}
81-
{{- if .Values.backup.encryptionKey }}
82-
BACKUP_ENCRYPTION_KEY={{ .Values.backup.encryptionKey }}
81+
{{- if .Values.backup.encryptionKeySecret }}
82+
BACKUP_ENCRYPTION_KEY_PATH=/lm/backup/encryption.key
8383
{{- end }}
8484
{{- if .Values.backup.remote.enabled }}
8585
BACKUP_REMOTE_PATH={{ .Values.backup.remote.path }}

k8s/values.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ backup:
120120
successfulJobsHistoryLimit: 30
121121
failedJobsHistoryLimit: 30
122122
existingClaim: data-api-backup
123-
# encryptionKey: <YOUR_ENCRYPTION_KEY>
123+
# encryptionKeySecret: lifemonitor-api-backup-key
124124
# Settings to mirror the (cluster) local backup
125125
# to a remote site via FTPS or SFTP
126126
remote:

lifemonitor/commands/backup.py

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from flask.blueprints import Blueprint
3434
from flask.cli import with_appcontext
3535
from flask.config import Config
36+
3637
from lifemonitor.utils import FtpUtils, encrypt_folder
3738

3839
from .db import backup, backup_options
@@ -51,6 +52,9 @@
5152
encryption_key_file_option = click.option("-kf", "--encryption-key-file",
5253
type=click.File("rb"), default=None,
5354
help="File containing the encryption key")
55+
encryption_asymmetric_option = click.option("-a", "--encryption-asymmetric", is_flag=True, default=False,
56+
show_default=True,
57+
help="Use asymmetric encryption")
5458

5559

5660
class RequiredIf(GroupedOption):
@@ -120,17 +124,29 @@ def bck(ctx):
120124
@backup_options
121125
@synch_otptions
122126
@with_appcontext
123-
def db_cmd(file, directory, encryption_key, encryption_key_file, verbose, *args, **kwargs):
127+
def db_cmd(file, directory,
128+
encryption_key, encryption_key_file, encryption_asymmetric,
129+
verbose, *args, **kwargs):
124130
"""
125131
Make a backup of the database
126132
"""
127-
result = backup_db(directory, file, encryption_key, encryption_key_file, verbose, *args, **kwargs)
133+
result = backup_db(directory, file,
134+
encryption_key=encryption_key,
135+
encryption_key_file=encryption_key_file,
136+
encryption_asymmetric=encryption_asymmetric,
137+
verbose=verbose, *args, **kwargs)
128138
sys.exit(result)
129139

130140

131-
def backup_db(directory, file=None, encryption_key=None, encryption_key_file=None, verbose=False, *args, **kwargs):
141+
def backup_db(directory, file=None,
142+
encryption_key=None, encryption_key_file=None, encryption_asymmetric=False,
143+
verbose=False, *args, **kwargs):
132144
logger.debug(sys.argv)
133-
result = backup(directory, file, encryption_key, encryption_key_file, verbose)
145+
logger.debug("Backup DB: %r - %r - %r - %r - %r - %r - %r",)
146+
logger.warning(f"Encryption asymmetric: {encryption_asymmetric}")
147+
result = backup(directory, file,
148+
encryption_key=encryption_key, encryption_key_file=encryption_key_file,
149+
encryption_asymmetric=encryption_asymmetric, verbose=verbose)
134150
if result.returncode == 0:
135151
synch = kwargs.pop('synch', False)
136152
if synch:
@@ -143,20 +159,25 @@ def backup_db(directory, file=None, encryption_key=None, encryption_key_file=Non
143159
help="Local path to store RO-Crates")
144160
@encryption_key_option
145161
@encryption_key_file_option
162+
@encryption_asymmetric_option
146163
@synch_otptions
147164
@with_appcontext
148-
def crates_cmd(directory, encryption_key, encryption_key_file, *args, **kwargs):
165+
def crates_cmd(directory,
166+
encryption_key, encryption_key_file, encryption_asymmetric,
167+
*args, **kwargs):
149168
"""
150169
Make a backup of the registered workflow RO-Crates
151170
"""
152171
result = backup_crates(current_app.config, directory,
153-
encryption_key, encryption_key_file,
172+
encryption_key=encryption_key, encryption_key_file=encryption_key_file,
173+
encryption_asymmetric=encryption_asymmetric,
154174
*args, **kwargs)
155175
sys.exit(result.returncode)
156176

157177

158178
def backup_crates(config, directory,
159179
encryption_key: bytes = None, encryption_key_file: BinaryIO = None,
180+
encryption_asymmetric: bool = False,
160181
*args, **kwargs) -> subprocess.CompletedProcess:
161182
assert config.get("DATA_WORKFLOWS", None), "DATA_WORKFLOWS not configured"
162183
# get the path of the RO-Crates
@@ -169,19 +190,23 @@ def backup_crates(config, directory,
169190
if encryption_key or encryption_key_file:
170191
if not encryption_key:
171192
encryption_key = encryption_key_file.read()
172-
result = encrypt_folder(rocrate_source_path, directory, encryption_key)
173-
result = subprocess.CompletedProcess(returncode=0 if result else 1, args=())
193+
result = encrypt_folder(rocrate_source_path, directory, encryption_key,
194+
encryption_asymmetric=encryption_asymmetric)
195+
result = subprocess.CompletedProcess(returncode=0 if result else 1, args=())
174196
else:
175197
result = subprocess.run(f'rsync -avh --delete {rocrate_source_path}/ {directory} ',
176198
shell=True, capture_output=True)
177-
if result:
199+
if result.returncode == 0:
178200
print("Created backup of workflow RO-Crates @ '%s'" % directory)
179201
synch = kwargs.pop('synch', False)
180202
if synch:
181203
logger.debug("Remaining args: %r", kwargs)
182204
return __remote_synch__(source=directory, **kwargs)
183205
else:
184-
print("Unable to backup workflow RO-Crates\n%s", result.stderr.decode())
206+
try:
207+
print("Unable to backup workflow RO-Crates\n%s", result.stderr.decode())
208+
except Exception:
209+
print("Unable to backup workflow RO-Crates\n")
185210
return result
186211

187212

@@ -192,20 +217,36 @@ def auto(config: Config):
192217
click.echo("No BACKUP_LOCAL_PATH found in your settings")
193218
sys.exit(0)
194219

195-
# search for an encryption key
196-
encryption_key = config.get("BACKUP_ENCRYPTION_KEY", None)
220+
# search for an encryption key file
221+
encryption_key = None
222+
encryption_key_file = config.get("BACKUP_ENCRYPTION_KEY_PATH", None)
223+
if not encryption_key_file:
224+
click.echo("WARNING: No BACKUP_ENCRYPTION_KEY_PATH found in your settings")
225+
logger.warning("No BACKUP_ENCRYPTION_KEY_PATH found in your settings")
226+
else:
227+
# read the encryption key from the file if the key is not provided
228+
if isinstance(encryption_key_file, str):
229+
with open(encryption_key_file, "rb") as encryption_key_file:
230+
encryption_key = encryption_key_file.read()
231+
elif isinstance(encryption_key_file, BinaryIO):
232+
encryption_key = encryption_key_file.read()
233+
else:
234+
raise ValueError("Invalid encryption key file")
197235

198236
# set paths
199237
base_path = base_path.removesuffix('/') # remove trailing '/'
200238
db_backups = f"{base_path}/db"
201239
rc_backups = f"{base_path}/crates"
202240
logger.debug("Backup paths: %r - %r - %r", base_path, db_backups, rc_backups)
203241
# backup database
204-
result = backup(db_backups, encryption_key=encryption_key)
242+
result = backup(db_backups,
243+
encryption_key=encryption_key,
244+
encryption_asymmetric=True)
205245
if result.returncode != 0:
206246
sys.exit(result.returncode)
207247
# backup crates
208-
result = backup_crates(config, rc_backups, encryption_key=encryption_key)
248+
result = backup_crates(config, rc_backups,
249+
encryption_key=encryption_key, encryption_asymmetric=True)
209250
if result.returncode != 0:
210251
sys.exit(result.returncode)
211252
# clean up old files

lifemonitor/commands/db.py

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@
2424
import subprocess
2525
import sys
2626
from datetime import datetime
27+
from typing import BinaryIO
2728

2829
import click
2930
from flask import current_app
3031
from flask.cli import with_appcontext
3132
from flask_migrate import cli, current, stamp, upgrade
33+
3234
from lifemonitor.auth.models import User
33-
from lifemonitor.utils import encrypt_file, decrypt_file, hide_secret
35+
from lifemonitor.utils import decrypt_file, encrypt_file, hide_secret
3436

3537
# set module level logger
3638
logger = logging.getLogger()
@@ -109,11 +111,14 @@ def wait_for_db():
109111
encryption_key_file_option = click.option("-kf", "--encryption-key-file",
110112
type=click.File("rb"),
111113
default=None, help="File containing the encryption key")
114+
encryption_asymmetric_option = click.option("-a", "--encryption-asymmetric", is_flag=True, default=False,
115+
help="Use asymmetric encryption", show_default=True)
112116

113117

114118
def backup_options(func):
115119
# backup command options (evaluated in reverse order!)
116120
func = verbose_option(func)
121+
func = encryption_asymmetric_option(func)
117122
func = encryption_key_file_option(func)
118123
func = encryption_key_option(func)
119124
func = click.option("-f", "--file", default=None, help="Backup filename (default 'hhmmss_yyyymmdd.tar')")(func)
@@ -124,23 +129,34 @@ def backup_options(func):
124129
@cli.db.command("backup")
125130
@backup_options
126131
@with_appcontext
127-
def backup_cmd(directory, file, encryption_key, encryption_key_file, verbose):
132+
def backup_cmd(directory, file,
133+
encryption_key, encryption_key_file, encryption_asymmetric,
134+
verbose):
128135
"""
129136
Make a backup of the current app database
130137
"""
131-
logger.debug("%r - %r - %r - %r - %r", file, directory, encryption_key, encryption_key_file, verbose)
132-
result = backup(directory, file, encryption_key, encryption_key_file, verbose)
138+
logger.debug("%r - %r - %r - %r - %r - %r ", file, directory,
139+
encryption_key, encryption_key_file, encryption_asymmetric, verbose)
140+
result = backup(directory, file,
141+
encryption_key=encryption_key,
142+
encryption_key_file=encryption_key_file,
143+
encryption_asymmetric=encryption_asymmetric,
144+
verbose=verbose)
133145
# report exit code to the main process
134146
sys.exit(result.returncode)
135147

136148

137149
def backup(directory, file=None,
138-
encryption_key=None, encryption_key_file=None,
150+
encryption_key=None, encryption_key_file: BinaryIO = None,
151+
encryption_asymmetric=False,
139152
verbose=False) -> subprocess.CompletedProcess:
140153
"""
141154
Make a backup of the current app database
142155
"""
143-
logger.debug("%r - %r - %r - %r - %r", file, directory, encryption_key, encryption_key_file, verbose)
156+
logger.debug("%r - %r - %r - %r - %r - %r",
157+
file, directory,
158+
encryption_key, encryption_key_file, encryption_asymmetric,
159+
verbose)
144160
from lifemonitor.db import db_connection_params
145161
params = db_connection_params()
146162
if not file:
@@ -161,21 +177,23 @@ def backup(directory, file=None,
161177
msg = f"Encrypting backup file {target_path}..."
162178
logger.debug(msg)
163179
print(msg)
164-
165180
# read the encryption key from the file if the key is not provided
166181
if encryption_key is None:
167182
encryption_key = encryption_key_file.read()
168-
169183
# encrypt the backup file using the encryption key with the Fernet algorithm
170184
try:
171185
with open(target_path, "rb") as input_file:
172186
with open(target_path + ".enc", "wb") as output_file:
173-
encrypt_file(input_file, output_file, encryption_key, raise_error=True)
187+
encrypt_file(input_file, output_file, encryption_key,
188+
encryption_asymmetric=encryption_asymmetric,
189+
raise_error=True)
174190
# remove the original backup file
175191
os.remove(target_path)
176192
msg = f"Backup file {target_path} encrypted"
177193
logger.debug(msg)
178194
print(msg)
195+
except ValueError as e:
196+
logger.error("Unable to encrypt backup file '%s'. ERROR: %s", target_path, str(e))
179197
except Exception as e:
180198
print("Unable to encrypt backup file '%s'. ERROR: %s" % (target_path, str(e)))
181199
sys.exit(1)
@@ -192,9 +210,12 @@ def backup(directory, file=None,
192210
help="Preserve the current database renaming it as '<dbname>_yyyymmdd_hhmmss'")
193211
@encryption_key_option
194212
@encryption_key_file_option
213+
@encryption_asymmetric_option
195214
@verbose_option
196215
@with_appcontext
197-
def restore(file, safe, encryption_key, encryption_key_file, verbose):
216+
def restore(file, safe,
217+
encryption_key, encryption_key_file, encryption_asymmetric,
218+
verbose):
198219
"""
199220
Restore a backup of the app database
200221
"""
@@ -228,7 +249,8 @@ def restore(file, safe, encryption_key, encryption_key_file, verbose):
228249
file = file.removesuffix(".enc")
229250
with open(encrypted_file, "rb") as input_file:
230251
with open(file, "wb") as output_file:
231-
decrypt_file(input_file, output_file, encryption_key)
252+
decrypt_file(input_file, output_file,
253+
encryption_key, encryption_asymmetric=encryption_asymmetric)
232254
logger.debug("Decrypted backup file '%s' to '%s'", encrypted_file, file)
233255

234256
# check if delete or preserve the current app database (if exists)

0 commit comments

Comments
 (0)