Skip to content

Commit 21df406

Browse files
committed
AES encryption for dump files
1 parent 8182954 commit 21df406

File tree

8 files changed

+190
-43
lines changed

8 files changed

+190
-43
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88
### Added
9-
- Add options to change the internal network name/target alias
10-
- Add option to change gzip compression level. Default: 6
9+
- AES Encryption of dump files
10+
- Option to change gzip compression level. Default: 6
11+
- Options to change the internal network name/target alias
1112

1213
## [0.1.0] - 2021-09-04
1314
### Added

Dockerfile

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.9-alpine
1+
FROM python:3.9-alpine AS base
22

33
LABEL org.opencontainers.image.ref.name="jan-di/database-backup"
44

@@ -8,15 +8,27 @@ RUN set -eux; \
88
postgresql-client \
99
tzdata
1010

11+
FROM base AS python-deps
12+
1113
RUN set -eux; \
14+
apk --no-cache add \
15+
# cryptography
16+
gcc musl-dev python3-dev libffi-dev openssl-dev cargo \
17+
; \
1218
pip install pipenv
1319

14-
WORKDIR /app
15-
1620
COPY Pipfile .
1721
COPY Pipfile.lock .
22+
ENV PIPENV_VENV_IN_PROJECT=1
1823
RUN set -eux; \
19-
pipenv install --system
24+
pipenv install --deploy
25+
26+
FROM base
27+
28+
COPY --from=python-deps /.venv /.venv
29+
ENV PATH="/.venv/bin:$PATH"
30+
31+
WORKDIR /app
2032

2133
RUN set -eux; \
2234
mkdir -p /dump

Pipfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ name = "pypi"
77
docker = "*"
88
humanize = "*"
99
requests = "*"
10+
pyaescrypt = "*"
1011

1112
[dev-packages]
1213

Pipfile.lock

Lines changed: 90 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ Name | Default | Description
4343
`port` | `auto` | Port (inside container). Possible values: `auto` or a valid port number. Auto gets the default port corresponding to the type.
4444
`compress` | `false` | Compress SQL Dump with gzip
4545
`compression_level` | `6` | Gzip compression level (1-9)
46+
`encrypt` | `false` | Encrypt SQL Dump with AES
47+
`encryption_key` | (none) | Key/Passphrase used to encrypt
4648

4749
## Example
4850

main.py

Lines changed: 71 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import time
66
import sys
77

8+
import pyAesCrypt
89
import humanize
910

1011
from src.database import Database, DatabaseType
@@ -47,6 +48,8 @@
4748

4849
for i, container in enumerate(containers):
4950
database = Database(container, global_labels)
51+
dump_file = f"/dump/{container.name}.sql"
52+
failed = False
5053

5154
logging.info(
5255
"[{}/{}] Processing container {} {} ({})".format(
@@ -58,25 +61,22 @@
5861
)
5962
)
6063

64+
if database.type == DatabaseType.unknown:
65+
logging.error(
66+
"FAILED: Cannot read database type. Please specify via label."
67+
)
68+
failed = True
69+
6170
logging.debug(
6271
"Login {}@host:{} using Password: {}".format(
6372
database.username,
6473
database.port,
6574
"YES" if len(database.password) > 0 else "NO",
6675
)
6776
)
68-
if database.compress:
69-
logging.debug("Compressing backup")
70-
71-
if database.type == DatabaseType.unknown:
72-
logging.error(
73-
"FAILED: Cannot read database type. Please specify via label."
74-
)
7577

78+
# Create dump
7679
network.connect(container, aliases=[config.docker_target_name])
77-
dumpFile = f"/dump/{container.name}.sql"
78-
error_code = 0
79-
error_text = ""
8080

8181
try:
8282
env = os.environ.copy()
@@ -95,7 +95,7 @@
9595
f" --ignore-database=mysql"
9696
f" --ignore-database=information_schema"
9797
f" --ignore-database=performance_schema"
98-
f" > {dumpFile}"
98+
f" > {dump_file}"
9999
),
100100
shell=True,
101101
text=True,
@@ -109,53 +109,92 @@
109109
f"pg_dumpall"
110110
f" --host={config.docker_target_name}"
111111
f" --username={database.username}"
112-
f" > {dumpFile}"
112+
f" > {dump_file}"
113113
),
114114
shell=True,
115115
text=True,
116116
capture_output=True,
117117
env=env,
118118
).check_returncode()
119119
except subprocess.CalledProcessError as e:
120-
error_code = e.returncode
121120
error_text = f"\n{e.stderr.strip()}".replace("\n", "\n> ").strip()
121+
logging.error(
122+
f"FAILED. Error while crating dump. Return Code: {e.returncode}; Error Output:"
123+
)
124+
logging.error(f"{error_text}")
125+
failed = True
126+
127+
if not failed and not os.path.exists(dump_file):
128+
logging.error(
129+
f"FAILED: Dump cannot be created due to an unknown error!"
130+
)
131+
failed = True
122132

123133
network.disconnect(container)
124134

125-
if error_code > 0:
126-
logging.error(f"FAILED. Return Code: {error_code}; Error Output:")
127-
logging.error(f"{error_text}")
128-
elif os.path.exists(dumpFile):
129-
dump_size = os.path.getsize(dumpFile)
135+
dump_size = os.path.getsize(dump_file)
136+
137+
# Compress pump
138+
if not failed and database.compress and dump_size > 0:
139+
logging.debug(f"Compressing dump (level: {database.compression_level})")
140+
compressed_dump_file = f"{dump_file}.gz"
141+
142+
try:
143+
if os.path.exists(compressed_dump_file):
144+
os.remove(compressed_dump_file)
130145

131-
# Compress pump
132-
if database.compress and dump_size > 0:
133-
if os.path.exists(dumpFile + ".gz"):
134-
os.remove(dumpFile + ".gz")
135146
subprocess.check_output(
136-
f'gzip -{database.compression_level} "{dumpFile}"', shell=True
147+
f'gzip -{database.compression_level} "{dump_file}"', shell=True
148+
)
149+
except Exception as e:
150+
logging.error(f"FAILED: Error while compressing: {e}")
151+
failed = True
152+
153+
processed_dump_size = os.path.getsize(compressed_dump_file)
154+
dump_file = compressed_dump_file
155+
else:
156+
database.compress = False
157+
158+
# Encrypt dump
159+
if not failed and database.encrypt and dump_size > 0:
160+
logging.debug(f"Encrypting dump")
161+
encrypted_dump_file = f"{dump_file}.aes"
162+
163+
try:
164+
if os.path.exists(encrypted_dump_file):
165+
os.remove(encrypted_dump_file)
166+
167+
pyAesCrypt.encryptFile(
168+
dump_file, encrypted_dump_file, database.encryption_key
137169
)
138-
dumpFile = dumpFile + ".gz"
139-
compressed_size = os.path.getsize(dumpFile)
140-
else:
141-
database.compress = False
170+
os.remove(dump_file)
171+
except Exception as e:
172+
logging.error(f"FAILED: Error while encrypting: {e}")
173+
failed = True
174+
175+
processed_dump_size = os.path.getsize(encrypted_dump_file)
176+
dump_file = encrypted_dump_file
142177

178+
else:
179+
database.encrypt = False
180+
181+
if not failed:
143182
# Change Owner of dump
144183
os.chown(
145-
dumpFile, config.dump_uid, config.dump_gid
184+
dump_file, config.dump_uid, config.dump_gid
146185
) # pylint: disable=maybe-no-member
147186

148187
successful_count += 1
149188
logging.info(
150189
"SUCCESS. Size: {}{}".format(
151190
humanize.naturalsize(dump_size),
152-
" (" + humanize.naturalsize(compressed_size) + " compressed)"
153-
if database.compress
191+
" ("
192+
+ humanize.naturalsize(processed_dump_size)
193+
+ " compressed/encrypted)"
194+
if database.compress or database.encrypt
154195
else "",
155196
)
156197
)
157-
else:
158-
logging.error("Dump file not found!")
159198

160199
network.disconnect(own_container.id)
161200
network.remove()

src/database.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ def _load_labels(self, values):
5656
if "port" in values: self.port = values["port"]
5757
if "username" in values: self.username = values["username"]
5858
if "password" in values: self.password = values["password"]
59-
if "compress" in values: self.compress = values["compress"]
59+
if "compress" in values: self.compress = distutils.util.strtobool(values["compress"])
6060
if "compression_level" in values: self.compression_level = values["compression_level"]
61+
if "encrypt" in values: self.encrypt = distutils.util.strtobool(values["encrypt"])
62+
if "encryption_key" in values: self.encryption_key = values["encryption_key"]
6163

6264
def _get_labels_from_container(self, container):
6365
labels = {}
@@ -92,5 +94,4 @@ def _resolve_labels(self, container):
9294
self.port = defaults.get("port")
9395
else:
9496
self.port = int(self.port)
95-
96-
self.compress = distutils.util.strtobool(self.compress)
97+

src/settings.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
"type": "auto",
2222
"port": "auto",
2323
"compress": "false",
24-
"compression_level": 6
24+
"compression_level": 6,
25+
"encrypt": "true",
26+
"encryption_key": None
2527
}
2628

2729
class Config:

0 commit comments

Comments
 (0)