Live data from Hacker News

How Go mitigates supply chain attacks

go.dev

151–160 of 265 posts

Re: How Go mitigates supply chain attacks

#151
post #30

The article, and the comments praising this approach, don’t do a great job of explaining how any of this is substantively different from running the likes of yarn install --frozen-lockfile, or cargo build --frozen. Here’s the thing: You can argue about being secure by default and encouraging better CI practices. I’d fully agree it isn’t great that one has to know a somewhat obscure flag to get a secure CI build in th…

the difference is in the part where you don't have a lock file yet (new dependency, upgrades), and need to choose versions for those.

Re: How Go mitigates supply chain attacks

#152
post #68
post #57

> Unlike most other package managers files, Go modules don’t have a separate list of constraints and a lock file pinning specific versions. The weird thing about the Go devs is there is always that little bit of elitism under the surface that I detect in their writing (whether it be colors in the playground, the GC, etc). I spent years writing Go and have now moved to Rust. What I find odd is the Rust team has done (…

This is a pretty genuinely confounding response, and I mean that with absolutely no offense intended. There is a tremendous amount of fighting between devs who prefer Go and Rust, and a tremendous amount of elitism as well, truly from both perspectives. Rust gained a reputation for elitism long before Go did; “Rust Evangelism Strike Force” was never meant to be pejorative, and “Rewrite it in Rust” was never meant to…

Without commenting on anything else here:

> “Rust Evangelism Strike Force” was never meant to be pejorative, and “Rewrite it in Rust” was never meant to be a joke

Maybe it's just been such a long time, but my recollection was exactly that: both of these terms were invented by outsiders intending to denigrate the Rust community, and became jokes inside the community as a means of recuperating them.

Okay, I will mention one other thing here:

> This article doesn’t mention Rust (that I noticed) and Go is still being measured up to Rust in the comments.

I agree with this in 99.999% of threads, this happens all the time, and probably shouldn't. However, in this thread in particular with the way that the Go package management story developed, including all of the drama there, I don't think it's surprising that Rust/Cargo get mentioned in comparison.

Re: How Go mitigates supply chain attacks

#153

Earlier quoted context omitted.

The version constraint is always listed in your top level go.mod file, so you know the dependency exists, no digging into the dependency tree required at all, and it’s not hidden in some lock file no one ever looks at. Plus, there are plenty of tools that help you with this problem, including the language server helping you directly in your editor and Dependabot on GitHub. I’m not aware of any languages that send you…

I'm sorry but I'm not super familiar with the workflow for working with dependencies in Go, I've only read about it. You say: > Raise the version there. Am I to understand that it's common to hand-edit the version constraint on a transitive dependency in your go.mod file? But that transitive dependency was first added there by the Go tool itself, right? How does a user easily keep track of what bits of data in the go…

> Am I to understand that it's common to hand-edit the version constraint on a transitive dependency in your go.mod file?

"Common" is probably not accurate, but what's wrong with hand editing it? If you mess up, your project won't compile until you fix it. You can update the versions a dozen different ways: editing by hand, `go get -u ` to update a specific dependency, `go get -u ./...` to update all dependencies, using your editor to view the go.mod file and select dependencies to update with the language server integration or by telling the language server to update all dependencies, by using Dependabot, or however else you like to do it. The options are all there for whatever way you're most comfortable with.

> But that transitive dependency was first added there by the Go tool itself, right?

So what? It's still your dependency now, and you are equally as responsible for watching after it as you are for any direct dependency. Any package management system that hides transitive dependencies is encouraging the user to ignore a significant fraction of the code that makes up their application. Every dependency is important.

> How does a user easily keep track of what bits of data in the go.mod file are hand-maintained and need to be preserved and which things were filled in implicitly by the tool traversing dependency graphs?

You already know[0] the answer to the most of that question, so I don't know why you're asking that part again. As a practical example, here is Caddy's go.mod file.[1] You can see the two distinct "require" blocks and the machine-generated comments that inform the reader that the second block is full of indirect dependencies. No one looking at this should be confused about which is which.

But the other part of that question doesn't really make sense. If you don't "preserve" the dependency, your code won't compile because there will be an unsatisfied dependency, regardless of whether you wrote the dependency there by hand or not, and regardless of whether it is a direct or indirect dependency. If you try to compile a program that depends on something not listed in the `go.mod` file, it won't compile. You can issue `go mod tidy` at any time to have Go edit your go.mod file for you to satisfy all constraints, so if you delete a line for whatever reason, `go mod tidy` can add it back, and Go doesn't update dependencies unless you ask it to, so `go mod tidy` won't muck around with other stuff in the process. There are very few ways the user can shoot themselves in the foot here.

Regardless, it is probably uncommon for people to manually edit anything in the go.mod file when there are so many tools that will handle it for you. The typical pattern for adding a dependency that I've observed is to add the import for the dependency from where you're trying to use it, and then tell your editor to update the go.mod file to include that dependency for you. The only time someone is likely to add a dependency to the go.mod file by hand editing it is when they need to do a "replace" operation to substitute one dependency in place of another (which applies throughout the dependency tree), usually only done when you need to patch a bug in third party code before the upstream is ready to merge that fix.

In my experience, most people either use Dependabot to keep up to date with their dependencies, or they update the dependencies using VS Code to view the go.mod file and click the "buttons"(/links/whatever) that the language server visually adds to the file to let you do the common tasks with a single click. They're both extremely simple to use and help you to update your direct and indirect dependencies.

> But I'm also not assuming they found a perfect solution to package management that all other package management tools failed to find. What's more likely is that they chose a different set of trade-offs, which is what this thread is exploring.

They were extremely late to the party, so they were able to learn from everyone else's mistakes. I really don't think it should be surprising that a latecomer is able to find solutions that other package management tools didn't, because the latecomer has the benefit of hindsight. They went from having some of the worst package management in the industry (effectively none; basically only beating out C and C++... and maybe Python, package management for which has been a nightmare for forever) to having arguably the best. Rust's Cargo comes extremely close, and I've used them both (as well as others) for the past 5+ years in several professional contexts. (Yes, that includes time before Go Modules existed, in the days when there was no real package management built in.) It seems like humans often want to assume that "things probably suck just as much everywhere else, just in different ways, and those people must simply be hiding it", but that's not always the case.

Some "trade-offs":

- For awhile, the `go` command would constantly be modifying your `go.mod` file for you whenever it didn't like what was in there, and that was definitely something I would have chalked up as a "trade-off", but they fixed it. Go will not touch your `go.mod` file unless you explicitly tell it to, which is a huge improvement in the consistency of the user experience.

- Go Modules requires version numbers to start with a "v", which annoys some people, because those people had been using git tags for years to track versions without a "v", so you could argue that's a trade-off too.

- There has been some debate about the way that major versions are implemented, since it requires you to change the name of the package for each major version increment. (By appending “/v2”, “/v3”, etc to the package name. The justification for this was to allow you to import multiple major versions of the same package into your dependency graph — and even the same file — without conflict.)

- The fact that the packages are still named after URLs is a source of consternation for some people, but it's only annoying in practice when you need to move the package from one organization to another or from one domain name to another. It's simply not an issue the rest of the time. Some people are also understandably confused into thinking that third party Go modules can vanish at any time because they're coming from URLs, but there is a transparent, immutable package proxy enabled by default that keeps copies of all[#] versions of all public dependencies that are fetched, so even if the original repo is deleted, the dependency will generally still continue to work indefinitely, and the lock sums will be retained indefinitely to prevent any malicious updates to existing dependency versions, which means that tampering is prevented both locally by your go.sum file and remotely by the proxy as an extra layer of protection for new projects that don't have a go.sum file yet. It is possible to disable this proxy (in whole or in part) or self-host your own if desired, but... I haven't encountered any use case that would dictate either. ([#]: There are a handful of rare exceptions involving packages without proper licenses which will only be cached for short periods of time plus the usual DMCA takedown notices that affect all "immutable" package registries, from what I understand.)

Beyond that... I don't know of any trade-offs. Seriously. I have taken the time to think through this and list what I could come up with above. A "trade-off" implies that some decision they made has known pros and cons. What are the cons? Maybe they could provide some "nicer" commands like `go mod update` to update all your dependencies instead of the somewhat obtuse `go get -u ./...` command? You have complained in several places about how Go combines indirect dependencies into the `go.mod` file, but... how is that not objectively better? Dependencies are dependencies. They all matter, and hiding some of them doesn't actually help anything except maybe aesthetics? I would love to know how making some of them exist exclusively in the lock file helps at all. Before Go Modules, I was always fine with that because I had never experienced anything better, but now I have.

There are plenty of things I would happily criticize about Go these days, but package management isn't one of them... it is simply stellar these days. It definitely did go through some growing pains, as any existing language adopting a new package manager would, and the drama that resulted from such a decision left a bitter taste in the mouth of some people, but I don't believe that bitterness was technical as much as it was a result of poor transparency from the core team with the community.

[0]: https://news.ycombinator.com/item?id=30870862

[1]: https://github.com/caddyserver/caddy/blob/master/go.mod

Re: How Go mitigates supply chain attacks

#154
post #70

Earlier quoted context omitted.

It’s very subtle, but there are some important differences. For example, lockfiles are not recursive in NPM: the NPM package (usually?) does not contain the lockfile and does not adhere to it when installed as a dependency. It will pick the newest version of dependencies that matches the spec in package.json. Go mod files are used recursively, and rather than try to pick the newest possible version, it will go with t…

This really depends on the specific package manager: if you're building an application in Rust, its lockfile will contain the full tree of dependencies, locked to a specific version.

Yes, this is true; I think the article is definitely making the distinction vs NPM and not Rust.

Re: How Go mitigates supply chain attacks

#155

Earlier quoted context omitted.

> No Go doesn't do it better, Go just doesn't give you the choice, from a convention perspective. You have a discipline issue with Java, it is not an issue in the language itself, it's with you and your team. Its my entire company, and its with the open source ecosystem. Its naive to think that language constructs dont influence dev's decisions. You need to make it easy to do the right thing, not hard, and go makes i…

> Its my entire company, and its with the open source ecosystem. Its naive to think that language constructs dont influence dev's decisions. You need to make it easy to do the right thing, not hard, and go makes it easy, which java fails at. Go makes it pretty easy to ignore errors, it's even worse than Java. An uncaught managed exception will not compile, and an uncaught unmanaged exception might terminate your prog…

> An uncaught managed exception will not compile, and an uncaught unmanaged exception might terminate your program.

While it's nice that uncaught exceptions stop the program from continuing with invalid state, it'd be strictly better to not have them in the first place. That's what languages with explicit error returns provide - can't have uncaught errors if you're forced to explicitly deal with every possible error. The resulting software is way more robust that way.

> _ := YieldsAnError()

This is a deliberate decision to ignore the error - the programmer needed to explicitly type this out. As such, it's not a problem at all. Even if it was, writing a linter to detect it would be trivial.

The real issue with Go's error handling is that you are allowed to implicitly ignore errors in certain cases. Most of them are caught by linters, but some edge cases remain and not compiling is better than needing a linter in the first place.

Re: How Go mitigates supply chain attacks

#156
post #126

Earlier quoted context omitted.

"but it also means that module publishers can pull versions (or the entire module) for arbitrary, selfish reasons, and then the community is left with a lot of difficulty" By default go get will download the source code into the pgk/mod folder. So if a module is pulled by the author, you can just use your copy of the source to fork it.

That's fine if you've already downloaded it, but doesn't help for people trying to pull it for the first time, either as a direct or transitive dependency.

that's what the proxy infrastructure is for: retracted versions won't be selected by default if you're adding it as a new direct dependency, but if you request a version directly (because you need it as an indirect dependency, you have a fresh machine, etc), it will always return the cached code.

Re: How Go mitigates supply chain attacks

#157
post #148

Earlier quoted context omitted.

That seems reasonable. I think I personally lean towards keeping them in separate files entirely because I like a clearer separation between human-authored content and machine-derived state.

but if you ever bump up the version of an indirect dependency (maybe to pick up a bugfix earlier), is this now a direct or indirect dependency?

Direct means that your code actually imports it directly instead of transitively. It has nothing to do with the version of that dependency being selected in the go.mod file.

Re: How Go mitigates supply chain attacks

#158
post #131

Earlier quoted context omitted.

How does go deal with the diamond dependency issue when transitively you've a dep on the same package via two different paths?

As far as I understand it... the import path that you use to import a package acts as its identity, and only one version of any given package will be installed. The way that it will determine this is by choosing the lowest version specified in any package that depends on a given package. Major versions of packages are required to have different import paths with Go modules, so when depending on two different major ve…

Okay but if I depend on A and A depends on C and has it pinned at 1.5, but I also depend on B and B has C pinned on 1.8, then I get 1.5? But what happens if C is on 1.8 because it doesn't work with 1.5 because an API it needs doesn't exist in 1.5?

Are we not talking about the transitively pinned dependencies in the "lock" section, or are we talking about logical constraints?

Logical constraints would make more sense, but if the constraints on C across various transitive deps are > 1.5 and > 1.8, and 1.9 or 1.10 exists, I probably want the last version of those.

Re: How Go mitigates supply chain attacks

#159

> There is no way for changes in the outside world—such as a new version of a dependency being published—to automatically affect a Go build. > Unlike most other package managers files, Go modules don’t have a separate list of constraints and a lock file pinning specific versions. The version of every dependency contributing to any Go build is fully determined by the go.mod file of the main module. I don't know if thi…

Npm lockfile will refer to same package because you can't republish npm with the same tag (there is also content hash in lockfile).

Referring to version tag in git from go's mod can't guarantee that because you can overwrite tag in git.

Am I wrong?

Re: How Go mitigates supply chain attacks

#160

> There is no way for changes in the outside world—such as a new version of a dependency being published—to automatically affect a Go build. > Unlike most other package managers files, Go modules don’t have a separate list of constraints and a lock file pinning specific versions. The version of every dependency contributing to any Go build is fully determined by the go.mod file of the main module. I don't know if thi…

Npm lockfile will refer to same package because you can't republish npm with the same tag (there is also content hash in lockfile). Referring to version tag in git from go's mod can't guarantee that because you can overwrite tag in git. Am I wrong?

Yes. The go.sum file that sits alongside go.mod keeps track of the hashes so that no modification like that can be made, and dependency fetches actually transparently go through a module proxy/mirror that keeps those same hashes as well, and it will prevent you from getting an altered version of a known module even if you’re starting a new project and don’t have a sum file yet. Versions can’t be republished.
Post reply on HN