Compare commits

...

7 Commits

Author SHA1 Message Date
Timmy Welch
67b8cd2449 Enable start on boot
Some checks failed
Push / CI (push) Has been cancelled
2025-07-12 14:35:50 -07:00
Timmy Welch
01a983bea8 Use a login shell if no command is given 2025-07-12 14:34:43 -07:00
dependabot[bot]
04ed8761da Bump golang.org/x/crypto from 0.33.0 to 0.35.0 (#23)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.33.0 to 0.35.0.
- [Commits](https://github.com/golang/crypto/compare/v0.33.0...v0.35.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.35.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-12 14:33:27 -07:00
Timmy Welch
f736b48d5d testing
All checks were successful
Push / CI (push) Successful in 12m32s
2025-05-15 14:37:10 -07:00
Timmy Welch
b499ac2afe Merge remote-tracking branch 'upstream/main'
Some checks failed
Push / CI (push) Has been cancelled
2025-03-09 15:45:46 -07:00
Timmy Welch
f4af94e536 Install apps to /bin on startup 2025-03-09 15:37:52 -07:00
Timmy Welch
bba58e7a3a Stuff
Some checks failed
Push / CI (push) Has been cancelled
Implement certificate authentication, certificate requires :gokrazy: principal
Read first line of /etc/passwd for home and shell
Shell uses `-l` to make it a login shell which will run .profile
2025-02-16 17:53:26 -08:00
4 changed files with 106 additions and 25 deletions

View File

@@ -10,12 +10,14 @@ import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"path"
"strings"
"syscall"
@@ -31,6 +33,10 @@ var (
"/perm/breakglass.authorized_keys",
"path to an OpenSSH authorized_keys file; if the value is 'ec2', fetch the SSH key(s) from the AWS IMDSv2 metadata")
authorizedUserCAPath = flag.String("authorized_ca",
"/perm/breakglass.authorized_user_ca",
"path to an OpenSSH TrustedUserCAKeys file; note the certificate must list ':gokrazy:' as a valid principal")
hostKeyPath = flag.String("host_key",
"/perm/breakglass.host_key",
"path to a PEM-encoded RSA, DSA or ECDSA private key (create using e.g. ssh-keygen -f /perm/breakglass.host_key -N '' -t rsa)")
@@ -46,6 +52,9 @@ var (
forwarding = flag.String("forward",
"",
"allow port forwarding. Use `loopback` for loopback interfaces and `private-network` for private networks")
home = "/perm/home"
shell = ""
)
func loadAuthorizedKeys(path string) (map[string]bool, error) {
@@ -81,6 +90,19 @@ func loadAuthorizedKeys(path string) (map[string]bool, error) {
return result, nil
}
func loadPasswd(passwd string) {
b, err := os.ReadFile(passwd)
if err != nil {
return
}
fields := bytes.SplitN(bytes.SplitN(b, []byte("\n"), 2)[0], []byte(":"), 7)
if len(fields) != 7 {
return
}
home = path.Clean(string(fields[5]))
shell = path.Clean(string(fields[6]))
}
func loadHostKey(path string) (ssh.Signer, error) {
b, err := ioutil.ReadFile(path)
if err != nil {
@@ -154,7 +176,7 @@ func initMOTD() error {
return err
}
motd = fmt.Sprintf(` __
motd = fmt.Sprintf(` __
.-----.-----| |--.----.---.-.-----.--.--.
| _ | _ | <| _| _ |-- __| | |
|___ |_____|__|__|__| |___._|_____|___ |
@@ -172,7 +194,9 @@ func main() {
flag.Parse()
log.SetFlags(log.LstdFlags | log.Lshortfile)
gokrazy.DontStartOnBoot()
installBusybox()
loadPasswd("/etc/passwd")
authorizedKeys, err := loadAuthorizedKeys(*authorizedKeysPath)
if err != nil {
@@ -182,19 +206,59 @@ func main() {
log.Fatal(err)
}
authorizedUserCertificateCA, err := loadAuthorizedKeys(strings.TrimPrefix(*authorizedUserCAPath, "ec2"))
if err != nil {
if os.IsNotExist(err) {
log.Printf("TrustedUserCAKeys not loaded")
}
}
if err := initMOTD(); err != nil {
log.Print(err)
}
config := &ssh.ServerConfig{
PublicKeyCallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
certChecker := ssh.CertChecker{
IsUserAuthority: func(auth ssh.PublicKey) bool {
return authorizedUserCertificateCA[string(auth.Marshal())]
},
UserKeyFallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
if authorizedKeys[string(pubKey.Marshal())] {
log.Printf("user %q successfully authorized from remote addr %s", conn.User(), conn.RemoteAddr())
return nil, nil
return &ssh.Permissions{map[string]string{}, map[string]string{}}, nil
}
return nil, fmt.Errorf("public key not found in %s", *authorizedKeysPath)
},
}
config := &ssh.ServerConfig{
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
cert, ok := key.(*ssh.Certificate)
if !ok {
if certChecker.UserKeyFallback != nil {
return certChecker.UserKeyFallback(conn, key)
}
return nil, errors.New("ssh: normal key pairs not accepted")
}
if cert.CertType != ssh.UserCert {
return nil, fmt.Errorf("ssh: cert has type %d", cert.CertType)
}
if !certChecker.IsUserAuthority(cert.SignatureKey) {
return nil, fmt.Errorf("ssh: certificate signed by unrecognized authority")
}
if err := certChecker.CheckCert(":gokrazy:", cert); err != nil {
return nil, err
}
if cert.Permissions.CriticalOptions == nil {
cert.Permissions.CriticalOptions = map[string]string{}
}
if cert.Permissions.Extensions == nil {
cert.Permissions.Extensions = map[string]string{}
}
return &cert.Permissions, nil
},
}
signer, err := loadHostKey(*hostKeyPath)
if err != nil {
@@ -244,7 +308,7 @@ func main() {
}
go func(conn net.Conn) {
_, chans, reqs, err := ssh.NewServerConn(conn, config)
c, chans, reqs, err := ssh.NewServerConn(conn, config)
if err != nil {
log.Printf("handshake: %v", err)
return
@@ -254,7 +318,7 @@ func main() {
go ssh.DiscardRequests(reqs)
for newChannel := range chans {
handleChannel(newChannel)
handleChannel(newChannel, c)
}
}(conn)
}

2
go.mod
View File

@@ -10,7 +10,7 @@ require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/kr/pty v1.1.8
github.com/pkg/sftp v1.13.5
golang.org/x/crypto v0.33.0
golang.org/x/crypto v0.35.0
)
require (

4
go.sum
View File

@@ -33,8 +33,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=

45
ssh.go
View File

@@ -2,12 +2,14 @@ package main
import (
"context"
"errors"
"fmt"
"io"
"log"
"net"
"os"
"os/exec"
"path"
"strconv"
"strings"
"sync"
@@ -21,11 +23,15 @@ import (
"golang.org/x/crypto/ssh"
)
func handleChannel(newChan ssh.NewChannel) {
func handleChannel(newChan ssh.NewChannel, conn *ssh.ServerConn) {
switch t := newChan.ChannelType(); t {
case "session":
handleSession(newChan)
handleSession(newChan, conn)
case "direct-tcpip":
if _, portForwardDenied := conn.Permissions.Extensions["no-port-forwarding"]; portForwardDenied {
newChan.Reject(ssh.Prohibited, "port forwarding is disabled. For you in particular :-P")
return
}
handleTCPIP(newChan)
default:
newChan.Reject(ssh.UnknownChannelType, fmt.Sprintf("unknown channel type: %q", t))
@@ -112,7 +118,7 @@ func handleTCPIP(newChan ssh.NewChannel) {
}()
}
func handleSession(newChannel ssh.NewChannel) {
func handleSession(newChannel ssh.NewChannel, conn *ssh.ServerConn) {
channel, requests, err := newChannel.Accept()
if err != nil {
log.Printf("Could not accept channel (%s)", err)
@@ -120,12 +126,12 @@ func handleSession(newChannel ssh.NewChannel) {
}
// Sessions have out-of-band requests such as "shell", "pty-req" and "env"
go func(channel ssh.Channel, requests <-chan *ssh.Request) {
go func(channel ssh.Channel, requests <-chan *ssh.Request, conn *ssh.ServerConn) {
ctx, canc := context.WithCancel(context.Background())
defer canc()
s := session{channel: channel}
for req := range requests {
if err := s.request(ctx, req); err != nil {
if err := s.request(ctx, req, conn); err != nil {
log.Printf("request(%q): %v", req.Type, err)
errmsg := []byte(err.Error())
// Append a trailing newline; the error message is
@@ -139,7 +145,7 @@ func handleSession(newChannel ssh.NewChannel) {
}
}
log.Printf("requests exhausted")
}(channel, requests)
}(channel, requests, conn)
}
func expandPath(env []string) []string {
@@ -220,24 +226,31 @@ func findShell() string {
// in standard locations (makes Emacs TRAMP work, for example).
if err := installBusybox(); err != nil {
log.Printf("installing busybox failed: %v", err)
// fallthrough
} else {
return "/bin/sh" // available after installation
// fallthrough, we don't return /bin/sh as we read /etc/passwd
}
}
if _, err := exec.LookPath(shell); path.IsAbs(shell) && err == nil {
return shell
}
if path, err := exec.LookPath("bash"); err == nil {
return path
}
if path, err := exec.LookPath("sh"); err == nil {
return path
}
const wellKnownSerialShell = "/tmp/serial-busybox/ash"
if _, err := os.Stat(wellKnownSerialShell); err == nil {
if _, err := exec.LookPath(wellKnownSerialShell); err == nil {
return wellKnownSerialShell
}
return ""
}
func (s *session) request(ctx context.Context, req *ssh.Request) error {
func (s *session) request(ctx context.Context, req *ssh.Request, conn *ssh.ServerConn) error {
switch req.Type {
case "pty-req":
if _, portForwardDenied := conn.Permissions.Extensions["no-pty"]; portForwardDenied {
return errors.New("Pseudo-Terminal is disabled. For you in particular :-P")
}
var r ptyreq
if err := ssh.Unmarshal(req.Payload, &r); err != nil {
return err
@@ -355,21 +368,25 @@ func (s *session) request(ctx context.Context, req *ssh.Request) error {
// Ensure the $HOME directory exists so that shell history works without
// any extra steps.
if err := os.MkdirAll("/perm/home", 0755); err != nil {
if err := os.MkdirAll(home, 0755); err != nil {
// TODO: Suppress -EROFS
log.Print(err)
}
var cmd *exec.Cmd
if shell := findShell(); shell != "" {
cmd = exec.CommandContext(ctx, shell, "-c", r.Command)
if len(cmdline) == 0 || (len(cmdline) == 1 && cmdline[0] == "sh") {
cmd = exec.CommandContext(ctx, shell, "-l")
} else {
cmd = exec.CommandContext(ctx, shell, "-c", r.Command)
}
} else {
cmd = exec.CommandContext(ctx, cmdline[0], cmdline[1:]...)
}
log.Printf("Starting cmd %q", cmd.Args)
env := expandPath(s.env)
env = append(env,
"HOME=/perm/home",
"HOME="+home,
"TMPDIR=/tmp")
cmd.Env = env
cmd.SysProcAttr = &syscall.SysProcAttr{}