Skip to content
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

feat: Encrypted Backups #14126

Closed
wants to merge 26 commits into from
Closed

feat: Encrypted Backups #14126

wants to merge 26 commits into from

Conversation

Anurag0911
Copy link
Contributor

@Anurag0911 Anurag0911 commented Sep 6, 2021

Problem: Backups were not encrypted.

Changes:

  • Added Encryption using GPG.
  • Checkbox to enable Encryption of backups
  • Button to Access the Encryption Key in the Download backups page.

Approach

  1. For Encryption

    • Creating .gpg file
    • Renaming the file
  2. For Decryption

    • Renaming file as .gpg
    • Decryption
    • Deletion of Encrypted file
    • Renaming
    • Rollbackup if any issues or error Occured
PR.Summary.mp4

CheckBox to enable/disable backup encryption

image

Backup Downloads page
Updates-

  • Icons to depict encrypted files
  • Button to get Encryption Key
    image
    image
  • Click on the 'Get Encryption Key' to look at the encryption key for the backups
    image

Backup encryption with files.

  • Use --backup-encryption-key flag to decrypt the file at the given path.
    image

  • No need to provide the encryption key on the same website as it is stored in the site config during the encryption.
    image

Compressed files encryption.

  • All the types of files are encrypted including .tgz
    image

Partial files encryption

  • Partial backup are also encrypted.
    image
    image

Rollback

  • The Encryption/Decryption is made as atomic as possible for any issues occurring during the processes.
    Examples:
    • Providing invalid key.
      image
      image

    • Providing wrong root password.
      image
      image

@gavindsouza gavindsouza changed the title Backup Encryption feat: Encrypted Backups Sep 6, 2021
@gavindsouza gavindsouza self-assigned this Sep 6, 2021
@Anurag0911 Anurag0911 marked this pull request as draft September 6, 2021 06:35
@codecov
Copy link

codecov bot commented Sep 6, 2021

Codecov Report

Merging #14126 (7ce0c54) into develop (b2f78de) will decrease coverage by 0.02%.
The diff coverage is 15.09%.

❗ Current head 7ce0c54 differs from pull request most recent head e92feb3. Consider uploading reports for the commit e92feb3 to get more accurate results

@@             Coverage Diff             @@
##           develop   #14126      +/-   ##
===========================================
- Coverage    56.25%   56.22%   -0.03%     
===========================================
  Files          499      503       +4     
  Lines        41290    41536     +246     
===========================================
+ Hits         23226    23352     +126     
- Misses       18064    18184     +120     

@abhishekbalam abhishekbalam self-assigned this Sep 9, 2021
@Anurag0911 Anurag0911 marked this pull request as ready for review September 10, 2021 07:35
@abhishekbalam
Copy link
Contributor

@Anurag0911

Create Docs and paste link in description.
Typo and case fixes.

@@ -47,6 +47,7 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin

@click.command('restore')
@click.argument('sql-file-path')
@click.option('--backup-encryption-key', help='Backup encryption Key')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@click.option('--backup-encryption-key', help='Backup encryption Key')
@click.option('--encryption-key', help='Backup encryption Key')

Backup seems a bit redundant here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already have "encryption key" for passwords. Won't it cause confusion?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Anurag810

The reason we kept a separate key was that we didn't want customers to have access to the site encryption key.
But we let them download the site config which has the key anyway.

So replace this key with the site encryption key. Reuse that, revert changes.

@@ -56,7 +57,7 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin
@click.option('--with-private-files', help='Restores the private files of the site, given path to its tar file')
@click.option('--force', is_flag=True, default=False, help='Ignore the validations and downgrade warnings. This action is not recommended')
@pass_context
def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None):
def restore(context, sql_file_path, backup_encryption_key=None, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best to add new kwargs at the end. Don't know if someone is using these commands programmatically.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 agree @Anurag0911


if not os.path.exists(sql_file_path):
print("Invalid path", sql_file_path)
sys.exit(1)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2021-09-17 at 6 22 55 PM

Files riddles with trailing whitespaces.

sys.exit(1)

# Check if file is encrypted
if "enc" in sql_file_path:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like a very weak check to figure out if a backup is encrypted

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can try reading the file and decrypt it in except block but adding a header in an encrypted file is not an option it causes issues with the decrypted files. Any other suggestions for identifying these files?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gavindsouza

We tried having a header in the tar, but it didn't work out as this encrypts the whole thing.

We should have a try-catch if this fails along with this.

@@ -66,10 +67,61 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
is_partial,
validate_database_sql
)
from frappe.utils.backups import backup_decryption, decryption_rollback
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Encryption and Decryption methods should be a part of the BackupGenerator. Either that or a separate class that represents a Backup alone.

Comment on lines 701 to 718
def backup_encryption_key():
"""
Checks if backup encryption key exists
else create one
Return:
Backup encryption key
"""
from frappe.installer import update_site_config
if 'backup_encryption_key' not in frappe.local.conf:
confirm = click.confirm(
"No encrytion key found. Generate new Key?"
)
if not confirm:
sys.exit(1)
backup_encryption_key = Fernet.generate_key().decode()
update_site_config('backup_encryption_key', backup_encryption_key)
frappe.local.conf.backup_encryption_key = backup_encryption_key
return frappe.local.conf.backup_encryption_key
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function does too many things; break it down to either get user input, set encryption key in config or change it. If ever invoked from anywhere except the CLI, it would cause it to stall and/or crash.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need more information regarding this. User input is required once and there should be a warning... it's harmless to remove, is it good idea to create even it's required once?

decryptedfile = file_path[:-4],
)

frappe.utils.execute_in_shell(command)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if file_path does not exist, NameError?

Comment on lines 675 to 680
def get_backup_encryption_key():
if 'backup_encryption_key' in frappe.local.conf:
message = frappe.local.conf.backup_encryption_key
else:
message = "No key found."
return frappe.msgprint(message,'Backup Encryption Key')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API endpoints should respond with some specific data, just returning frappe.conf.backup_encryption_key would do here. The user message should be handled at the frontend.

Also, this API should have a restriction such as only for System Managers, or the Administrator user.

Comment on lines +255 to +267
file_type_slugs = {
"database": "*-{{}}-{}database.sql.gz".format('*' if partial else ''),
"public": "*-{}-files.tar",
"private": "*-{}-private-files.tar",
"config": "*-{}-site_config_backup.json",
}
else:
file_type_slugs = {
"database": "*-{{}}-{}database.enc.sql.gz".format('*' if partial else ''),
"public": "*-{}-files.enc.tar",
"private": "*-{}-private-files.enc.tar",
"config": "*-{}-site_config_backup.json",
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be DRY-er


#Get the key from site config
site = get_site(context)
frappe.init(site)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not do this whole block right before calling partial_restore? There doesn't seem to be any reason to initialize werkzeug locals and release them twice here.

@stale
Copy link

stale bot commented Sep 27, 2021

This pull request has been automatically marked as stale because it has not had recent activity. It will be closed within 3 days if no further activity occurs, but it only takes a comment to keep a contribution alive :) Also, even if it is closed, you can always reopen the PR when you're ready. Thank you for contributing.

@stale stale bot added the inactive label Sep 27, 2021
@stale stale bot closed this Sep 30, 2021
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Nov 2, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants