Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support group unique ssh keys for githubrepo hook #2480

Merged
10 commits merged into from
May 21, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- only runs SSH proxy commands if the ssh_proxy configuration item has been defined (@jameskirsop)
- updated vrp.rb to correctly parse huawei devices
- asa: information about the configuration change time is deleted
- Extended `remote_repo` configuration to allow repo specific ssh keys (@mjbnz)
- sonicos: added scrubbing for hashed values (@televat0rs)
- nxos: Additional scrubbing for nxos device passwords (@derekivey)
- nxos: Fix password match to avoid stripping out the user role. (@derekivey)
Expand Down
26 changes: 24 additions & 2 deletions docs/Hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Several authentication methods are supported:

* Provide a `password` for username + password authentication
* Provide both a `publickey` and a `privatekey` for ssh key-based authentication
* Provide only a `privatekey` (public key filename is assumed to be `privatekey` + "`.pub`"
* Don't provide any credentials for ssh-agent authentication

The username will be set to the relevant part of the `remote_repo` URI, with a fallback to `git`. It is also possible to provide one by setting the `username` configuration key.
Expand All @@ -83,13 +84,15 @@ For ssh key-based authentication, it is possible to set the environment variable
* `remote_repo`: the remote repository to be pushed to.
* `username`: username for repository auth.
* `password`: password for repository auth.
* `publickey`: public key file path for repository auth.
* `publickey`: public key file path for repository auth. (optional)
* `privatekey`: private key file path for repository auth.
* NOTE: this key needs to be in the legacy PEM format, not the newer OpenSSL format [#1877](https://github.com/ytti/oxidized/issues/1877), [#2324](https://github.com/ytti/oxidized/issues/2324)
* To convert a key beginning with `BEGIN OPENSSH PRIVATE KEY` to the legacy PEM format, run this command:
`ssh-keygen -p -m PEM -f $MY_KEY_HERE`

When using groups, each group must have a unique entry in the `remote_repo` config.
When using groups, `remote_repo` must be a dictionary of groups that the hook should apply to. If a group is missing from the dictionary, no action will be taken.

The dictionary entry can either be a url alone:

```yaml
hooks:
Expand All @@ -100,6 +103,25 @@ hooks:
firewalls: git@git.intranet:oxidized/firewalls.git
```

... or it can be a dictionary with `url` and `privatekey` specified:

```yaml
hooks:
push_to_remote:
remote_repo:
routers:
url: git@git.intranet:oxidized/routers.git
privatekey: /root/.ssh/id_rsa_routers
switches:
url: git@git.intranet:oxidized/switches.git
privatekey: /root/.ssh/id_rsa_switches
firewalls:
url: git@git.intranet:oxidized/firewalls.git
privatekey: /root/.ssh/id_rsa_firewalls
```

Both forms can be mixed and matched.

### githubrepo hook configuration example

Authenticate with a username and a password without groups in use:
Expand Down
55 changes: 44 additions & 11 deletions lib/oxidized/hook/githubrepo.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,32 @@ def validate_cfg!
end

def run_hook(ctx)
repo = Rugged::Repository.new(ctx.node.repo)
repo = Rugged::Repository.new(ctx.node.repo)
creds = credentials(ctx.node)
url = remote_repo(ctx.node)

if url.nil? || url.empty?
log "No repository defined for #{ctx.node.group}/#{ctx.node.name}", :debug
return
end

log "Pushing local repository(#{repo.path})..."
remote = repo.remotes['origin'] || repo.remotes.create('origin', remote_repo(ctx.node))
log "to remote: #{remote.url}"
log "to remote: #{url}"

if repo.remotes['origin'].nil?
repo.remotes.create('origin', url)
elsif repo.remotes['origin'].url != url
repo.remotes.set_url('origin', url)
end
remote = repo.remotes['origin']

fetch_and_merge_remote(repo)
fetch_and_merge_remote(repo, creds)

remote.push([repo.head.name], credentials: credentials)
remote.push([repo.head.name], credentials: creds)
end

def fetch_and_merge_remote(repo)
result = repo.fetch('origin', [repo.head.name], credentials: credentials)
def fetch_and_merge_remote(repo, creds)
result = repo.fetch('origin', [repo.head.name], credentials: creds)
log result.inspect, :debug

unless result[:total_deltas].positive?
Expand Down Expand Up @@ -43,27 +57,46 @@ def fetch_and_merge_remote(repo)

private

def credentials
def credentials(node)
Proc.new do |_url, username_from_url, _allowed_types| # rubocop:disable Style/Proc
git_user = cfg.has_key?('username') ? cfg.username : (username_from_url || 'git')
if cfg.has_key?('password')
log "Authenticating using username and password as '#{git_user}'", :debug
Rugged::Credentials::UserPassword.new(username: git_user, password: cfg.password)
elsif cfg.has_key?('publickey') && cfg.has_key?('privatekey')
elsif cfg.has_key?('privatekey')
pubkey = cfg.has_key?('publickey') ? cfg.publickey : nil;
log "Authenticating using ssh keys as '#{git_user}'", :debug
Rugged::Credentials::SshKey.new(username: git_user, publickey: File.expand_path(cfg.publickey), privatekey: File.expand_path(cfg.privatekey), passphrase: ENV["OXIDIZED_SSH_PASSPHRASE"])
rugged_sshkey(git_user: git_user, privkey: cfg.privatekey, pubkey: pubkey)
elsif cfg.has_key?('remote_repo') && cfg.remote_repo.has_key?(node.group) && cfg.remote_repo[node.group].has_key?('privatekey')
pubkey = cfg.remote_repo[node.group].has_key?('publickey') ? cfg.remote_repo[node.group].publickey : nil;
log "Authenticating using ssh keys as '#{git_user}' for '#{node.group}/#{node.name}'", :debug
rugged_sshkey(git_user: git_user, privkey: cfg.remote_repo[node.group].privatekey, pubkey: pubkey)
else
log "Authenticating using ssh agent as '#{git_user}'", :debug
Rugged::Credentials::SshKeyFromAgent.new(username: git_user)
end
end
end

def rugged_sshkey(args = {})
git_user = args[:git_user]
privkey = args[:privkey]
pubkey = args[:pubkey] || privkey + '.pub'
Rugged::Credentials::SshKey.new(username: git_user,
publickey: File.expand_path(pubkey),
privatekey: File.expand_path(privkey),
passphrase: ENV["OXIDIZED_SSH_PASSPHRASE"])
end

def remote_repo(node)
if node.group.nil? || cfg.remote_repo.is_a?(String)
cfg.remote_repo
else
elsif cfg.remote_repo[node.group].is_a?(String)
cfg.remote_repo[node.group]
elsif cfg.remote_repo[node.group].url.is_a?(String)
cfg.remote_repo[node.group].url
else
nil
end
end
end