feat: add encrypt backup using public key, migrate gpg to go gpg dependency
This commit is contained in:
@@ -52,7 +52,7 @@ ENV VERSION=${appVersion}
|
|||||||
LABEL author="Jonas Kaninda"
|
LABEL author="Jonas Kaninda"
|
||||||
LABEL version=${appVersion}
|
LABEL version=${appVersion}
|
||||||
|
|
||||||
RUN apk --update add --no-cache postgresql-client gnupg tzdata
|
RUN apk --update add --no-cache postgresql-client tzdata
|
||||||
RUN mkdir $WORKDIR
|
RUN mkdir $WORKDIR
|
||||||
RUN mkdir $BACKUPDIR
|
RUN mkdir $BACKUPDIR
|
||||||
RUN mkdir -p $BACKUP_TMP_DIR
|
RUN mkdir -p $BACKUP_TMP_DIR
|
||||||
|
|||||||
@@ -10,15 +10,17 @@ The image supports encrypting backups using one of two available methods: GPG wi
|
|||||||
|
|
||||||
## Using GPG passphrase
|
## Using GPG passphrase
|
||||||
|
|
||||||
The image supports encrypting backups using GPG out of the box. In case a `GPG_PASSPHRASE` environment variable is set, the backup archive will be encrypted using the given key and saved as a sql.gpg file instead or sql.gz.gpg.
|
The image supports encrypting backups using GPG out of the box. In case a `GPG_PASSPHRASE` or `GPG_PUBLIC_KEY` environment variable is set, the backup archive will be encrypted using the given key and saved as a sql.gpg file instead or sql.gz.gpg.
|
||||||
|
|
||||||
{: .warning }
|
{: .warning }
|
||||||
To restore an encrypted backup, you need to provide the same GPG passphrase used during backup process.
|
To restore an encrypted backup, you need to provide the same GPG passphrase used during backup process.
|
||||||
Or
|
|
||||||
|
|
||||||
- GPG home directory `/config/gnupg`
|
- GPG home directory `/config/gnupg`
|
||||||
- Cipher algorithm `aes256`
|
- Cipher algorithm `aes256`
|
||||||
|
|
||||||
|
{: .note }
|
||||||
|
The backup encrypted using `GPG passphrase` method can be restored automatically, no need to decrypt it before restoration.
|
||||||
|
|
||||||
|
|
||||||
To decrypt manually, you need to install `gnupg`
|
To decrypt manually, you need to install `gnupg`
|
||||||
|
|
||||||
@@ -27,7 +29,10 @@ gpg --batch --passphrase "my-passphrase" \
|
|||||||
--output database_20240730_044201.sql.gz \
|
--output database_20240730_044201.sql.gz \
|
||||||
--decrypt database_20240730_044201.sql.gz.gpg
|
--decrypt database_20240730_044201.sql.gz.gpg
|
||||||
```
|
```
|
||||||
|
Using your private key
|
||||||
|
```shell
|
||||||
|
gpg --output database_20240730_044201.sql.gz --decrypt database_20240730_044201.sql.gz.gpg
|
||||||
|
```
|
||||||
### Backup
|
### Backup
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
@@ -56,4 +61,3 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
web:
|
web:
|
||||||
```
|
```
|
||||||
## Using GPG public key
|
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -10,7 +10,6 @@ require (
|
|||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.8.0
|
||||||
golang.org/x/crypto v0.28.0
|
golang.org/x/crypto v0.28.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
@@ -296,12 +296,12 @@ func encryptBackup(config *BackupConfig) {
|
|||||||
if config.usingKey {
|
if config.usingKey {
|
||||||
err := encryptWithGPGPublicKey(filepath.Join(tmpPath, config.backupFileName), config.publicKey)
|
err := encryptWithGPGPublicKey(filepath.Join(tmpPath, config.backupFileName), config.publicKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Fatal("Error during encrypting backup %v", err)
|
utils.Fatal("error during encrypting backup %v", err)
|
||||||
}
|
}
|
||||||
} else if config.passphrase != "" {
|
} else if config.passphrase != "" {
|
||||||
err := encryptWithGPGSymmetric(filepath.Join(tmpPath, config.backupFileName), config.passphrase)
|
err := encryptWithGPG(filepath.Join(tmpPath, config.backupFileName), config.passphrase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Fatal("Error during encrypting backup %v", err)
|
utils.Fatal("error during encrypting backup %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,50 +12,59 @@ import (
|
|||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/jkaninda/pg-bkup/utils"
|
"github.com/jkaninda/pg-bkup/utils"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// decryptWithGPGSymmetric decrypts backup file using a passphrase
|
// decryptWithGPG decrypts backup file using a passphrase
|
||||||
func decryptWithGPGSymmetric(inputFile string, passphrase string) error {
|
func decryptWithGPG(inputFile string, passphrase string) error {
|
||||||
utils.Info("Decrypting backup using passphrase...")
|
utils.Info("Decrypting backup using passphrase...")
|
||||||
|
// Read the encrypted file
|
||||||
//Create gpg home dir
|
encFileContent, err := os.ReadFile(inputFile)
|
||||||
err := utils.MakeDirAll(gpgHome)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.New(fmt.Sprintf("Error reading encrypted file: %s", err))
|
||||||
}
|
}
|
||||||
utils.SetEnv("GNUPGHOME", gpgHome)
|
// Define the passphrase used to encrypt the file
|
||||||
cmd := exec.Command("gpg", "--batch", "--passphrase", passphrase, "--output", RemoveLastExtension(inputFile), "--decrypt", inputFile)
|
_passphrase := []byte(passphrase)
|
||||||
cmd.Stdout = os.Stdout
|
// Create a PGP message object from the encrypted file content
|
||||||
cmd.Stderr = os.Stderr
|
encryptedMessage := crypto.NewPGPMessage(encFileContent)
|
||||||
|
// Decrypt the message using the passphrase
|
||||||
err = cmd.Run()
|
plainMessage, err := crypto.DecryptMessageWithPassword(encryptedMessage, _passphrase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.New(fmt.Sprintf("Error decrypting file: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the decrypted file (restore it)
|
||||||
|
err = os.WriteFile(RemoveLastExtension(inputFile), plainMessage.GetBinary(), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(fmt.Sprintf("Error saving decrypted file: %s", err))
|
||||||
}
|
}
|
||||||
utils.Info("Decrypting backup using passphrase...done")
|
utils.Info("Decrypting backup using passphrase...done")
|
||||||
utils.Info("Backup file decrypted successful!")
|
utils.Info("Backup file decrypted successful!")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// encryptWithGPGSymmetric encrypts backup using a passphrase
|
// encryptWithGPG encrypts backup using a passphrase
|
||||||
func encryptWithGPGSymmetric(inputFile string, passphrase string) error {
|
func encryptWithGPG(inputFile string, passphrase string) error {
|
||||||
utils.Info("Encrypting backup using passphrase...")
|
utils.Info("Encrypting backup using passphrase...")
|
||||||
|
// Read the file to be encrypted
|
||||||
//Create gpg home dir
|
plainFileContent, err := os.ReadFile(inputFile)
|
||||||
err := utils.MakeDirAll(gpgHome)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.New(fmt.Sprintf("Error reading file: %s", err))
|
||||||
}
|
}
|
||||||
utils.SetEnv("GNUPGHOME", gpgHome)
|
// Define the passphrase to encrypt the file
|
||||||
cmd := exec.Command("gpg", "--batch", "--passphrase", passphrase, "--symmetric", "--cipher-algo", algorithm, inputFile)
|
_passphrase := []byte(passphrase)
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
|
|
||||||
err = cmd.Run()
|
// Create a message object from the file content
|
||||||
|
message := crypto.NewPlainMessage(plainFileContent)
|
||||||
|
// Encrypt the message using the passphrase
|
||||||
|
encryptedMessage, err := crypto.EncryptMessageWithPassword(message, _passphrase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.New(fmt.Sprintf("Error encrypting backup file: %s", err))
|
||||||
|
}
|
||||||
|
// Save the encrypted .tar file
|
||||||
|
err = os.WriteFile(fmt.Sprintf("%s.%s", inputFile, gpgExtension), encryptedMessage.GetBinary(), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(fmt.Sprintf("Error saving encrypted filee: %s", err))
|
||||||
}
|
}
|
||||||
utils.Info("Encrypting backup using passphrase...done")
|
utils.Info("Encrypting backup using passphrase...done")
|
||||||
utils.Info("Backup file encrypted successful!")
|
utils.Info("Backup file encrypted successful!")
|
||||||
@@ -88,7 +97,7 @@ func encryptWithGPGPublicKey(inputFile string, publicKey string) error {
|
|||||||
return errors.New(fmt.Sprintf("Error reading file: %v", err))
|
return errors.New(fmt.Sprintf("Error reading file: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// encryptWithGPGSymmetric the file
|
// encryptWithGPG the file
|
||||||
message := crypto.NewPlainMessage(fileContent)
|
message := crypto.NewPlainMessage(fileContent)
|
||||||
encMessage, err := keyRing.Encrypt(message, nil)
|
encMessage, err := keyRing.Encrypt(message, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -149,7 +158,7 @@ func decryptWithGPGPrivateKey(inputFile, privateKey, passphrase string) error {
|
|||||||
return errors.New(fmt.Sprintf("Error reading encrypted file: %s", err))
|
return errors.New(fmt.Sprintf("Error reading encrypted file: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// decryptWithGPGSymmetric the file
|
// decryptWithGPG the file
|
||||||
encryptedMessage := crypto.NewPGPMessage(encFileContent)
|
encryptedMessage := crypto.NewPGPMessage(encFileContent)
|
||||||
message, err := keyRing.Decrypt(encryptedMessage, nil, 0)
|
message, err := keyRing.Decrypt(encryptedMessage, nil, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/jkaninda/pg-bkup/utils"
|
"github.com/jkaninda/pg-bkup/utils"
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -140,24 +139,6 @@ func testDatabaseConnection(db *dbConfig) {
|
|||||||
utils.Info("Successfully connected to %s database", db.dbName)
|
utils.Info("Successfully connected to %s database", db.dbName)
|
||||||
|
|
||||||
}
|
}
|
||||||
func readConf(filename string) (*Config, error) {
|
|
||||||
configFile := filepath.Join("", filename)
|
|
||||||
if utils.FileExists(configFile) {
|
|
||||||
buf, err := os.ReadFile(configFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c := &Config{}
|
|
||||||
err = yaml.Unmarshal(buf, c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("in file %q: %w", filename, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("config file %q not found", filename)
|
|
||||||
}
|
|
||||||
func checkPubKeyFile(pubKey string) (string, error) {
|
func checkPubKeyFile(pubKey string) (string, error) {
|
||||||
utils.Info("Checking file %s ...", pubKey)
|
utils.Info("Checking file %s ...", pubKey)
|
||||||
// Define possible key file names
|
// Define possible key file names
|
||||||
|
|||||||
@@ -81,13 +81,13 @@ func RestoreDatabase(db *dbConfig, conf *RestoreConfig) {
|
|||||||
utils.Error("Error, passphrase or private key required")
|
utils.Error("Error, passphrase or private key required")
|
||||||
utils.Fatal("Your file seems to be a GPG file.\nYou need to provide GPG keys. GPG_PASSPHRASE or GPG_PRIVATE_KEY environment variable is required.")
|
utils.Fatal("Your file seems to be a GPG file.\nYou need to provide GPG keys. GPG_PASSPHRASE or GPG_PRIVATE_KEY environment variable is required.")
|
||||||
} else {
|
} else {
|
||||||
//decryptWithGPGSymmetric file
|
//decryptWithGPG file
|
||||||
err := decryptWithGPGSymmetric(filepath.Join(tmpPath, conf.file), conf.passphrase)
|
err := decryptWithGPG(filepath.Join(tmpPath, conf.file), conf.passphrase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
utils.Fatal("Error decrypting file %s %v", file, err)
|
utils.Fatal("Error decrypting file %s %v", file, err)
|
||||||
}
|
}
|
||||||
//Update file name
|
//Update file name
|
||||||
file = RemoveLastExtension(file)
|
conf.file = RemoveLastExtension(file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
**/
|
**/
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
const RestoreExample = "pg-bkup restore --dbname database --file db_20231219_022941.sql.gz\n" +
|
const RestoreExample = "restore --dbname database --file db_20231219_022941.sql.gz\n" +
|
||||||
"restore --dbname database --storage s3 --path /custom-path --file db_20231219_022941.sql.gz"
|
"restore --dbname database --storage s3 --path /custom-path --file db_20231219_022941.sql.gz"
|
||||||
const BackupExample = "pg-bkup backup --dbname database --disable-compression\n" +
|
const BackupExample = "backup --dbname database --disable-compression\n" +
|
||||||
"backup --dbname database --storage s3 --path /custom-path --disable-compression"
|
"backup --dbname database --storage s3 --path /custom-path --disable-compression"
|
||||||
|
|
||||||
const MainExample = "pg-bkup backup --dbname database --disable-compression\n" +
|
const MainExample = "backup --dbname database --disable-compression\n" +
|
||||||
"backup --dbname database --storage s3 --path /custom-path\n" +
|
"backup --dbname database --storage s3 --path /custom-path\n" +
|
||||||
"restore --dbname database --file db_20231219_022941.sql.gz"
|
"restore --dbname database --file db_20231219_022941.sql.gz"
|
||||||
|
|||||||
Reference in New Issue
Block a user