58

I am trying to deploy my app to Heroku however I rely on using some private git repos as modules. I do this for code reuse between projects, e.g. I have a custom logger I use in multiple apps.

"logger":"git+ssh://git@bitbucket.org..............#master"

The problem is Heroku obviously does not have ssh access to this code. I can't find anything on this problem. Ideally Heroku have a public key I can can just add to the modules.

henry.oswald
  • 5,304
  • 13
  • 51
  • 73
  • 1
    The modules should be installed in node_modules directory? You could just archive application and then install it on heroku after sending it to heroku? – Alfred Jun 03 '12 at 15:49
  • I don't fully understand, but I think you are saying I could just store the code inside the node_modules folder and main repo which would work but its a bit of a hack. – henry.oswald Jun 05 '12 at 15:01
  • When you do npm install on your local PC that's is standard behaviour since npm 1.0? – Alfred Jun 05 '12 at 23:36
  • Crossed wires. I was hoping to not ad any other processes in between the heroky deployment, starts to defeat the purpose. – henry.oswald Jun 17 '12 at 23:00
  • 3
    I'd like to know the answer as well. You can tie your Github/Bitbucket SSH key to your Heroku account: `heroku keys:add ~/.ssh/id_rsa.pub`. Theoretically this should do the trick, but `git push heroku master` still results in "Host key verification failed.". You asked this back in June, have you found the answer since? – lefnire Nov 09 '12 at 19:49

10 Answers10

76

Basic auth

GitHub has support for basic auth:

"dependencies" : {
    "my-module" : "git+https://my_username:my_password@github.com/my_github_account/my_repo.git"
}

As does BitBucket:

"dependencies" : {
    "my-module": "git+https://my_username:my_password@bitbucket.org/my_bitbucket_account/my_repo.git"
}

But having plain passwords in your package.json is probably not desired.

Personal access tokens (GitHub)

To make this answer more up-to-date, I would now suggest using a personal access token on GitHub instead of username/password combo.

You should now use:

"dependencies" : {
    "my-module" : "git+https://<username>:<token>@github.com/my_github_account/my_repo.git"
}

For Github you can generate a new token here:

https://github.com/settings/tokens

App passwords (Bitbucket)

App passwords are primarily intended as a way to provide compatibility with apps that don't support two-factor authentication, and you can use them for this purpose as well. First, create an app password, then specify your dependency like this:

"dependencies" : {
    "my-module": "git+https://<username>:<app-password>@bitbucket.org/my_bitbucket_account/my_repo.git"
}

[Deprecated] API key for teams (Bitbucket)

For BitBucket you can generate an API Key on the Manage Team page and then use this URL:

"dependencies" : {
    "my-module" : "git+https://<teamname>:<api-key>@bitbucket.org/team_name/repo_name.git"
}
Karl Adler
  • 15,780
  • 10
  • 70
  • 88
