| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | "errors" |
| 7 | "fmt" |
| 8 | "io" |
| 9 | "log" |
| 10 | "os" |
| 11 | "os/user" |
| 12 | "strconv" |
| 13 | "strings" |
| 14 | |
| 15 | "github.com/carlosframework/platform/internal/rootaws" |
| 16 | "github.com/carlosframework/platform/internal/safefile" |
| 17 | "golang.org/x/sys/unix" |
| 18 | ) |
| 19 | |
| 20 | // openOffcloudCredentials pins the parent and refuses links and special files |
| 21 | // before either ownership or mode is changed. O_PATH inspects special files |
| 22 | // without opening a device or blocking on a planted FIFO. |
| 23 | func openOffcloudCredentials(path string) (_ *os.File, err error) { |
| 24 | defer func() { |
| 25 | if err != nil && !errors.Is(err, os.ErrNotExist) { |
| 26 | log.Printf("ALERT: refusing unsafe offcloud credentials %s: %v", path, err) |
| 27 | } |
| 28 | }() |
| 29 | parent, name, err := openRegistryParent(path) |
| 30 | if err != nil { |
| 31 | return nil, err |
| 32 | } |
| 33 | defer parent.Close() |
| 34 | fd, err := unix.Openat2(int(parent.Fd()), name, &unix.OpenHow{ |
| 35 | Flags: unix.O_PATH | unix.O_NOFOLLOW | unix.O_CLOEXEC, |
| 36 | Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_XDEV, |
| 37 | }) |
| 38 | if err != nil { |
| 39 | return nil, err |
| 40 | } |
| 41 | f := os.NewFile(uintptr(fd), path) |
| 42 | defer f.Close() |
| 43 | var st unix.Stat_t |
| 44 | if err := unix.Fstat(fd, &st); err != nil { |
| 45 | return nil, err |
| 46 | } |
| 47 | if st.Mode&unix.S_IFMT != unix.S_IFREG || st.Nlink != 1 { |
| 48 | return nil, fmt.Errorf("not a plain single-link regular file") |
| 49 | } |
| 50 | // Reopen the pinned regular inode, never the caller's mutable pathname. |
| 51 | opened, err := unix.Open(fmt.Sprintf("/proc/self/fd/%d", fd), unix.O_RDONLY|unix.O_CLOEXEC, 0) |
| 52 | if err != nil { |
| 53 | return nil, err |
| 54 | } |
| 55 | return os.NewFile(uintptr(opened), path), nil |
| 56 | } |
| 57 | |
| 58 | func checkOffcloudCredentials(path string) (os.FileInfo, error) { |
| 59 | f, err := openOffcloudCredentials(path) |
| 60 | if err != nil { |
| 61 | return nil, err |
| 62 | } |
| 63 | defer f.Close() |
| 64 | return f.Stat() |
| 65 | } |
| 66 | |
| 67 | // Missing means a cloud box. Repair replaces the inode so a service process |
| 68 | // holding a writable descriptor cannot change the credentials root will read. |
| 69 | func repairOffcloudCredentials(path string) (err error) { |
| 70 | defer func() { |
| 71 | if err != nil { |
| 72 | log.Printf("ALERT: refusing unsafe offcloud credentials %s: %v", path, err) |
| 73 | } |
| 74 | }() |
| 75 | f, err := openOffcloudCredentials(path) |
| 76 | if errors.Is(err, os.ErrNotExist) { |
| 77 | return nil |
| 78 | } |
| 79 | if err != nil { |
| 80 | return err |
| 81 | } |
| 82 | defer f.Close() |
| 83 | g, err := user.LookupGroup(serviceUser) |
| 84 | if err != nil { |
| 85 | return err |
| 86 | } |
| 87 | gid, err := strconv.Atoi(g.Gid) |
| 88 | if err != nil { |
| 89 | return err |
| 90 | } |
| 91 | return secureOffcloudCredentials(f, gid) |
| 92 | } |
| 93 | |
| 94 | func secureOffcloudCredentials(f *os.File, gid int) (err error) { |
| 95 | defer func() { |
| 96 | if err != nil { |
| 97 | log.Printf("ALERT: refusing unsafe offcloud credentials %s: %v", f.Name(), err) |
| 98 | } |
| 99 | }() |
| 100 | var st unix.Stat_t |
| 101 | if err := unix.Fstat(int(f.Fd()), &st); err != nil { |
| 102 | return err |
| 103 | } |
| 104 | body, err := io.ReadAll(io.LimitReader(f, rootaws.MaxCredentialsSize+1)) |
| 105 | if err != nil { |
| 106 | return err |
| 107 | } |
| 108 | if len(body) > rootaws.MaxCredentialsSize { |
| 109 | return io.ErrShortBuffer |
| 110 | } |
| 111 | credentials, err := rootaws.ParseStaticCredentials(body) |
| 112 | if err != nil { |
| 113 | return err |
| 114 | } |
| 115 | if st.Uid == 0 && st.Gid == uint32(gid) && st.Mode&0o7777 == 0o640 { |
| 116 | return nil |
| 117 | } |
| 118 | var validated strings.Builder |
| 119 | fmt.Fprintf(&validated, "[default]\naws_access_key_id=%s\naws_secret_access_key=%s\n", credentials.AccessKeyID, credentials.SecretAccessKey) |
| 120 | if credentials.SessionToken != "" { |
| 121 | fmt.Fprintf(&validated, "aws_session_token=%s\n", credentials.SessionToken) |
| 122 | } |
| 123 | return safefile.Write(f.Name(), 0o640, func(w io.Writer) error { |
| 124 | fresh := w.(*os.File) |
| 125 | if err := fresh.Chown(0, gid); err != nil { |
| 126 | return err |
| 127 | } |
| 128 | _, err := io.WriteString(w, validated.String()) |
| 129 | return err |
| 130 | }) |
| 131 | } |
| 132 | |