diff options
author | Wim <wim@42.be> | 2020-10-11 02:38:09 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-10-10 20:38:09 -0400 |
commit | 9066d09c57d3eaddd27c4986900e6afc0885e0c7 (patch) | |
tree | c50c56ab6adf9baa8793f8ddff2e829e08471121 /models | |
parent | 7eb846013299465e4cd73f76f13a00984183146e (diff) | |
download | gitea-9066d09c57d3eaddd27c4986900e6afc0885e0c7.tar.gz gitea-9066d09c57d3eaddd27c4986900e6afc0885e0c7.zip |
Add ssh certificate support (#12281)
* Add ssh certificate support
* Add ssh certificate support to builtin ssh
* Write trusted-user-ca-keys.pem based on configuration
* Update app.example.ini
* Update templates/user/settings/keys_principal.tmpl
Co-authored-by: silverwind <me@silverwind.io>
* Remove unused locale string
* Update options/locale/locale_en-US.ini
Co-authored-by: silverwind <me@silverwind.io>
* Update options/locale/locale_en-US.ini
Co-authored-by: silverwind <me@silverwind.io>
* Update models/ssh_key.go
Co-authored-by: silverwind <me@silverwind.io>
* Add missing creation of SSH.Rootpath
* Update cheatsheet, example and locale strings
* Update models/ssh_key.go
Co-authored-by: zeripath <art27@cantab.net>
* Update models/ssh_key.go
Co-authored-by: zeripath <art27@cantab.net>
* Update models/ssh_key.go
Co-authored-by: zeripath <art27@cantab.net>
* Update models/ssh_key.go
Co-authored-by: zeripath <art27@cantab.net>
* Update models/ssh_key.go
* Optimizations based on feedback
* Validate CA keys for external sshd
* Add filename option and change default filename
Add a SSH_TRUSTED_USER_CA_KEYS_FILENAME option which default is
RUN_USER/.ssh/gitea-trusted-user-ca-keys.pem
Do not write a file when SSH_TRUSTED_USER_CA_KEYS is empty.
Add some more documentation.
* Remove unneeded principalkey functions
* Add blank line
* Apply suggestions from code review
Co-authored-by: zeripath <art27@cantab.net>
* Add SSH_AUTHORIZED_PRINCIPALS_ALLOW option
This adds a SSH_AUTHORIZED_PRINCIPALS_ALLOW which is default
email,username this means that users only can add the principals
that match their email or username.
To allow anything the admin need to set the option anything.
This allows for a safe default in gitea which protects against malicious
users using other user's prinicipals. (before that user could set it).
This commit also has some small other fixes from the last code review.
* Rewrite principal keys file on user deletion
* Use correct rewrite method
* Set correct AuthorizedPrincipalsBackup default setting
* Rewrite principalsfile when adding principals
* Add update authorized_principals option to admin dashboard
* Handle non-primary emails
Signed-off-by: Andrew Thornton <art27@cantab.net>
* Add the command actually to the dashboard template
* Update models/ssh_key.go
Co-authored-by: silverwind <me@silverwind.io>
* By default do not show principal options unless there are CA keys set or they are explicitly set
Signed-off-by: Andrew Thornton <art27@cantab.net>
* allow settings when enabled
* Fix typos in TrustedUserCAKeys path
* Allow every CASignatureAlgorithms algorithm
As this depends on the content of TrustedUserCAKeys we should allow all
signature algorithms as admins can choose the specific algorithm on their
signing CA
* Update models/ssh_key.go
Co-authored-by: Lauris BH <lauris@nix.lv>
* Fix linting issue
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: zeripath <art27@cantab.net>
Co-authored-by: Lauris BH <lauris@nix.lv>
Co-authored-by: techknowlogick <matti@mdranta.net>
Co-authored-by: techknowlogick <techknowlogick@gitea.io>
Diffstat (limited to 'models')
-rw-r--r-- | models/ssh_key.go | 238 | ||||
-rw-r--r-- | models/user.go | 4 |
2 files changed, 238 insertions, 4 deletions
diff --git a/models/ssh_key.go b/models/ssh_key.go index b46ff76b94..d67981398b 100644 --- a/models/ssh_key.go +++ b/models/ssh_key.go @@ -40,6 +40,8 @@ const ( tplCommentPrefix = `# gitea public key` tplCommand = "%s --config=%s serv key-%d" tplPublicKey = tplCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s` + "\n" + + authorizedPrincipalsFile = "authorized_principals" ) var sshOpLocker sync.Mutex @@ -52,6 +54,8 @@ const ( KeyTypeUser = iota + 1 // KeyTypeDeploy specifies the deploy key KeyTypeDeploy + // KeyTypePrincipal specifies the authorized principal key + KeyTypePrincipal ) // PublicKey represents a user or deploy SSH public key. @@ -401,6 +405,9 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error { } for _, key := range keys { + if key.Type == KeyTypePrincipal { + continue + } if _, err = f.WriteString(key.AuthorizedString()); err != nil { return err } @@ -571,6 +578,25 @@ func SearchPublicKeyByContent(content string) (*PublicKey, error) { return searchPublicKeyByContentWithEngine(x, content) } +func searchPublicKeyByContentExactWithEngine(e Engine, content string) (*PublicKey, error) { + key := new(PublicKey) + has, err := e. + Where("content = ?", content). + Get(key) + if err != nil { + return nil, err + } else if !has { + return nil, ErrKeyNotExist{} + } + return key, nil +} + +// SearchPublicKeyByContentExact searches content +// and returns public key found. +func SearchPublicKeyByContentExact(content string) (*PublicKey, error) { + return searchPublicKeyByContentExactWithEngine(x, content) +} + // SearchPublicKey returns a list of public keys matching the provided arguments. func SearchPublicKey(uid int64, fingerprint string) ([]*PublicKey, error) { keys := make([]*PublicKey, 0, 5) @@ -586,7 +612,7 @@ func SearchPublicKey(uid int64, fingerprint string) ([]*PublicKey, error) { // ListPublicKeys returns a list of public keys belongs to given user. func ListPublicKeys(uid int64, listOptions ListOptions) ([]*PublicKey, error) { - sess := x.Where("owner_id = ?", uid) + sess := x.Where("owner_id = ? AND type != ?", uid, KeyTypePrincipal) if listOptions.Page != 0 { sess = listOptions.setSessionPagination(sess) @@ -662,6 +688,10 @@ func DeletePublicKey(doer *User, id int64) (err error) { } sess.Close() + if key.Type == KeyTypePrincipal { + return RewriteAllPrincipalKeys() + } + return RewriteAllPublicKeys() } @@ -727,11 +757,10 @@ func RegeneratePublicKeys(t io.StringWriter) error { } func regeneratePublicKeys(e Engine, t io.StringWriter) error { - err := e.Iterate(new(PublicKey), func(idx int, bean interface{}) (err error) { + if err := e.Where("type != ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean interface{}) (err error) { _, err = t.WriteString((bean.(*PublicKey)).AuthorizedString()) return err - }) - if err != nil { + }); err != nil { return err } @@ -1041,3 +1070,204 @@ func SearchDeployKeys(repoID int64, keyID int64, fingerprint string) ([]*DeployK } return keys, x.Where(cond).Find(&keys) } + +// __________ .__ .__ .__ +// \______ _______|__| ____ ____ |_____________ | | ______ +// | ___\_ __ | |/ \_/ ___\| \____ \__ \ | | / ___/ +// | | | | \| | | \ \___| | |_> / __ \| |__\___ \ +// |____| |__| |__|___| /\___ |__| __(____ |____/____ > +// \/ \/ |__| \/ \/ + +// AddPrincipalKey adds new principal to database and authorized_principals file. +func AddPrincipalKey(ownerID int64, content string, loginSourceID int64) (*PublicKey, error) { + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return nil, err + } + + // Principals cannot be duplicated. + has, err := sess. + Where("content = ? AND type = ?", content, KeyTypePrincipal). + Get(new(PublicKey)) + if err != nil { + return nil, err + } else if has { + return nil, ErrKeyAlreadyExist{0, "", content} + } + + key := &PublicKey{ + OwnerID: ownerID, + Name: content, + Content: content, + Mode: AccessModeWrite, + Type: KeyTypePrincipal, + LoginSourceID: loginSourceID, + } + if err = addPrincipalKey(sess, key); err != nil { + return nil, fmt.Errorf("addKey: %v", err) + } + + if err = sess.Commit(); err != nil { + return nil, err + } + + sess.Close() + + return key, RewriteAllPrincipalKeys() +} + +func addPrincipalKey(e Engine, key *PublicKey) (err error) { + // Save Key representing a principal. + if _, err = e.Insert(key); err != nil { + return err + } + + return nil +} + +// CheckPrincipalKeyString strips spaces and returns an error if the given principal contains newlines +func CheckPrincipalKeyString(user *User, content string) (_ string, err error) { + if setting.SSH.Disabled { + return "", ErrSSHDisabled{} + } + + content = strings.TrimSpace(content) + if strings.ContainsAny(content, "\r\n") { + return "", errors.New("only a single line with a single principal please") + } + + // check all the allowed principals, email, username or anything + // if any matches, return ok + for _, v := range setting.SSH.AuthorizedPrincipalsAllow { + switch v { + case "anything": + return content, nil + case "email": + emails, err := GetEmailAddresses(user.ID) + if err != nil { + return "", err + } + for _, email := range emails { + if !email.IsActivated { + continue + } + if content == email.Email { + return content, nil + } + } + + case "username": + if content == user.Name { + return content, nil + } + } + } + + return "", fmt.Errorf("didn't match allowed principals: %s", setting.SSH.AuthorizedPrincipalsAllow) +} + +// RewriteAllPrincipalKeys removes any authorized principal and rewrite all keys from database again. +// Note: x.Iterate does not get latest data after insert/delete, so we have to call this function +// outside any session scope independently. +func RewriteAllPrincipalKeys() error { + return rewriteAllPrincipalKeys(x) +} + +func rewriteAllPrincipalKeys(e Engine) error { + // Don't rewrite key if internal server + if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedPrincipalsFile { + return nil + } + + sshOpLocker.Lock() + defer sshOpLocker.Unlock() + + if setting.SSH.RootPath != "" { + // First of ensure that the RootPath is present, and if not make it with 0700 permissions + // This of course doesn't guarantee that this is the right directory for authorized_keys + // but at least if it's supposed to be this directory and it doesn't exist and we're the + // right user it will at least be created properly. + err := os.MkdirAll(setting.SSH.RootPath, 0700) + if err != nil { + log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err) + return err + } + } + + fPath := filepath.Join(setting.SSH.RootPath, authorizedPrincipalsFile) + tmpPath := fPath + ".tmp" + t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer func() { + t.Close() + os.Remove(tmpPath) + }() + + if setting.SSH.AuthorizedPrincipalsBackup && com.IsExist(fPath) { + bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix()) + if err = com.Copy(fPath, bakPath); err != nil { + return err + } + } + + if err := regeneratePrincipalKeys(e, t); err != nil { + return err + } + + t.Close() + return os.Rename(tmpPath, fPath) +} + +// ListPrincipalKeys returns a list of principals belongs to given user. +func ListPrincipalKeys(uid int64, listOptions ListOptions) ([]*PublicKey, error) { + sess := x.Where("owner_id = ? AND type = ?", uid, KeyTypePrincipal) + if listOptions.Page != 0 { + sess = listOptions.setSessionPagination(sess) + + keys := make([]*PublicKey, 0, listOptions.PageSize) + return keys, sess.Find(&keys) + } + + keys := make([]*PublicKey, 0, 5) + return keys, sess.Find(&keys) +} + +// RegeneratePrincipalKeys regenerates the authorized_principals file +func RegeneratePrincipalKeys(t io.StringWriter) error { + return regeneratePrincipalKeys(x, t) +} + +func regeneratePrincipalKeys(e Engine, t io.StringWriter) error { + if err := e.Where("type = ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean interface{}) (err error) { + _, err = t.WriteString((bean.(*PublicKey)).AuthorizedString()) + return err + }); err != nil { + return err + } + + fPath := filepath.Join(setting.SSH.RootPath, authorizedPrincipalsFile) + if com.IsExist(fPath) { + f, err := os.Open(fPath) + if err != nil { + return err + } + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, tplCommentPrefix) { + scanner.Scan() + continue + } + _, err = t.WriteString(line + "\n") + if err != nil { + f.Close() + return err + } + } + f.Close() + } + return nil +} diff --git a/models/user.go b/models/user.go index 63ce6ffdfc..6c57dd473a 100644 --- a/models/user.go +++ b/models/user.go @@ -1254,6 +1254,10 @@ func deleteUser(e *xorm.Session, u *User) error { if err != nil { return err } + err = rewriteAllPrincipalKeys(e) + if err != nil { + return err + } // ***** END: PublicKey ***** // ***** START: GPGPublicKey ***** |