Git er done.. securely.

Dan Fedick
7 min readMar 13, 2023
Built with Discord Midjourney Bot

So, you’re a developer.. and you’ve been writing code for years. When you set up your Git credentials for the first time, it was a safer world. You have a GitHub/GitLab Personal Access Token(PAT) or an SSH key setup in your organization. You’ve been using it to authenticate on the command line for a long time. Great! As long as you have those credentials, you are able to clone, fork, push, rebase and pull! You stopped thinking about that step in the Git process long ago!

Then you read some poor schlep got their PAT stolen. The nerve! Some bad-actor gained access to their computer through some kind of key logger, phishing program, trojan horse, etc. It could have been any number of entry points. They found their Personal Access Token and walked off with it.

The problem with the classic Personal Access Tokens and SSH Keys is they give you coarse grain access to everything in the organizations that your user has access to. So, depending on your role in the GitHub / GitLab organization(s) your token or ssh keys could give individuals with malicious intent, a buffet of access to many repositories!

That buffet could have your entire codebase and maybe some GitHub Actions? Not so tasty.

Built with Discord Midjourney Bot

Ok, so what’s the right posture for managing my Git credential workflow?

In a perfect world, you really want to have the following stance regarding credential access:

  • Dynamic creds are primo, static credentials are dumb.
  • Short-lived creds are muy bueno and long-lived creds are malo! If someone does gain access to your credential, it doesn’t do anyone any good for very long.
  • Authorize and authenticate access to credentials! Ie. Use a secure credential store.
  • Think least-privilege access to resources. If you are committing work to a repo, then your credentials should have enough to do basic development on a specific repo. No more, no less.
  • Try not to leave credentials in a file on disk. Your friendly bad actor wants your credentials in that directory.

The GitHub and GitLab CLIs.

A while ago, GitHub created the GitHub CLI to authenticate against GitHub and do all kinds of organizational updates including authenticating git commands.

“Oh, I love the new gh command, does it help with this?” No, not really, but there is hope for a brighter future with gh . I believe this workflow will be how we do most work in the future regarding GitHub authentication. I’m hopeful at least.

Right now if you authenticate with the GitHub CLI, (Ie. gh auth login ) a browser window is opened and you are forced to authenticate. If you have a YubiKey or some other form of 2fA setup, you have to use that. Great!

# gh auth login -p https -w
? You're already logged into Do you want to re-authenticate? Yes
? Authenticate Git with your GitHub credentials? No

! First copy your one-time code: AEDC-AB0C
Press Enter to open in your browser...
✓ Authentication complete.
- gh config set -h git_protocol https
✓ Configured git protocol
✓ Logged in as danfedick

Ok! My command line is authenticated. Doh. I have an OAuth token in a config credential file on my laptop. Ugh. This breaks the model I’m looking for. I don’t want bad actors snooping around on my laptop and finding the key in my ~/.config/gh/hosts.yml file. Hmmm.

# cat ~/.config/gh/hosts.yml
1 │
2 │ oauth_token: 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
3 │ git_protocol: https
4 │ user: myusername

Wait, what’s the expiration on this token? 🤷‍♂. No idea.

What’s the scope of this token? The minimum scope is access to “repo:read” level access to all repositories that my user has access to based on my user attributes. Eww.

What happens when I log out, is my token revoked? No. Gross

This sounds like a nightmare. I don’t want to be that poor schlep that gave up access to my tokens!

One good way to manage the gh command is to use the GitHub Shell Plugin for 1Password. In practice, I like this because you’ve at least stored your credential in a password manager and are adding a layer of authN/authZ to your workflow, but it still depends on the coarse-grained access PATs.

Even with all that being said, I think there is a future where the GitHub/GitLab CLIs could have a robust set of features around authentication.

Note to GitHub CLI devs.

Some ideas for CLI options. Please add to the gh/glab auth sub-command:

gh|glab auth login --repo <orgname>/reponame.git> --ttl 1h

gh|glab auth status that lists repositories, expiration ttls, and scope

gh|glab auth logout that expires/revokes all tokens

gh|glab auth --with-token <<< ${FINE_GRAINED_TOKEN} with the fine-grained PAT that was generated via the UI

gh|glab auth cache --osxkeychain|--lastpass|--vault|--1password|--gcmfw

