Encrypt migration credentials at rest (#15895)
* encrypt migration credentials in task persistence Not sure this is the best approach, we could encrypt the entire `PayloadContent` instead. Also instead of clearing individual fields in payload content, we could just delete the task once it has (successfully) finished..? * remove credentials of past migrations * only run DB migration for completed tasks * fix binding * add omitempty * never serialize unencrypted credentials * fix import order Co-authored-by: techknowlogick <techknowlogick@gitea.io> Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
parent
256b1a3561
commit
cb940c4312
5 changed files with 145 additions and 5 deletions
|
@ -309,6 +309,8 @@ var migrations = []Migration{
|
||||||
NewMigration("Add LFS columns to Mirror", addLFSMirrorColumns),
|
NewMigration("Add LFS columns to Mirror", addLFSMirrorColumns),
|
||||||
// v179 -> v180
|
// v179 -> v180
|
||||||
NewMigration("Convert avatar url to text", convertAvatarURLToText),
|
NewMigration("Convert avatar url to text", convertAvatarURLToText),
|
||||||
|
// v180 -> v181
|
||||||
|
NewMigration("Delete credentials from past migrations", deleteMigrationCredentials),
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDBVersion returns the current db version
|
// GetCurrentDBVersion returns the current db version
|
||||||
|
|
74
models/migrations/v180.go
Normal file
74
models/migrations/v180.go
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.gitea.io/gitea/models"
|
||||||
|
"code.gitea.io/gitea/modules/migrations/base"
|
||||||
|
"code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
"xorm.io/builder"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func deleteMigrationCredentials(x *xorm.Engine) (err error) {
|
||||||
|
const batchSize = 100
|
||||||
|
|
||||||
|
// only match migration tasks, that are not pending or running
|
||||||
|
cond := builder.Eq{
|
||||||
|
"type": structs.TaskTypeMigrateRepo,
|
||||||
|
}.And(builder.Gte{
|
||||||
|
"status": structs.TaskStatusStopped,
|
||||||
|
})
|
||||||
|
|
||||||
|
sess := x.NewSession()
|
||||||
|
defer sess.Close()
|
||||||
|
|
||||||
|
for start := 0; ; start += batchSize {
|
||||||
|
tasks := make([]*models.Task, 0, batchSize)
|
||||||
|
if err = sess.Limit(batchSize, start).Where(cond, 0).Find(&tasks); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(tasks) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err = sess.Begin(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, t := range tasks {
|
||||||
|
if t.PayloadContent, err = removeCredentials(t.PayloadContent); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err = sess.ID(t.ID).Cols("payload_content").Update(t); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = sess.Commit(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeCredentials(payload string) (string, error) {
|
||||||
|
var opts base.MigrateOptions
|
||||||
|
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
|
err := json.Unmarshal([]byte(payload), &opts)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.AuthPassword = ""
|
||||||
|
opts.AuthToken = ""
|
||||||
|
opts.CloneAddr = util.SanitizeURLCredentials(opts.CloneAddr, true)
|
||||||
|
|
||||||
|
confBytes, err := json.Marshal(opts)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(confBytes), nil
|
||||||
|
}
|
|
@ -8,8 +8,11 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
migration "code.gitea.io/gitea/modules/migrations/base"
|
migration "code.gitea.io/gitea/modules/migrations/base"
|
||||||
|
"code.gitea.io/gitea/modules/secret"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
|
@ -110,6 +113,24 @@ func (task *Task) MigrateConfig() (*migration.MigrateOptions, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// decrypt credentials
|
||||||
|
if opts.CloneAddrEncrypted != "" {
|
||||||
|
if opts.CloneAddr, err = secret.DecryptSecret(setting.SecretKey, opts.CloneAddrEncrypted); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.AuthPasswordEncrypted != "" {
|
||||||
|
if opts.AuthPassword, err = secret.DecryptSecret(setting.SecretKey, opts.AuthPasswordEncrypted); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.AuthTokenEncrypted != "" {
|
||||||
|
if opts.AuthToken, err = secret.DecryptSecret(setting.SecretKey, opts.AuthTokenEncrypted); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &opts, nil
|
return &opts, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("Task type is %s, not Migrate Repo", task.Type.Name())
|
return nil, fmt.Errorf("Task type is %s, not Migrate Repo", task.Type.Name())
|
||||||
|
@ -205,12 +226,31 @@ func createTask(e Engine, task *Task) error {
|
||||||
func FinishMigrateTask(task *Task) error {
|
func FinishMigrateTask(task *Task) error {
|
||||||
task.Status = structs.TaskStatusFinished
|
task.Status = structs.TaskStatusFinished
|
||||||
task.EndTime = timeutil.TimeStampNow()
|
task.EndTime = timeutil.TimeStampNow()
|
||||||
|
|
||||||
|
// delete credentials when we're done, they're a liability.
|
||||||
|
conf, err := task.MigrateConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conf.AuthPassword = ""
|
||||||
|
conf.AuthToken = ""
|
||||||
|
conf.CloneAddr = util.SanitizeURLCredentials(conf.CloneAddr, true)
|
||||||
|
conf.AuthPasswordEncrypted = ""
|
||||||
|
conf.AuthTokenEncrypted = ""
|
||||||
|
conf.CloneAddrEncrypted = ""
|
||||||
|
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
|
confBytes, err := json.Marshal(conf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
task.PayloadContent = string(confBytes)
|
||||||
|
|
||||||
sess := x.NewSession()
|
sess := x.NewSession()
|
||||||
defer sess.Close()
|
defer sess.Close()
|
||||||
if err := sess.Begin(); err != nil {
|
if err := sess.Begin(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := sess.ID(task.ID).Cols("status", "end_time").Update(task); err != nil {
|
if _, err := sess.ID(task.ID).Cols("status", "end_time", "payload_content").Update(task); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,10 +11,13 @@ import "code.gitea.io/gitea/modules/structs"
|
||||||
// this is for internal usage by migrations module and func who interact with it
|
// this is for internal usage by migrations module and func who interact with it
|
||||||
type MigrateOptions struct {
|
type MigrateOptions struct {
|
||||||
// required: true
|
// required: true
|
||||||
CloneAddr string `json:"clone_addr" binding:"Required"`
|
CloneAddr string `json:"clone_addr" binding:"Required"`
|
||||||
AuthUsername string `json:"auth_username"`
|
CloneAddrEncrypted string `json:"clone_addr_encrypted,omitempty"`
|
||||||
AuthPassword string `json:"auth_password"`
|
AuthUsername string `json:"auth_username"`
|
||||||
AuthToken string `json:"auth_token"`
|
AuthPassword string `json:"-"`
|
||||||
|
AuthPasswordEncrypted string `json:"auth_password_encrypted,omitempty"`
|
||||||
|
AuthToken string `json:"-"`
|
||||||
|
AuthTokenEncrypted string `json:"auth_token_encrypted,omitempty"`
|
||||||
// required: true
|
// required: true
|
||||||
UID int `json:"uid" binding:"Required"`
|
UID int `json:"uid" binding:"Required"`
|
||||||
// required: true
|
// required: true
|
||||||
|
|
|
@ -13,8 +13,11 @@ import (
|
||||||
"code.gitea.io/gitea/modules/migrations/base"
|
"code.gitea.io/gitea/modules/migrations/base"
|
||||||
"code.gitea.io/gitea/modules/queue"
|
"code.gitea.io/gitea/modules/queue"
|
||||||
repo_module "code.gitea.io/gitea/modules/repository"
|
repo_module "code.gitea.io/gitea/modules/repository"
|
||||||
|
"code.gitea.io/gitea/modules/secret"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -65,6 +68,24 @@ func MigrateRepository(doer, u *models.User, opts base.MigrateOptions) error {
|
||||||
|
|
||||||
// CreateMigrateTask creates a migrate task
|
// CreateMigrateTask creates a migrate task
|
||||||
func CreateMigrateTask(doer, u *models.User, opts base.MigrateOptions) (*models.Task, error) {
|
func CreateMigrateTask(doer, u *models.User, opts base.MigrateOptions) (*models.Task, error) {
|
||||||
|
// encrypt credentials for persistence
|
||||||
|
var err error
|
||||||
|
opts.CloneAddrEncrypted, err = secret.EncryptSecret(setting.SecretKey, opts.CloneAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
opts.CloneAddr = util.SanitizeURLCredentials(opts.CloneAddr, true)
|
||||||
|
opts.AuthPasswordEncrypted, err = secret.EncryptSecret(setting.SecretKey, opts.AuthPassword)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
opts.AuthPassword = ""
|
||||||
|
opts.AuthTokenEncrypted, err = secret.EncryptSecret(setting.SecretKey, opts.AuthToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
opts.AuthToken = ""
|
||||||
|
|
||||||
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
bs, err := json.Marshal(&opts)
|
bs, err := json.Marshal(&opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Reference in a new issue