Skip to content

Commit 9066d09

Browse files
42wimsilverwindzeripathlafrikstechknowlogick
authored
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 <[email protected]> * Remove unused locale string * Update options/locale/locale_en-US.ini Co-authored-by: silverwind <[email protected]> * Update options/locale/locale_en-US.ini Co-authored-by: silverwind <[email protected]> * Update models/ssh_key.go Co-authored-by: silverwind <[email protected]> * Add missing creation of SSH.Rootpath * Update cheatsheet, example and locale strings * Update models/ssh_key.go Co-authored-by: zeripath <[email protected]> * Update models/ssh_key.go Co-authored-by: zeripath <[email protected]> * Update models/ssh_key.go Co-authored-by: zeripath <[email protected]> * Update models/ssh_key.go Co-authored-by: zeripath <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * Add the command actually to the dashboard template * Update models/ssh_key.go Co-authored-by: silverwind <[email protected]> * By default do not show principal options unless there are CA keys set or they are explicitly set Signed-off-by: Andrew Thornton <[email protected]> * 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 <[email protected]> * Fix linting issue Co-authored-by: silverwind <[email protected]> Co-authored-by: zeripath <[email protected]> Co-authored-by: Lauris BH <[email protected]> Co-authored-by: techknowlogick <[email protected]> Co-authored-by: techknowlogick <[email protected]>
1 parent 7eb8460 commit 9066d09

File tree

15 files changed

+557
-28
lines changed

15 files changed

+557
-28
lines changed

