code.gitea.io/gitea@v1.21.7/models/asymkey/ssh_key_authorized_keys.go (about) 1 // Copyright 2021 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package asymkey 5 6 import ( 7 "bufio" 8 "context" 9 "fmt" 10 "io" 11 "os" 12 "path/filepath" 13 "strings" 14 "sync" 15 "time" 16 17 "code.gitea.io/gitea/models/db" 18 "code.gitea.io/gitea/modules/log" 19 "code.gitea.io/gitea/modules/setting" 20 "code.gitea.io/gitea/modules/util" 21 ) 22 23 // _____ __ .__ .__ .___ 24 // / _ \ __ ___/ |_| |__ ___________|__|_______ ____ __| _/ 25 // / /_\ \| | \ __\ | \ / _ \_ __ \ \___ // __ \ / __ | 26 // / | \ | /| | | Y ( <_> ) | \/ |/ /\ ___// /_/ | 27 // \____|__ /____/ |__| |___| /\____/|__| |__/_____ \\___ >____ | 28 // \/ \/ \/ \/ \/ 29 // ____ __. 30 // | |/ _|____ ___.__. ______ 31 // | <_/ __ < | |/ ___/ 32 // | | \ ___/\___ |\___ \ 33 // |____|__ \___ > ____/____ > 34 // \/ \/\/ \/ 35 // 36 // This file contains functions for creating authorized_keys files 37 // 38 // There is a dependence on the database within RegeneratePublicKeys however most of these functions probably belong in a module 39 40 const ( 41 tplCommentPrefix = `# gitea public key` 42 tplPublicKey = tplCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict %s` + "\n" 43 ) 44 45 var sshOpLocker sync.Mutex 46 47 // AuthorizedStringForKey creates the authorized keys string appropriate for the provided key 48 func AuthorizedStringForKey(key *PublicKey) string { 49 sb := &strings.Builder{} 50 _ = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sb, map[string]any{ 51 "AppPath": util.ShellEscape(setting.AppPath), 52 "AppWorkPath": util.ShellEscape(setting.AppWorkPath), 53 "CustomConf": util.ShellEscape(setting.CustomConf), 54 "CustomPath": util.ShellEscape(setting.CustomPath), 55 "Key": key, 56 }) 57 58 return fmt.Sprintf(tplPublicKey, util.ShellEscape(sb.String()), key.Content) 59 } 60 61 // appendAuthorizedKeysToFile appends new SSH keys' content to authorized_keys file. 62 func appendAuthorizedKeysToFile(keys ...*PublicKey) error { 63 // Don't need to rewrite this file if builtin SSH server is enabled. 64 if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile { 65 return nil 66 } 67 68 sshOpLocker.Lock() 69 defer sshOpLocker.Unlock() 70 71 if setting.SSH.RootPath != "" { 72 // First of ensure that the RootPath is present, and if not make it with 0700 permissions 73 // This of course doesn't guarantee that this is the right directory for authorized_keys 74 // but at least if it's supposed to be this directory and it doesn't exist and we're the 75 // right user it will at least be created properly. 76 err := os.MkdirAll(setting.SSH.RootPath, 0o700) 77 if err != nil { 78 log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err) 79 return err 80 } 81 } 82 83 fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys") 84 f, err := os.OpenFile(fPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600) 85 if err != nil { 86 return err 87 } 88 defer f.Close() 89 90 // Note: chmod command does not support in Windows. 91 if !setting.IsWindows { 92 fi, err := f.Stat() 93 if err != nil { 94 return err 95 } 96 97 // .ssh directory should have mode 700, and authorized_keys file should have mode 600. 98 if fi.Mode().Perm() > 0o600 { 99 log.Error("authorized_keys file has unusual permission flags: %s - setting to -rw-------", fi.Mode().Perm().String()) 100 if err = f.Chmod(0o600); err != nil { 101 return err 102 } 103 } 104 } 105 106 for _, key := range keys { 107 if key.Type == KeyTypePrincipal { 108 continue 109 } 110 if _, err = f.WriteString(key.AuthorizedString()); err != nil { 111 return err 112 } 113 } 114 return nil 115 } 116 117 // RewriteAllPublicKeys removes any authorized key and rewrite all keys from database again. 118 // Note: db.GetEngine(db.DefaultContext).Iterate does not get latest data after insert/delete, so we have to call this function 119 // outside any session scope independently. 120 func RewriteAllPublicKeys(ctx context.Context) error { 121 // Don't rewrite key if internal server 122 if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile { 123 return nil 124 } 125 126 sshOpLocker.Lock() 127 defer sshOpLocker.Unlock() 128 129 if setting.SSH.RootPath != "" { 130 // First of ensure that the RootPath is present, and if not make it with 0700 permissions 131 // This of course doesn't guarantee that this is the right directory for authorized_keys 132 // but at least if it's supposed to be this directory and it doesn't exist and we're the 133 // right user it will at least be created properly. 134 err := os.MkdirAll(setting.SSH.RootPath, 0o700) 135 if err != nil { 136 log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err) 137 return err 138 } 139 } 140 141 fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys") 142 tmpPath := fPath + ".tmp" 143 t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) 144 if err != nil { 145 return err 146 } 147 defer func() { 148 t.Close() 149 if err := util.Remove(tmpPath); err != nil { 150 log.Warn("Unable to remove temporary authorized keys file: %s: Error: %v", tmpPath, err) 151 } 152 }() 153 154 if setting.SSH.AuthorizedKeysBackup { 155 isExist, err := util.IsExist(fPath) 156 if err != nil { 157 log.Error("Unable to check if %s exists. Error: %v", fPath, err) 158 return err 159 } 160 if isExist { 161 bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix()) 162 if err = util.CopyFile(fPath, bakPath); err != nil { 163 return err 164 } 165 } 166 } 167 168 if err := RegeneratePublicKeys(ctx, t); err != nil { 169 return err 170 } 171 172 t.Close() 173 return util.Rename(tmpPath, fPath) 174 } 175 176 // RegeneratePublicKeys regenerates the authorized_keys file 177 func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error { 178 if err := db.GetEngine(ctx).Where("type != ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean any) (err error) { 179 _, err = t.WriteString((bean.(*PublicKey)).AuthorizedString()) 180 return err 181 }); err != nil { 182 return err 183 } 184 185 fPath := filepath.Join(setting.SSH.RootPath, "authorized_keys") 186 isExist, err := util.IsExist(fPath) 187 if err != nil { 188 log.Error("Unable to check if %s exists. Error: %v", fPath, err) 189 return err 190 } 191 if isExist { 192 f, err := os.Open(fPath) 193 if err != nil { 194 return err 195 } 196 scanner := bufio.NewScanner(f) 197 for scanner.Scan() { 198 line := scanner.Text() 199 if strings.HasPrefix(line, tplCommentPrefix) { 200 scanner.Scan() 201 continue 202 } 203 _, err = t.WriteString(line + "\n") 204 if err != nil { 205 f.Close() 206 return err 207 } 208 } 209 f.Close() 210 } 211 return nil 212 }