Koen.
  • 25,449
  • 7
  • 83
  • 78
  • That causes package.json parse error, since format spec is {module_name: location_or_semver}, rather than just {location_or_semver}: "Installing dependencies with npm npm ERR! Couldn't read dependencies. npm ERR! Failed to parse json npm ERR! Unexpected token }" – lefnire Nov 14 '12 at 16:23
  • Got it: "dependencies" : { "my-module" : "git+https://my_username:my_password@bitbucket.org/my_bitbucket_account/my_repo.git" } (note SO is parsing this url, see https://gist.github.com/4073148) – lefnire Nov 14 '12 at 16:29
  • Cool, didn't know about the `git+` prefix, only tested the basic auth by simply pulling the repo with such an URL. – Koen. Nov 14 '12 at 16:54
  • 8
    With the authorizations API you can do this more securely by issuing yourself an OAuth token and using that instead of your account's username and password: https://help.github.com/articles/git-over-https-using-oauth-token – Rafael May 07 '13 at 01:41
  • 4
    Just a quick update to this, I was trying to use the API approach as mentioned for bitbucket, BUT the syntax shown is incorrect. You need to do `git+https://:@bitbucket.org//.git` – Grofit Jul 25 '15 at 13:12
  • @Grofit any docs on this url format? Because the only thing I've found is: https://confluence.atlassian.com/display/BITBUCKET/OAuth+on+Bitbucket#OAuthonBitbucket-Cloningarepositorywithanaccesstoken – Koen. Jul 27 '15 at 12:39
  • @Koen Couldnt find anything on it, this was literally the only place I found any mention of using bitbucket in this way, and after trying the answer listed and having no luck I messed around a bit and got it working with the solution in the comment. – Grofit Jul 27 '15 at 14:53
  • Found the correct syntax for bitbucket integration with api key, the one posted by @Grofit wasn't working anymore, use this one: `git+https://:x-oauth-basic@bitbucket.org//.git` source: [link](https://bitbucket.org/site/master/issues/7735/allow-http-access-to-repositories-via) – MQ87 Sep 16 '15 at 14:11
  • @MQ87 the link you supply as source ultimately refers to https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-238027431.html#OAuthonBitbucket-Cloningarepositorywithanaccesstoken, which I posted before, and mentions `x-token-auth:{access_token}@` not `{access_token}:x-token-auth@`. They explicitly name the difference with GitHub: _Our process is similar to GitHub, yet slightly different: the difference is GitHub puts the actual token in the username field._ – Koen. Sep 16 '15 at 15:55
  • @Koen. I think that you need to use that version when you have an oauth token for the OAuth consumer, and to generate that kind of token you have to follow all the steps in your source. In this case we are using a "team token", and that is the only way to make it work. No other documentation is provided anywhere – MQ87 Sep 16 '15 at 16:03
  • As of this date, for BitBucket the method described by @Grofit worked for us (teamname:apikey) – Mirage Jun 15 '16 at 15:11
  • 1
    I've updated the answer to reference newer Bitbucket techniques, the comments before this one are referencing an older version of this answer. – Flimm Nov 18 '16 at 10:13
  • Is there any way to do this with deploy keys instead of user tokens? – Atav32 Feb 20 '18 at 20:39
  • 2
    Would you mind please adding a line on how to use .env vars in the dependencies section of your package.json? – Brandon Keith Biggs May 12 '21 at 01:57
45

Update 2016-03-26

The method described no longer works if you are using npm3, since npm3 fetches all modules described in package.json before running the preinstall script. This has been confirmed as a bug.

The official node.js Heroku buildpack now includes heroku-prebuild and heroku-postbuild, which will be run before and after npm install respectively. You should use these scripts instead of preinstall and postinstall in all cases, to support both npm2 and npm3.

In other words, your package.json should resemble:

 "scripts": {
      "heroku-prebuild": "bash preinstall.sh",
      "heroku-postbuild": "bash postinstall.sh"
    }

I've come up with an alternative to Michael's answer, retaining the (IMO) favourable requirement of keeping your credentials out of source control, whilst not requiring a custom buildpack. This was borne out of frustration that the buildpack linked by Michael is rather out of date.

The solution is to setup and tear down the SSH environment in npm's preinstall and postinstall scripts, instead of in the buildpack.

Follow these instructions:

  • Create two scripts in your repo, let's call them preinstall.sh and postinstall.sh.
  • Make them executable (chmod +x *.sh).
  • Add the following to preinstall.sh:
    #!/bin/bash
    # Generates an SSH config file for connections if a config var exists.

    if [ "$GIT_SSH_KEY" != "" ]; then
      echo "Detected SSH key for git. Adding SSH config" >&1
      echo "" >&1

      # Ensure we have an ssh folder
      if [ ! -d ~/.ssh ]; then
        mkdir -p ~/.ssh
        chmod 700 ~/.ssh
      fi

      # Load the private key into a file.
      echo $GIT_SSH_KEY | base64 --decode > ~/.ssh/deploy_key

      # Change the permissions on the file to
      # be read-only for this user.
      chmod 400 ~/.ssh/deploy_key

      # Setup the ssh config file.
      echo -e "Host github.com\n"\
              " IdentityFile ~/.ssh/deploy_key\n"\
              " IdentitiesOnly yes\n"\
              " UserKnownHostsFile=/dev/null\n"\
              " StrictHostKeyChecking no"\
              > ~/.ssh/config
    fi
  • Add the following to postinstall.sh:
    #!/bin/bash

    if [ "$GIT_SSH_KEY" != "" ]; then
      echo "Cleaning up SSH config" >&1
      echo "" >&1

      # Now that npm has finished running, we shouldn't need the ssh key/config anymore.
      # Remove the files that we created.
      rm -f ~/.ssh/config
      rm -f ~/.ssh/deploy_key

      # Clear that sensitive key data from the environment
      export GIT_SSH_KEY=0
    fi
  • Add the following to your package.json:

    "scripts": {
      "preinstall": "bash preinstall.sh",
      "postinstall": "bash postinstall.sh"
    }
    
  • Generate a private/public key pair using ssh-agent.

  • Add the public key as a deploy key on Github.
  • Create a base64 encoded version of your private key, and set it as the Heroku config var GIT_SSH_KEY.
  • Commit and push your app to Github.

When Heroku builds your app, before npm installs your dependencies, the preinstall.sh script is run. This creates a private key file from the decoded contents of the GIT_SSH_KEY environment variable, and creates an SSH config file to tell SSH to use this file when connecting to github.com. (If you are connecting to Bitbucket instead, then update the Host entry in preinstall.sh to bitbucket.org). npm then installs the modules using this SSH config. After installation, the private key is removed and the config is wiped.

This allows Heroku to pull down your private modules via SSH, while keeping the private key out of the codebase. If your private key becomes compromised, since it is just one half of a deploy key, you can revoke the public key in GitHub and regenerate the keypair.

As an aside, since GitHub deploy keys have read/write permissions, if you are hosting the module in a GitHub organization, you can instead create a read-only team and assign a 'deploy' user to it. The deploy user can then be configured with the public half of the keypair. This adds an extra layer of security to your module.

Tom Spencer
  • 7,816
  • 4
  • 54
  • 50
  • For me `rm -f ~/.ssh/config` in the `cleanup-ssh.script` is a problem. As i am already using this for some other settings. So i can't delete that file. Good to add a notion here. – Samar Panda Jun 03 '15 at 13:18
  • Yes, that's a good point. I guess you could make a copy of your original `config` file, and restore it afterwards, instead of just removing it. – Tom Spencer Jun 05 '15 at 07:37
  • Nice work! I never have liked having to maintain a fork of the buildpack in order to do this – Michael Lang Jul 08 '15 at 18:04
  • This looks good - I'm trying it out with your buildpack PR. A few comments: 1) I'd call the scripts `preinstall.sh` and `postinstall.sh` so its clear they are bash scripts and when they are called. 2) What is the purpose of base64 encoding? Just so it doesn't have hard returns? – JBCP Feb 29 '16 at 17:18
  • Thanks, have changed the names as suggested. Base64 is to remove the line feeds, that's correct. – Tom Spencer Mar 01 '16 at 11:16
  • I have some private bitbucket repo dependencies in my probject and it worked before but fails to install now. I've used the buildpack that you referenced (with `heroku buildpacks:set https://github.com/heroku/heroku-buildpack-nodejs#v83 -a my-app`), but the error when pushing to heroku is not gone. Did I miss something or do something wrong to use the buildpack? – KwiZ Mar 26 '16 at 04:31
  • The latest Heroku buildpack now has the scripts `heroku-prebuild` and `heroku-postbuild`. You should replace `preinstall` with `heroku-prebuild` and `postinstall` with `heroku-postbuild` to achieve what you want. Amongst other things, this will support npm3, which [runs the scripts in a different order to npm2](https://github.com/npm/npm/issues/10379). – Tom Spencer Mar 26 '16 at 13:32
  • Can you elaborate on how you created the base64 encoded version of your private key? I've used an online tool, but I still get the error `Permission denied (publickey).` which makes me think the value I entered in Heroku settings is incorrect. The preinstall script is running, as I can see in the logs. – Peter Feb 13 '17 at 19:15
  • Found it. I used `cat /path/to/private/key | base64 > output.txt`. Then remove any `\n` (newlines) and pasted that in my Heroku settings. – Peter Feb 14 '17 at 08:25
  • What if we need multiple SSH keys? Does the name of the key matter at all? – Pier Jun 13 '17 at 17:40
  • I was able to get this working only if private key doesn't have a passphrase... Is this expected? – roboli Jan 22 '18 at 17:22
  • 1
    @roboli yes this technique will only work without a passphrase since it is unattended, I'd advocate creating a separate SSH key for just this operation, which would make it easy to revoke if compromised at a later date. – Tom Spencer Jan 25 '18 at 10:19
  • 1
    @Pier the name doesn't matter at all, you could easily use `GIT_SSH_01`, `GIT_SSH_02` etc - just remember to modify the script accordingly to use the correct environment variables. – Tom Spencer Jan 25 '18 at 10:20
  • Feels a bit hacky, but I would upvote this 1000 time if I could as it is the _only_ solution here that doesn't hardcode your credential in your code. (other similar below btw) – marco6 Apr 24 '18 at 19:43
  • I ran into the same problem as @Peter, and his solution to remove newlines helped. Could someone tell me why this step is so necessary? – ack_inc May 07 '20 at 19:36
  • @ack_inc I guess because the newline is regarded as any other character, which makes the key different than the same string of characters without the newline. – Peter May 20 '20 at 12:20
  • This had been working for a long time for me, until suddenly I got an SSH related error in the "Pruning devDependencies" step (`npm prune` calls `git ls-remote`, which fails because the SSH key has already been removed). Fixed it by using the `heroku-cleanup` hook instead of `heroku-postbuild` (which seems more appropriate anyway, according to the [docs](https://devcenter.heroku.com/articles/nodejs-support#heroku-specific-build-steps)). – d0gb3r7 Sep 11 '20 at 16:43
  • I realized that the `export GIT_SSH_KEY=0` line only overwrites the variable _within the running postbuild/cleanup script_. To clear it for the actual (potentially exposed) dyno processes, I prefixed the startup commands in my Procfile with `env -u GIT_SSH_KEY `. – d0gb3r7 Oct 27 '20 at 13:11
17

It's a REALLY bad idea to have plain text passwords in your git repo, using an access token is better, but you will still want to be super careful.

"my_module": "git+https://ACCESS_TOKEN:x-oauth-basic@github.com/me/my_module.git"
Dom Barker
  • 1,889
  • 1
  • 16
  • 29
14

I created a custom nodeJS buildpack that will allow you to specify an SSH key that is registered with ssh-agent and used by npm when dynos are first setup. It seamlessly allows you to specify your module as an ssh url in your package.json like shown:

"private_module": "git+ssh://git@github.com:me/my_module.git"

To setup your app to use your private key:

  • Generate a key: ssh-keygen -t rsa -C "your_email@example.com" (Enter no passphrase. The buildpack does not support keys with passphrases)
  • Add the public key to github: pbcopy < ~/.ssh/id_rsa.pub (in OS X) and paste the results into the github admin
  • Add the private key to your heroku app's config: cat id_rsa | base64 | pbcopy, then heroku config:set GIT_SSH_KEY=<paste_here> --app your-app-name
  • Setup your app to use the buildpack as described in the heroku nodeJS buildpack README included in the project. In summary the simplest way is to set a special config value with heroku config:set to the github url of the repository containing the desired buildpack. I'd recommend forking my version and linking to your own github fork, as I'm not promising to not change my buildpack.

My custom buildpack can be found here: https://github.com/thirdiron/heroku-buildpack-nodejs and it works for my system. Comments and pull requests are more than welcome.

Michael Lang
  • 2,157
  • 3
  • 22
  • 29
6

Based on the answer from @fiznool I created a buildpack to solve this problem using a custom ssh key stored as an environment variable. As the buildpack is technology agnostic, it can be used to download dependencies using any tool like composer for php, bundler for ruby, npm for javascript, etc: https://github.com/simon0191/custom-ssh-key-buildpack

  1. Add the buildpack to your app:

    $ heroku buildpacks:add --index 1 https://github.com/simon0191/custom-ssh-key-buildpack
    
  2. Generate a new SSH key without passphrase (lets say you named it deploy_key)

  3. Add the public key to your private repository account. For example:

  4. Encode the private key as a base64 string and add it as the CUSTOM_SSH_KEY environment variable of the heroku app.

  5. Make a comma separated list of the hosts for which the ssh key should be used and add it as the CUSTOM_SSH_KEY_HOSTS environment variable of the heroku app.

    # MacOS
    $ heroku config:set CUSTOM_SSH_KEY=$(base64 --input ~/.ssh/deploy_key) CUSTOM_SSH_KEY_HOSTS=bitbucket.org,github.com
    # Ubuntu
    $ heroku config:set CUSTOM_SSH_KEY=$(base64 ~/.ssh/deploy_key) CUSTOM_SSH_KEY_HOSTS=bitbucket.org,github.com
    
  6. Deploy your app and enjoy :)
Simon Soriano
  • 803
  • 12
  • 19
  • 1
    The input part of the script should be `--input ~/.ssh/deploy_key`. You have decoded base64 of private key in your script. Please correct. – gmuraleekrishna Dec 20 '17 at 19:12
  • @gmuraleekrishna I found that using `--input` flag depends on the version of base64 that you are using. For example in macOS it was needed but in ubuntu 14.04 the flag wasn't recognized. – Simon Soriano Dec 21 '17 at 12:37
  • @SimonSoriano I meant you should be using private key instead of public key. – gmuraleekrishna Dec 22 '17 at 10:00
  • @gmuraleekrishna thanks for pointing that out, you're right. I corrected the answer and the README of the repo. – Simon Soriano Jan 04 '18 at 12:10
  • I was able to get this working only if private key doesn't have a passphrase... Is this expected? – roboli Jan 22 '18 at 17:23
  • 1
    @roboli, yes, passphrases in ssh keys are not commonly used for automation because in order to decrypt the private key you would need to manually enter the passphrase which won't work in this case because the buildpack scripts are automatically run by Heroku, or you would need to provide also the passphrase to the automation tool (in this case the buildpack) and will therefore defeat the purpose of the passphrase which is to add an additional layer of security. Thanks for the feedback, I updated the answer to clarify that the SSH key must be generated without a passphrase. – Simon Soriano Jan 23 '18 at 20:59
  • I am getting a `Couldn't find that app` error if I execute `heroku buildpacks:add --index 1 https://github.com/simon0191/custom-ssh-key-buildpack`. Any one experiencing this too? – Karl-John Chow Jan 10 '19 at 01:50
  • @Karl-JohnChow that errors seems more related to the app name rather than the buildpack. What's the entire command you're using? Did you change the name of the heroku app? – Simon Soriano Jan 10 '19 at 13:55
  • @SimonSoriano I didnt change anything, I just fired off that particular command (copy pasted) but it did not seemed to work – Karl-John Chow Jan 10 '19 at 22:24
3

I was able to setup resolving of Github private repositories in Heroku build via Personal access tokens.

  • Generate Github access token here: https://github.com/settings/tokens
  • Set access token as Heroku config var: heroku config:set GITHUB_TOKEN=<paste_here> --app your-app-name or via Heroku Dashboard
  • Add heroku-prebuild.sh script:

    #!/bin/bash
    if [ "$GITHUB_TOKEN" != "" ]; then
        echo "Detected GITHUB_TOKEN. Setting git config to use the security token" >&1
        git config --global url."https://${GITHUB_TOKEN}@github.com/".insteadOf git@github.com:
    fi
    
  • add the prebuild script to package.json:

    "scripts": {
        "heroku-prebuild": "bash heroku-prebuild.sh"
    }
    

For local environment we can also use git config ... or we can add the access token to ~/.netrc file:

machine github.com
  login PASTE_GITHUB_USERNAME_HERE
  password PASTE_GITHUB_TOKEN_HERE

and installing private github repos should work.

npm install OWNER/REPO --save will appear in package.json as: "REPO": "github:OWNER/REPO"

and resolving private repos in Heroku build should also work. optionally you can setup a postbuild script to unset the GITHUB_TOKEN.

Michal Moravcik
  • 2,250
  • 20
  • 18
2

This answer is good https://stackoverflow.com/a/29677091/6135922, but I changed a little bit preinstall script. Hope this will help someone.

#!/bin/bash
# Generates an SSH config file for connections if a config var exists.

echo "Preinstall"

if [ "$GIT_SSH_KEY" != "" ]; then
  echo "Detected SSH key for git. Adding SSH config" >&1
  echo "" >&1

  # Ensure we have an ssh folder
  if [ ! -d ~/.ssh ]; then
    mkdir -p ~/.ssh
    chmod 700 ~/.ssh
  fi

  # Load the private key into a file.
  echo $GIT_SSH_KEY | base64 --decode > ~/.ssh/deploy_key

  # Change the permissions on the file to
  # be read-only for this user.
  chmod o-w ~/
  chmod 700 ~/.ssh
  chmod 600 ~/.ssh/deploy_key

  # Setup the ssh config file.
  echo -e "Host bitbucket.org\n"\
          " IdentityFile ~/.ssh/deploy_key\n"\
          " HostName bitbucket.org\n" \
          " IdentitiesOnly yes\n"\
          " UserKnownHostsFile=/dev/null\n"\
          " StrictHostKeyChecking no"\
          > ~/.ssh/config

  echo "eval `ssh-agent -s`"
  eval `ssh-agent -s`

  echo "ssh-add -l"
  ssh-add -l

  echo "ssh-add ~/.ssh/deploy_key"
  ssh-add ~/.ssh/deploy_key

  # uncomment to check that everything works just fine
  # ssh -v git@bitbucket.org
fi
Community
  • 1
  • 1
-2

You can use in package.json private repository with authentication example below:

https://usernamegit:passwordgit@github.com/reponame/web/tarball/branchname
-4

In short it is not possible. The best solution to this problem I came up with is to use the new git subtree's. At the time of writing they are not in the official git source and so needs to be installed manual but they will be included in v1.7.11. At the moment it is available on homebrew and apt-get. it is then a case of doing

git subtree add -P /node_modules/someprivatemodue git@github.......someprivatemodule {master|tag|commit}

this bulks out the repo size but an update is easy by doing the command above with gitsubtree pull.

henry.oswald
  • 5,304
  • 13
  • 51
  • 73
-6

I have done this before with modules from github. Npm currently accepts the name of the package or a link to a tar.gz file which contains the package.

For example if you want to use express.js directly from Github (grab the link via the download section) you could do:

"dependencies" : {
  "express"   :  "https://github.com/visionmedia/express/tarball/2.5.9"
}

So you need to find a way to access you repository as a tar.gz file via http(s).

TheHippo
  • 61,720
  • 15
  • 75
  • 100
  • thanks, but this would not get around the repo being private to the world problem. – henry.oswald Jun 05 '12 at 15:00
  • As said, either npm or tar.gz, probably there is some way to export your module as an archive. If not, there is always the possibility to use git submodules for that. – TheHippo Jun 06 '12 at 02:13
  • but because the module is private and requires ssh access as soon as heroku tries to get it they will be denied regardless of how the code is transmitted. – henry.oswald Jun 06 '12 at 08:22
  • Use a cronjob, post commit hook or whatever to dump a tag.gz somewhere it is accessible. (basic http auth could protect it from everybody else.) As said before, as far as I know, there is no magic way to solve you problem with writing something easy into you package.json. You need to be a little bit more creative here. – TheHippo Jun 06 '12 at 10:40
  • @TheHippo dude just remove this. – Iman Mohamadi Sep 25 '18 at 06:26