So, don’t use GitHub or GitLab?

Of course, you obviously have to use some sort of VCS repo and you want to get all the benefits out of those tools. Let’s fall back to a best-case scenario with git authentication.

Good ole’ Git — I have standards, you know…

If I’m a human, doing human develop-y things ( A Person Entity, or “PE” ) I want to stick to my security standards:

  • Least-privilege credentials
  • Cached in a credential store and not on disk.
  • Short-Lived (as low as an hour)
  • Dynamic
  • AuthN/AuthZ to access credentials.

Last but not least….

  • Part of my workflow. This auth process should work on the command line and in my IDE and not be so cumbersome that I do everything I can to circumvent it.

Thanks GitHub!

If using GitHub, I can stick to some of these security standards by creating fine-grained access tokens. These tokens are in Beta right now but are pretty useful.

The Good: The main benefit is generating the token and specifying the attributes down to the specific repo with expiration dates.

GitHub has also added the ability to set the organization-level policy to approve/accept fine-grained PATs. See the announcement on GitHub’s blog for a more in-depth explanation.

Great! Where’s the API for this? I need to automate this process!

Not so fast! Not yet. I think/hope we’ll eventually get there.

The best credential solution today for PE interaction.

I believe the best solution as of today looks like the following:

  1. Generate a dynamic, short-lived fine-grained GitHub PAT: (The shortest token possible via the UI is 1 day.) This might get too cumbersome because it has to be done via the UI. I don’t know of a way to do this with REST API, yet. I can see moving this to a 7-day token given how we will manage that token.

2. Ensure this token only has least-privilege access to the repo you are working with. Ensure the token is scoped to specific attributes in that repo.

3. Place the token into a password manager / credential store. In my example, I will do this with 1Password.

4. Create a local shell function for quickly retrieving the PAT from 1Password on the command line.

5. Configure Git (global attribute) to cache credentials. 15 minutes is the default git cache expiration.

6. When pushing/pulling/fetching, etc.. use Username and dynamic fine-grained PAT associated with a specific repository pulled from 1Password.

HowTo — Let’s do this!

Step 1

Open the browser and create a Fine-Grained PAT:

Screenshot in opened web browser for Fine-grained tokens.

Step 2 — Setup repo-level attributes:

In my example, I’m adding the following attributes. This would obviously changed based on your needs:

Fine-Grained Repository attributes

Step 3 — Store credential

Place the token into a password manager / credential store. In my first example, I’ll show how to do this with 1Password. This is my preferred solution because there’s an authN/authZ step, the workflow is much simpler, and there’s never a file sitting on disk.

If you want to have quick access via the CLI, you can add the following functions to your .bashrc or your .zshrc to quickly add/update the credential in your 1Password vault.

Create a New Vault in 1password, specifically for storing GitHub credentials. I called mine gh. The name of each entry will correspond to the org/repo name.

### requires jq command
function set_token() {
ORIGIN=$(git remote get-url origin |tr "." " " |awk -F"/" '{print $4, $5}' |awk '{print $1,$2}')
ORG=$(echo $ORIGIN |awk '{print $1}')
REPO=$(echo $ORIGIN |awk '{print $2}')
EXIST=$(op item list --vault 'gh' --format=json | jq -re '.[] |select( .title == '\"$ORG/$REPO\"')'| echo $?)

echo "Enter the new token for ${ORG}/${REPO}: "
read token
echo "Enter the Expiration date YYYY-MM-DD: "
read expiry

if [[ $EXIST == 0 ]]
op item edit "${ORG}/${REPO}" \
--vault="gh" \
"credential=${token}" \
op item create \
--category="API Credential" \
--title="${ORG}/${REPO}" \
--vault="gh" \
"type=Bearer Token" \
"credential=${token}" \

The following function should get the credential and copy into buffer for use when interacting with Git.

### Get Token for Specific Repo
function get_token() {
ORIGIN=$(git remote get-url origin |tr "." " " |awk -F"/" '{print $4, $5}' |awk '{print $1,$2}')
ORG=$(echo $ORIGIN |awk '{print $1}')
REPO=$(echo $ORIGIN |awk '{print $2}')
op item get --vault "gh" $ORG/$REPO --fields label=credential |pbcopy