You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ssh_key_authorized_keys.go 6.3KB

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