cmd/serv.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,12 @@ func runServ(c *cli.Context) error {
113113
if err != nil {
114114
fail("Internal error", "Failed to check provided key: %v", err)
115115
}
116-
if key.Type == models.KeyTypeDeploy {
116+
switch key.Type {
117+
case models.KeyTypeDeploy:
117118
println("Hi there! You've successfully authenticated with the deploy key named " + key.Name + ", but Gitea does not provide shell access.")
118-
} else {
119+
case models.KeyTypePrincipal:
120+
println("Hi there! You've successfully authenticated with the principal " + key.Content + ", but Gitea does not provide shell access.")
121+
default:
119122
println("Hi there, " + user.Name + "! You've successfully authenticated with the key named " + key.Name + ", but Gitea does not provide shell access.")
120123
}
121124
println("If this is unexpected, please log in with password and setup Gitea under another user.")

custom/conf/app.example.ini

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,9 @@ SSH_ROOT_PATH =
297297
; Gitea will create a authorized_keys file by default when it is not using the internal ssh server
298298
; If you intend to use the AuthorizedKeysCommand functionality then you should turn this off.
299299
SSH_CREATE_AUTHORIZED_KEYS_FILE = true
300+
; Gitea will create a authorized_principals file by default when it is not using the internal ssh server
301+
; If you intend to use the AuthorizedPrincipalsCommand functionality then you should turn this off.
302+
SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE = true
300303
; For the built-in SSH server, choose the ciphers to support for SSH connections,
301304
; for system SSH this setting has no effect
302305
SSH_SERVER_CIPHERS = aes128-ctr, aes192-ctr, aes256-ctr, [email protected], arcfour256, arcfour128
@@ -312,7 +315,26 @@ SSH_KEY_TEST_PATH =
312315
; Path to ssh-keygen, default is 'ssh-keygen' which means the shell is responsible for finding out which one to call.
313316
SSH_KEYGEN_PATH = ssh-keygen
314317
; Enable SSH Authorized Key Backup when rewriting all keys, default is true
315-
SSH_BACKUP_AUTHORIZED_KEYS = true
318+
SSH_AUTHORIZED_KEYS_BACKUP = true
319+
; Determines which principals to allow
320+
; - empty: if SSH_TRUSTED_USER_CA_KEYS is empty this will default to off, otherwise will default to email, username.
321+
; - off: Do not allow authorized principals
322+
; - email: the principal must match the user's email
323+
; - username: the principal must match the user's username
324+
; - anything: there will be no checking on the content of the principal
325+
SSH_AUTHORIZED_PRINCIPALS_ALLOW = email, username
326+
; Enable SSH Authorized Principals Backup when rewriting all keys, default is true
327+
SSH_AUTHORIZED_PRINCIPALS_BACKUP = true
328+
; Specifies the public keys of certificate authorities that are trusted to sign user certificates for authentication.
329+
; Multiple keys should be comma separated.
330+
; E.g."ssh-<algorithm> <key>". or "ssh-<algorithm> <key1>, ssh-<algorithm> <key2>".
331+
; For more information see "TrustedUserCAKeys" in the sshd config manpages.
332+
SSH_TRUSTED_USER_CA_KEYS =
333+
; Absolute path of the `TrustedUserCaKeys` file gitea will manage.
334+
; Default this `RUN_USER`/.ssh/gitea-trusted-user-ca-keys.pem
335+
; If you're running your own ssh server and you want to use the gitea managed file you'll also need to modify your
336+
; sshd_config to point to this file. The official docker image will automatically work without further configuration.
337+
SSH_TRUSTED_USER_CA_KEYS_FILENAME =
316338
; Enable exposure of SSH clone URL to anonymous visitors, default is false
317339
SSH_EXPOSE_ANONYMOUS = false
318340
; Indicate whether to check minimum key size with corresponding type

docker/root/etc/templates/sshd_config

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ HostKey /data/ssh/ssh_host_ecdsa_key
1313
HostKey /data/ssh/ssh_host_dsa_key
1414

1515
AuthorizedKeysFile .ssh/authorized_keys
16+
AuthorizedPrincipalsFile .ssh/authorized_principals
17+
TrustedUserCAKeys /data/git/.ssh/gitea-trusted-user-ca-keys.pem
18+
CASignatureAlgorithms ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,[email protected],ssh-ed25519,[email protected],rsa-sha2-512,rsa-sha2-256,ssh-rsa
1619

1720
UseDNS no
1821
AllowAgentForwarding no

docs/content/doc/advanced/config-cheat-sheet.en-us.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,11 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
251251
- `SSH_LISTEN_PORT`: **%(SSH\_PORT)s**: Port for the built-in SSH server.
252252
- `SSH_ROOT_PATH`: **~/.ssh**: Root path of SSH directory.
253253
- `SSH_CREATE_AUTHORIZED_KEYS_FILE`: **true**: Gitea will create a authorized_keys file by default when it is not using the internal ssh server. If you intend to use the AuthorizedKeysCommand functionality then you should turn this off.
254+
- `SSH_TRUSTED_USER_CA_KEYS`: **\<empty\>**: Specifies the public keys of certificate authorities that are trusted to sign user certificates for authentication. Multiple keys should be comma separated. E.g.`ssh-<algorithm> <key>` or `ssh-<algorithm> <key1>, ssh-<algorithm> <key2>`. For more information see `TrustedUserCAKeys` in the sshd config man pages. When empty no file will be created and `SSH_AUTHORIZED_PRINCIPALS_ALLOW` will default to `off`.
255+
- `SSH_TRUSTED_USER_CA_KEYS_FILENAME`: **`RUN_USER`/.ssh/gitea-trusted-user-ca-keys.pem**: Absolute path of the `TrustedUserCaKeys` file gitea will manage. If you're running your own ssh server and you want to use the gitea managed file you'll also need to modify your sshd_config to point to this file. The official docker image will automatically work without further configuration.
256+
- `SSH_AUTHORIZED_PRINCIPALS_ALLOW`: **off** or **username, email**: \[off, username, email, anything\]: Specify the principals values that users are allowed to use as principal. When set to `anything` no checks are done on the principal string. When set to `off` authorized principal are not allowed to be set.
257+
- `SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE`: **false/true**: Gitea will create a authorized_principals file by default when it is not using the internal ssh server and `SSH_AUTHORIZED_PRINCIPALS_ALLOW` is not `off`.
258+
- `SSH_AUTHORIZED_PRINCIPALS_BACKUP`: **false/true**: Enable SSH Authorized Principals Backup when rewriting all keys, default is true if `SSH_AUTHORIZED_PRINCIPALS_ALLOW` is not `off`.
254259
- `SSH_SERVER_CIPHERS`: **aes128-ctr, aes192-ctr, aes256-ctr, [email protected], arcfour256, arcfour128**: For the built-in SSH server, choose the ciphers to support for SSH connections, for system SSH this setting has no effect.
255260
- `SSH_SERVER_KEY_EXCHANGES`: **diffie-hellman-group1-sha1, diffie-hellman-group14-sha1, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, [email protected]**: For the built-in SSH server, choose the key exchange algorithms to support for SSH connections, for system SSH this setting has no effect.
256261
- `SSH_SERVER_MACS`: **[email protected], hmac-sha2-256, hmac-sha1, hmac-sha1-96**: For the built-in SSH server, choose the MACs to support for SSH connections, for system SSH this setting has no effect

models/ssh_key.go

Lines changed: 234 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ const (
4040
tplCommentPrefix = `# gitea public key`
4141
tplCommand = "%s --config=%s serv key-%d"
4242
tplPublicKey = tplCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s` + "\n"
43+
44+
authorizedPrincipalsFile = "authorized_principals"
4345
)
4446

4547
var sshOpLocker sync.Mutex
@@ -52,6 +54,8 @@ const (
5254
KeyTypeUser = iota + 1
5355
// KeyTypeDeploy specifies the deploy key
5456
KeyTypeDeploy
57+
// KeyTypePrincipal specifies the authorized principal key
58+
KeyTypePrincipal
5559
)
5660

5761
// PublicKey represents a user or deploy SSH public key.
@@ -401,6 +405,9 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
401405
}
402406

403407
for _, key := range keys {
408+
if key.Type == KeyTypePrincipal {
409+
continue
410+
}
404411
if _, err = f.WriteString(key.AuthorizedString()); err != nil {
405412
return err
406413
}
@@ -571,6 +578,25 @@ func SearchPublicKeyByContent(content string) (*PublicKey, error) {
571578
return searchPublicKeyByContentWithEngine(x, content)
572579
}
573580

581+
func searchPublicKeyByContentExactWithEngine(e Engine, content string) (*PublicKey, error) {
582+
key := new(PublicKey)
583+
has, err := e.
584+
Where("content = ?", content).
585+
Get(key)
586+
if err != nil {
587+
return nil, err
588+
} else if !has {
589+
return nil, ErrKeyNotExist{}
590+
}
591+
return key, nil
592+
}
593+
594+
// SearchPublicKeyByContentExact searches content
595+
// and returns public key found.
596+
func SearchPublicKeyByContentExact(content string) (*PublicKey, error) {
597+
return searchPublicKeyByContentExactWithEngine(x, content)
598+
}
599+
574600
// SearchPublicKey returns a list of public keys matching the provided arguments.
575601
func SearchPublicKey(uid int64, fingerprint string) ([]*PublicKey, error) {
576602
keys := make([]*PublicKey, 0, 5)
@@ -586,7 +612,7 @@ func SearchPublicKey(uid int64, fingerprint string) ([]*PublicKey, error) {
586612

587613
// ListPublicKeys returns a list of public keys belongs to given user.
588614
func ListPublicKeys(uid int64, listOptions ListOptions) ([]*PublicKey, error) {
589-
sess := x.Where("owner_id = ?", uid)
615+
sess := x.Where("owner_id = ? AND type != ?", uid, KeyTypePrincipal)
590616
if listOptions.Page != 0 {
591617
sess = listOptions.setSessionPagination(sess)
592618

@@ -662,6 +688,10 @@ func DeletePublicKey(doer *User, id int64) (err error) {
662688
}
663689
sess.Close()
664690

691+
if key.Type == KeyTypePrincipal {
692+
return RewriteAllPrincipalKeys()
693+
}
694+
665695
return RewriteAllPublicKeys()
666696
}
667697

@@ -727,11 +757,10 @@ func RegeneratePublicKeys(t io.StringWriter) error {
727757
}
728758

729759
func regeneratePublicKeys(e Engine, t io.StringWriter) error {
730-
err := e.Iterate(new(PublicKey), func(idx int, bean interface{}) (err error) {
760+
if err := e.Where("type != ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean interface{}) (err error) {
731761
_, err = t.WriteString((bean.(*PublicKey)).AuthorizedString())
732762
return err
733-
})
734-
if err != nil {
763+
}); err != nil {
735764
return err
736765
}
737766

@@ -1041,3 +1070,204 @@ func SearchDeployKeys(repoID int64, keyID int64, fingerprint string) ([]*DeployK
10411070
}
10421071
return keys, x.Where(cond).Find(&keys)
10431072
}
1073+
1074+
// __________ .__ .__ .__
1075+
// \______ _______|__| ____ ____ |_____________ | | ______
1076+
// | ___\_ __ | |/ \_/ ___\| \____ \__ \ | | / ___/
1077+
// | | | | \| | | \ \___| | |_> / __ \| |__\___ \
1078+
// |____| |__| |__|___| /\___ |__| __(____ |____/____ >
1079+
// \/ \/ |__| \/ \/
1080+
1081+
// AddPrincipalKey adds new principal to database and authorized_principals file.
1082+
func AddPrincipalKey(ownerID int64, content string, loginSourceID int64) (*PublicKey, error) {
1083+
sess := x.NewSession()
1084+
defer sess.Close()
1085+
if err := sess.Begin(); err != nil {
1086+
return nil, err
1087+
}
1088+
1089+
// Principals cannot be duplicated.
1090+
has, err := sess.
1091+
Where("content = ? AND type = ?", content, KeyTypePrincipal).
1092+
Get(new(PublicKey))
1093+
if err != nil {
1094+
return nil, err
1095+
} else if has {
1096+
return nil, ErrKeyAlreadyExist{0, "", content}
1097+
}
1098+
1099+
key := &PublicKey{
1100+
OwnerID: ownerID,
1101+
Name: content,
1102+
Content: content,
1103+
Mode: AccessModeWrite,
1104+
Type: KeyTypePrincipal,
1105+
LoginSourceID: loginSourceID,
1106+
}
1107+
if err = addPrincipalKey(sess, key); err != nil {
1108+
return nil, fmt.Errorf("addKey: %v", err)
1109+
}
1110+
1111+
if err = sess.Commit(); err != nil {
1112+
return nil, err
1113+
}
1114+
1115+
sess.Close()
1116+
1117+
return key, RewriteAllPrincipalKeys()
1118+
}
1119+
1120+
func addPrincipalKey(e Engine, key *PublicKey) (err error) {
1121+
// Save Key representing a principal.
1122+
if _, err = e.Insert(key); err != nil {
1123+
return err
1124+
}
1125+
1126+
return nil
1127+
}
1128+
1129+
// CheckPrincipalKeyString strips spaces and returns an error if the given principal contains newlines
1130+
func CheckPrincipalKeyString(user *User, content string) (_ string, err error) {
1131+
if setting.SSH.Disabled {
1132+
return "", ErrSSHDisabled{}
1133+
}
1134+
1135+
content = strings.TrimSpace(content)
1136+
if strings.ContainsAny(content, "\r\n") {
1137+
return "", errors.New("only a single line with a single principal please")
1138+
}
1139+
1140+
// check all the allowed principals, email, username or anything
1141+
// if any matches, return ok
1142+
for _, v := range setting.SSH.AuthorizedPrincipalsAllow {
1143+
switch v {
1144+
case "anything":
1145+
return content, nil
1146+
case "email":
1147+
emails, err := GetEmailAddresses(user.ID)
1148+
if err != nil {
1149+
return "", err
1150+
}
1151+
for _, email := range emails {
1152+
if !email.IsActivated {
1153+
continue
1154+
}
1155+
if content == email.Email {
1156+
return content, nil
1157+
}
1158+
}
1159+
1160+
case "username":
1161+
if content == user.Name {
1162+
return content, nil
1163+
}
1164+
}
1165+
}
1166+
1167+
return "", fmt.Errorf("didn't match allowed principals: %s", setting.SSH.AuthorizedPrincipalsAllow)
1168+
}
1169+
1170+
// RewriteAllPrincipalKeys removes any authorized principal and rewrite all keys from database again.
1171+
// Note: x.Iterate does not get latest data after insert/delete, so we have to call this function
1172+
// outside any session scope independently.
1173+
func RewriteAllPrincipalKeys() error {
1174+
return rewriteAllPrincipalKeys(x)
1175+
}
1176+
1177+
func rewriteAllPrincipalKeys(e Engine) error {
1178+
// Don't rewrite key if internal server
1179+
if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedPrincipalsFile {
1180+
return nil
1181+
}
1182+
1183+
sshOpLocker.Lock()
1184+
defer sshOpLocker.Unlock()
1185+
1186+
if setting.SSH.RootPath != "" {
1187+
// First of ensure that the RootPath is present, and if not make it with 0700 permissions
1188+
// This of course doesn't guarantee that this is the right directory for authorized_keys
1189+
// but at least if it's supposed to be this directory and it doesn't exist and we're the
1190+
// right user it will at least be created properly.
1191+
err := os.MkdirAll(setting.SSH.RootPath, 0700)
1192+
if err != nil {
1193+
log.Error("Unable to MkdirAll(%s): %v", setting.SSH.RootPath, err)
1194+
return err
1195+
}
1196+
}
1197+
1198+
fPath := filepath.Join(setting.SSH.RootPath, authorizedPrincipalsFile)
1199+
tmpPath := fPath + ".tmp"
1200+
t, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
1201+
if err != nil {
1202+
return err
1203+
}
1204+
defer func() {
1205+
t.Close()
1206+
os.Remove(tmpPath)
1207+
}()
1208+
1209+
if setting.SSH.AuthorizedPrincipalsBackup && com.IsExist(fPath) {
1210+
bakPath := fmt.Sprintf("%s_%d.gitea_bak", fPath, time.Now().Unix())
1211+
if err = com.Copy(fPath, bakPath); err != nil {
1212+
return err
1213+
}
1214+
}
1215+
1216+
if err := regeneratePrincipalKeys(e, t); err != nil {
1217+
return err
1218+
}
1219+
1220+
t.Close()
1221+
return os.Rename(tmpPath, fPath)
1222+
}
1223+
1224+
// ListPrincipalKeys returns a list of principals belongs to given user.
1225+
func ListPrincipalKeys(uid int64, listOptions ListOptions) ([]*PublicKey, error) {
1226+
sess := x.Where("owner_id = ? AND type = ?", uid, KeyTypePrincipal)
1227+
if listOptions.Page != 0 {
1228+
sess = listOptions.setSessionPagination(sess)
1229+
1230+
keys := make([]*PublicKey, 0, listOptions.PageSize)
1231+
return keys, sess.Find(&keys)
1232+
}
1233+
1234+
keys := make([]*PublicKey, 0, 5)
1235+
return keys, sess.Find(&keys)
1236+
}
1237+
1238+
// RegeneratePrincipalKeys regenerates the authorized_principals file
1239+
func RegeneratePrincipalKeys(t io.StringWriter) error {
1240+
return regeneratePrincipalKeys(x, t)
1241+
}
1242+
1243+
func regeneratePrincipalKeys(e Engine, t io.StringWriter) error {
1244+
if err := e.Where("type = ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean interface{}) (err error) {
1245+
_, err = t.WriteString((bean.(*PublicKey)).AuthorizedString())
1246+
return err
1247+
}); err != nil {
1248+
return err
1249+
}
1250+
1251+
fPath := filepath.Join(setting.SSH.RootPath, authorizedPrincipalsFile)
1252+
if com.IsExist(fPath) {
1253+
f, err := os.Open(fPath)
1254+
if err != nil {
1255+
return err
1256+
}
1257+
scanner := bufio.NewScanner(f)
1258+
for scanner.Scan() {
1259+
line := scanner.Text()
1260+
if strings.HasPrefix(line, tplCommentPrefix) {
1261+
scanner.Scan()
1262+
continue
1263+
}
1264+
_, err = t.WriteString(line + "\n")
1265+
if err != nil {
1266+
f.Close()
1267+
return err
1268+
}
1269+
}
1270+
f.Close()
1271+
}
1272+
return nil
1273+
}

models/user.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1254,6 +1254,10 @@ func deleteUser(e *xorm.Session, u *User) error {
12541254
if err != nil {
12551255
return err
12561256
}
1257+
err = rewriteAllPrincipalKeys(e)
1258+
if err != nil {
1259+
return err
1260+
}
12571261
// ***** END: PublicKey *****
12581262

12591263
// ***** START: GPGPublicKey *****

0 commit comments

Comments
 (0)