Compare commits

...

496 Commits

Author SHA1 Message Date
dependabot[bot]
4d1515ad61 chore(deps): bump actions/checkout from 2 to 3.1.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.1.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3.1.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-07 04:07:54 +00:00
semgrep.dev on behalf of @moul
73926212d5 Add Semgrep CI 2022-05-18 18:23:41 +00:00
Manfred Touron
773b7d5a8b Merge pull request #333 from moul/dependabot/go_modules/golang.org/x/tools-0.1.10 2022-03-22 23:06:12 +01:00
Manfred Touron
3ee75e47dd Merge pull request #334 from moul/dependabot/docker/golang-1.18.0 2022-03-22 23:06:05 +01:00
dependabot[bot]
bb6e7c46cc chore(deps): bump golang from 1.17.6 to 1.18.0
Bumps golang from 1.17.6 to 1.18.0.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-17 04:06:08 +00:00
dependabot[bot]
111ced03ad chore(deps): bump golang.org/x/tools from 0.1.9 to 0.1.10
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.1.9 to 0.1.10.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.1.9...v0.1.10)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-16 04:06:02 +00:00
Manfred Touron
46970b6d17 Merge pull request #318 from moul/renovate/all 2022-02-08 12:27:47 +01:00
Renovate Bot
afc3888afe fix(deps): update golang.org/x/crypto commit hash to 20e1d8d 2022-02-08 05:38:20 +00:00
Manfred Touron
7ecdb808df Merge pull request #317 from moul/renovate/all
fix(deps): update golang.org/x/crypto commit hash to 30dcbda
2022-02-06 21:07:55 +01:00
Renovate Bot
d3d45da163 fix(deps): update golang.org/x/crypto commit hash to 30dcbda 2022-02-02 20:50:26 +00:00
Manfred Touron
2287353585 Merge pull request #315 from moul/renovate/all
fix(deps): update golang.org/x/crypto commit hash to 198e437
2022-01-30 20:37:31 +01:00
Manfred Touron
ee5a89413e Merge pull request #316 from moul/dependabot/go_modules/golang.org/x/tools-0.1.9
chore(deps): bump golang.org/x/tools from 0.1.8 to 0.1.9
2022-01-29 21:08:51 +01:00
Renovate Bot
9b30972e1e fix(deps): update all 2022-01-28 21:08:08 +00:00
dependabot[bot]
9b849441fa chore(deps): bump golang.org/x/tools from 0.1.8 to 0.1.9
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.1.8 to 0.1.9.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.1.8...v0.1.9)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-27 04:04:25 +00:00
Manfred Touron
0a6ee0f985 Merge pull request #310 from moul/renovate/all 2022-01-24 11:14:17 +01:00
Manfred Touron
271f10d389 Merge pull request #313 from moul/dependabot/go_modules/gorm.io/driver/mysql-1.2.3 2022-01-20 16:28:04 +01:00
Manfred Touron
ec0a59a72b Merge pull request #312 from moul/dependabot/go_modules/github.com/docker/docker-20.10.12incompatible 2022-01-20 16:27:55 +01:00
Manfred Touron
e7ca57196e Merge pull request #311 from moul/dependabot/docker/golang-1.17.6 2022-01-20 16:27:50 +01:00
Manfred Touron
d05a6cd3bf Merge pull request #309 from Gurkengewuerz/dev/failed-auth-log 2022-01-20 16:27:24 +01:00
dependabot[bot]
68a4bf2bd3 chore(deps): bump gorm.io/driver/mysql from 1.2.2 to 1.2.3
Bumps [gorm.io/driver/mysql](https://github.com/go-gorm/mysql) from 1.2.2 to 1.2.3.
- [Release notes](https://github.com/go-gorm/mysql/releases)
- [Commits](https://github.com/go-gorm/mysql/compare/v1.2.2...v1.2.3)

---
updated-dependencies:
- dependency-name: gorm.io/driver/mysql
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-15 08:01:36 +00:00
Manfred Touron
3f6c5f1860 Merge pull request #314 from moul/dependabot/go_modules/gorm.io/gorm-1.22.5
chore(deps): bump gorm.io/gorm from 1.22.4 to 1.22.5
2022-01-15 09:00:49 +01:00
dependabot[bot]
4a2648f6be chore(deps): bump gorm.io/gorm from 1.22.4 to 1.22.5
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.22.4 to 1.22.5.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.22.4...v1.22.5)

---
updated-dependencies:
- dependency-name: gorm.io/gorm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-13 04:04:26 +00:00
Renovate Bot
3610fbeb04 chore(deps): update all docker tags 2022-01-12 19:14:52 +00:00
dependabot[bot]
d0ab97ebf7 chore(deps): bump github.com/docker/docker
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 20.10.9+incompatible to 20.10.12+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Changelog](https://github.com/moby/moby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/docker/docker/compare/v20.10.9...v20.10.12)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-11 04:05:03 +00:00
dependabot[bot]
0e8da37c80 chore(deps): bump golang from 1.17.5 to 1.17.6
Bumps golang from 1.17.5 to 1.17.6.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-07 04:05:13 +00:00
Gurkengewuerz
2d7b79703a feat: added auth failed logging 2022-01-02 23:56:10 +01:00
Manfred Touron
d95f1bd145 Merge pull request #305 from moul/renovate/all 2021-12-29 23:33:42 +01:00
Renovate Bot
f8b0224c44 chore(deps): update all 2021-12-24 07:59:57 +00:00
Manfred Touron
1b97531c6e Merge pull request #308 from moul/dependabot/docker/golang-1.17.5
chore(deps): bump golang from 1.17.3 to 1.17.5
2021-12-12 14:52:40 +01:00
dependabot[bot]
4f38f0f507 chore(deps): bump golang from 1.17.3 to 1.17.5
Bumps golang from 1.17.3 to 1.17.5.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-10 04:05:10 +00:00
Manfred Touron
ef0083ed71 Merge pull request #304 from moul/renovate/all
fix(deps): update all
2021-11-21 08:36:55 +01:00
Renovate Bot
2215b5e1a7 fix(deps): update all 2021-11-17 20:25:01 +00:00
Manfred Touron
f72618618b Merge pull request #299 from moul/renovate/all 2021-11-14 08:35:48 +01:00
Renovate Bot
9f5b05461b chore(deps): update all docker tags 2021-11-13 09:49:43 +00:00
b0undl3ss
4c7fcec0e3 docs: add testing info to readme (#301)
* refactor: Upgrade to Gorm 2.0

* Clean up dependencies

* Fix linting errors

Remove gorm v1 delete callback

* Resolve unittest failures

* Fix association query

* Add testing section to readme

Co-authored-by: Jordan Craven <jordan.craven@wearepop.com>
2021-11-04 20:25:17 +01:00
b0undl3ss
6068e6e48e refactor: upgrade to Gorm 2.0 (#300)
Co-authored-by: Jordan Craven <jordan.craven@wearepop.com>
2021-11-02 08:31:49 +01:00
Manfred Touron
01e4dcb686 Merge pull request #282 from moul/renovate/all 2021-10-15 11:32:06 +02:00
Renovate Bot
b140f70abd chore(deps): update all docker tags 2021-10-15 09:27:54 +00:00
Manfred Touron
5fc247810b Merge pull request #296 from moul/dependabot/docker/golang-1.17.2 2021-10-15 11:27:00 +02:00
Manfred Touron
9205d8faca Merge pull request #284 from moul/dependabot/go_modules/github.com/gliderlabs/ssh-0.3.3 2021-10-15 11:26:51 +02:00
Manfred Touron
71bb63d122 Merge pull request #283 from moul/dependabot/go_modules/golang.org/x/tools-0.1.4 2021-10-15 11:26:41 +02:00
dependabot[bot]
daf4af9225 chore(deps): bump golang from 1.16.5 to 1.17.2
Bumps golang from 1.16.5 to 1.17.2.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-08 04:04:45 +00:00
dependabot[bot]
f366e52d9e chore(deps): bump github.com/gliderlabs/ssh from 0.3.2 to 0.3.3
Bumps [github.com/gliderlabs/ssh](https://github.com/gliderlabs/ssh) from 0.3.2 to 0.3.3.
- [Release notes](https://github.com/gliderlabs/ssh/releases)
- [Commits](https://github.com/gliderlabs/ssh/compare/v0.3.2...v0.3.3)

---
updated-dependencies:
- dependency-name: github.com/gliderlabs/ssh
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-29 04:07:42 +00:00
dependabot[bot]
f1137fd16b chore(deps): bump golang.org/x/tools from 0.1.3 to 0.1.4
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.1.3...v0.1.4)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-24 04:07:41 +00:00
Manfred Touron
65de503b2e Merge pull request #280 from moul/renovate/all
fix(deps): update module golang.org/x/tools to v0.1.3
2021-06-11 06:08:25 +02:00
Renovate Bot
728cb237a0 fix(deps): update module golang.org/x/tools to v0.1.3 2021-06-10 18:36:37 +00:00
Manfred Touron
3c701b2405 Merge pull request #271 from moul/renovate/all
chore(deps): update all docker tags
2021-06-05 16:20:23 +02:00
Renovate Bot
173839c7c1 chore(deps): update all docker tags 2021-06-05 01:03:25 +00:00
Manfred Touron
caf80be30c Merge pull request #277 from moul/dependabot/github_actions/actions/cache-2.1.6
chore(deps): bump actions/cache from 2.1.5 to 2.1.6
2021-05-29 01:52:52 +02:00
Manfred Touron
c57cb0073a Merge pull request #273 from moul/dependabot/github_actions/codfish/semantic-release-action-1.9.0 2021-05-29 01:52:36 +02:00
dependabot[bot]
a496f3ce36 chore(deps): bump actions/cache from 2.1.5 to 2.1.6
Bumps [actions/cache](https://github.com/actions/cache) from 2.1.5 to 2.1.6.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v2.1.5...v2.1.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-28 04:03:32 +00:00
dependabot[bot]
e83216a34f chore(deps): bump codfish/semantic-release-action from 1 to 1.9.0
Bumps [codfish/semantic-release-action](https://github.com/codfish/semantic-release-action) from 1 to 1.9.0.
- [Release notes](https://github.com/codfish/semantic-release-action/releases)
- [Commits](https://github.com/codfish/semantic-release-action/compare/v1...v1.9.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-12 04:03:20 +00:00
Manfred Touron
b0ec9c99f9 Merge pull request #272 from moul/dependabot/docker/golang-1.16.4
chore(deps): bump golang from 1.16.3 to 1.16.4
2021-05-07 08:18:19 +02:00
dependabot[bot]
cfaf215713 chore(deps): bump golang from 1.16.3 to 1.16.4
Bumps golang from 1.16.3 to 1.16.4.

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-07 04:04:32 +00:00
Manfred Touron
debb5bf179 Merge pull request #266 from moul/dependabot/docker/golang-1.16.3
chore(deps): bump golang from 1.16.2 to 1.16.3
2021-05-01 09:32:27 +02:00
dependabot[bot]
d5bf550e96 chore(deps): bump golang from 1.16.2 to 1.16.3
Bumps golang from 1.16.2 to 1.16.3.

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-27 08:37:08 +00:00
Manfred Touron
7c68c861bd Merge pull request #267 from moul/dependabot/github_actions/golangci/golangci-lint-action-v2.5.2
chore(deps): bump golangci/golangci-lint-action from v2.5.1 to v2.5.2
2021-04-27 10:11:38 +02:00
Manfred Touron
bae5ad94a9 Merge pull request #227 from moul/renovate/all
fix(deps): update all
2021-04-27 10:03:47 +02:00
Renovate Bot
5369a4f966 fix(deps): update all 2021-04-25 16:18:08 +00:00
Manfred Touron
1c98ef283e Merge pull request #270 from moul/dev/moul/maintenance
chore: repo maintenance 🤖
2021-04-25 18:15:51 +02:00
moul-bot
3dc2801c60 chore: repo maintenance 🤖
more details: https://github.com/moul/repoman

Signed-off-by: moul-bot <bot@moul.io>
Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com>
2021-04-25 16:08:45 +00:00
dependabot[bot]
04913f1f4c chore(deps): bump golangci/golangci-lint-action from v2.5.1 to v2.5.2
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from v2.5.1 to v2.5.2.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v2.5.1...5c56cd6c9dc07901af25baab6f2b0d9f3b7c3018)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-25 09:53:46 +00:00
Manfred Touron
0415f116ea Merge pull request #212 from GreyOBox/dev/GreyOBox/acls-cmd-hook 2021-04-25 11:50:28 +02:00
Manfred Touron
68ce353c5d Merge pull request #269 from moul/dev/moul/maintenance 2021-04-25 11:47:36 +02:00
Manfred Touron
f7ed3a66f2 fix: email address validator 2021-04-24 12:54:42 +00:00
moul-bot
4e9c5205c7 chore: repo maintenance 🤖
more details: https://github.com/moul/repoman

Signed-off-by: moul-bot <bot@moul.io>
2021-04-24 12:35:39 +00:00
Manfred Touron
63b4aa5533 Merge pull request #268 from moul/dependabot/github_actions/actions/cache-v2.1.5
chore(deps): bump actions/cache from v2.1.4 to v2.1.5
2021-04-21 09:09:52 +02:00
Sergey Yashchuk
d580b14d62 Merge branch 'master' into dev/GreyOBox/acls-cmd-hook 2021-04-20 18:42:36 +07:00
Sergey Yashchuk
669577de47 Merge pull request #1 from moul/master
Update from upstream master
2021-04-20 18:28:12 +07:00
dependabot[bot]
868be6af11 chore(deps): bump actions/cache from v2.1.4 to v2.1.5
Bumps [actions/cache](https://github.com/actions/cache) from v2.1.4 to v2.1.5.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v2.1.4...1a9e2138d905efd099035b49d8b7a3888c653ca8)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-13 04:13:10 +00:00
Sergey Yashchuk
97bf5d3168 lint fix 2021-04-02 10:49:18 +07:00
Sergey Yashchuk
32fcfa370c Fixes related to comments in PR 2021-04-02 10:29:46 +07:00
Manfred Touron
a710e50b1e Merge pull request #239 from moul/dependabot/github_actions/actions/cache-v2.1.4
chore(deps): bump actions/cache from v2.1.3 to v2.1.4
2021-03-31 13:50:53 +02:00
dependabot[bot]
55010dcc09 chore(deps): bump actions/cache from v2.1.3 to v2.1.4
Bumps [actions/cache](https://github.com/actions/cache) from v2.1.3 to v2.1.4.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v2.1.3...26968a09c0ea4f3e233fdddbafd1166051a095f6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-31 10:56:17 +00:00
Manfred Touron
9413b75dc8 Merge pull request #263 from moul/dev/moul/bump-ci-go
chore: bump CI's go version
2021-03-31 12:55:17 +02:00
Manfred Touron
2648418463 chore: bump CI's go version 2021-03-31 10:52:14 +00:00
Manfred Touron
79cbaa3afe Merge pull request #262 from moul/fix/email-validator
feat: New Email validator
2021-03-28 22:46:58 +02:00
Darko Djalevski
2def328f6a fix: fix email validating in shell input
fix: test cases

fix feedback

fix: validate email with custom validator in shell input
2021-03-28 22:25:25 +02:00
Manfred Touron
ab9c53f1b0 chore: maintenance (#260) 2021-03-26 17:26:10 +01:00
Manfred Touron
614418e7be Merge pull request #243 from matteyeux/master
Fix typo in "shell commands" section in README.md
2021-03-26 16:29:29 +01:00
Manfred Touron
a5bade8761 Merge pull request #249 from jwessel/fix_host_inspect
fix: host inspect causes db errors with later operations
2021-03-26 16:28:44 +01:00
Manfred Touron
7404704bfe Merge pull request #254 from jwessel/feat_userkey_create
feat: Allow user multiple keys with userkey create
2021-03-26 16:27:43 +01:00
Manfred Touron
84a0a31eda Merge pull request #253 from jwessel/feat_postgres
feat: Add postgres support
2021-03-26 16:26:05 +01:00
Manfred Touron
40bbea590c Merge pull request #248 from jwessel/master
feat: Allow removal by user for 'userkey rm'
2021-03-26 16:18:27 +01:00
Manfred Touron
e455d50db9 Merge pull request #251 from jwessel/feat_user_udpate
feat: Allow for update or removal of the invite token
2021-03-26 16:11:28 +01:00
Manfred Touron
be3f215e24 Merge pull request #256 from moul/renovate/docker-all
chore(deps): update all docker tags to v1.16.2
2021-03-26 11:46:56 +01:00
Renovate Bot
c290253546 chore(deps): update all docker tags to v1.16.2 2021-03-12 02:24:27 +00:00
Jason Wessel
28a5fd1846 feat: Allow user multiple keys with userkey create
And end user may have more than one ssh key, the userkey create
command should be able to accept more than one key so you can do
something like:

   curl https://github.com/USER.keys | ssh sshportal -p 2222 -l admin userkey create USER

The userkey create command also does not work properly from an
interactive shell due to the use of bufio.  This patch adds the
ability to use either the interactive shell or direct ssh command to
input one or more keys.

Signed-off-by: Jason Wessel <jason.wessel@windriver.com>
2021-03-09 09:29:10 -06:00
Jason Wessel
19605f0054 feat: Add postgres support
Postgres is more picky about submitting a string to the id column in a
table.  Postgres requires the use of only integers for the array of
values in a select statement containing: where id IN (...array...)

This patch fixes all the following class of problems:

   SELECT * FROM "ssh_keys" WHERE
   "ssh_keys"."deleted_at" IS NULL AND ((id IN ('host')) OR (name IN
   ('host'))) ORDER BY "ssh_keys"."id" ASC LIMIT 1 [0 rows affected or
   returned ] error: pq: invalid input syntax for
   type integer: "host"

Signed-off-by: Jason Wessel <jason.wessel@windriver.com>
2021-03-09 08:57:08 -06:00
Jason Wessel
5b4332072c feat: Allow for update or removal of the invite token
If the invite leaks for the admin user it is possible for the admin
user to be compromised by another invite request.  It needs to be
possible to entirely remove the invite capability for any given user.

New arguments added to user update:

   --invite_token value, -i value            Updates the invite token
   --remove_invite, -R                       Remove invite token

Signed-off-by: Jason Wessel <jason.wessel@windriver.com>
2021-03-09 08:51:45 -06:00
Jason Wessel
c1c4c556b4 feat: Allow removal by user for 'userkey rm'
The userkey rm command implies that it can remove a key by user or the
id key, but it only works against the data base id of the key.  This
patch allows the userkey rm command to work with the user name, so
that all the keys for the user can be cleared out in one command.

Signed-off-by: Jason Wessel <jason.wessel@windriver.com>
2021-03-08 11:22:38 -06:00
Jason Wessel
3c32177213 fix: host inspect causes db errors with later operations
The most simple case with a fresh install of sshportal using the
following commands put the shell into a unrecoverable state.

config> host create test1@test1
1
config> host inspect 1
config> host create test2@test2
error: can't preload field Groups for dbmodels.SSHKey

The issue is caused because the global db handle is replaced with the
inspect command.

Signed-off-by: Jason Wessel <jason.wessel@windriver.com>
2021-03-01 07:37:15 -08:00
Manfred Touron
762736d622 Merge pull request #216 from jle64/ecdsa 2021-02-28 07:46:06 +01:00
Manfred Touron
bbbc484fe8 Merge pull request #247 from moul/dependabot/github_actions/golangci/golangci-lint-action-v2.5.1
chore(deps): bump golangci/golangci-lint-action from v2.3.0 to v2.5.1
2021-02-28 07:44:57 +01:00
Manfred Touron
e1602364c8 Merge pull request #235 from moul/dependabot/go_modules/github.com/gliderlabs/ssh-0.3.2
chore(deps): bump github.com/gliderlabs/ssh from 0.3.1 to 0.3.2
2021-02-28 07:44:52 +01:00
Manfred Touron
2540d1e861 Merge pull request #241 from moul/dependabot/go_modules/github.com/olekukonko/tablewriter-0.0.5
chore(deps): bump github.com/olekukonko/tablewriter from 0.0.4 to 0.0.5
2021-02-28 07:44:48 +01:00
Manfred Touron
177a198420 Merge pull request #244 from moul/renovate/docker-all
chore(deps): update all docker tags to v1.16.0
2021-02-28 07:44:44 +01:00
Manfred Touron
51612aab13 Merge pull request #245 from moul/dependabot/docker/golang-1.16.0
chore(deps): bump golang from 1.15.8 to 1.16.0
2021-02-28 07:44:38 +01:00
dependabot[bot]
e20af1dde5 chore(deps): bump golangci/golangci-lint-action from v2.3.0 to v2.5.1
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from v2.3.0 to v2.5.1.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v2.3.0...d9f0e73c0497685d68af8c58280f49fcaf0545ff)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-25 04:18:52 +00:00
Renovate Bot
6caa1f1657 chore(deps): update all docker tags to v1.16.0 2021-02-18 03:57:04 +00:00
dependabot[bot]
e0f76d15ec chore(deps): bump golang from 1.15.8 to 1.16.0
Bumps golang from 1.15.8 to 1.16.0.

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-17 04:17:32 +00:00
matteyeux
05225a4b25 Fix typo in "shell commands" section in README.md 2021-02-15 11:27:12 +01:00
dependabot[bot]
bcc150727f chore(deps): bump github.com/olekukonko/tablewriter from 0.0.4 to 0.0.5
Bumps [github.com/olekukonko/tablewriter](https://github.com/olekukonko/tablewriter) from 0.0.4 to 0.0.5.
- [Release notes](https://github.com/olekukonko/tablewriter/releases)
- [Commits](https://github.com/olekukonko/tablewriter/compare/v0.0.4...v0.0.5)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-11 04:21:57 +00:00
Manfred Touron
9062417d13 Merge pull request #237 from moul/renovate/docker-all
chore(deps): update all docker tags to v1.15.8
2021-02-07 12:12:52 +01:00
Manfred Touron
baeade4043 Merge pull request #238 from moul/dependabot/docker/golang-1.15.8
chore(deps): bump golang from 1.15.7 to 1.15.8
2021-02-07 12:12:41 +01:00
Renovate Bot
b9552e98b5 chore(deps): update all docker tags to v1.15.8 2021-02-06 02:14:21 +00:00
dependabot[bot]
715ccde829 chore(deps): bump golang from 1.15.7 to 1.15.8
Bumps golang from 1.15.7 to 1.15.8.

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-05 04:16:20 +00:00
dependabot[bot]
f5dc1bd1b9 chore(deps): bump github.com/gliderlabs/ssh from 0.3.1 to 0.3.2
Bumps [github.com/gliderlabs/ssh](https://github.com/gliderlabs/ssh) from 0.3.1 to 0.3.2.
- [Release notes](https://github.com/gliderlabs/ssh/releases)
- [Commits](https://github.com/gliderlabs/ssh/compare/v0.3.1...v0.3.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-02 04:22:33 +00:00
Jonathan Lestrelin
c79c50aeb6 Remove go versions with missing requirements for ecdsa/ed2519 from CI. 2021-01-25 18:21:44 +01:00
Manfred Touron
df3542c6ee Merge pull request #233 from moul/dependabot/docker/golang-1.15.7
chore(deps): bump golang from 1.15.6 to 1.15.7
2021-01-25 14:22:48 +01:00
Manfred Touron
e40f5307a3 Merge pull request #232 from moul/renovate/docker-all
chore(deps): update all docker tags to v1.15.7
2021-01-25 14:22:04 +01:00
Renovate Bot
6e6045306b chore(deps): update all docker tags to v1.15.7 2021-01-21 00:14:55 +00:00
dependabot[bot]
874467b1e6 chore(deps): bump golang from 1.15.6 to 1.15.7
Bumps golang from 1.15.6 to 1.15.7.

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-20 04:03:40 +00:00
Jonathan Lestrelin
5c1c559a9a Merge remote-tracking branch 'upstream/master' into ecdsa 2021-01-12 08:18:28 +01:00
Manfred Touron
6872c727ef Merge pull request #231 from moul/dev/moul/maintenance
chore: repo maintenance 🤖
2021-01-02 10:51:40 +01:00
moul-bot
cae996d041 chore: repo maintenance 🤖
more details: https://github.com/moul/repoman

Signed-off-by: moul-bot <bot@moul.io>
2021-01-01 15:24:08 +01:00
Manfred Touron
a23b77282c Merge pull request #229 from moul/renovate/docker-all
chore(deps): update circleci/golang docker tag to v1.15.6
2020-12-27 12:12:16 +01:00
Manfred Touron
24814c4152 Merge pull request #230 from moul/dependabot/docker/golang-1.15.6
chore(deps): bump golang from 1.15.5 to 1.15.6
2020-12-26 19:01:22 +01:00
Renovate Bot
07359988d0 chore(deps): update all docker tags to v1.15.6 2020-12-05 00:51:28 +00:00
dependabot[bot]
db6eb63297 chore(deps): bump golang from 1.15.5 to 1.15.6
Bumps golang from 1.15.5 to 1.15.6.

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-04 04:22:41 +00:00
Manfred Touron
5fdb31b97d Merge pull request #221 from moul/dependabot/github_actions/golangci/golangci-lint-action-v2.3.0
chore(deps): bump golangci/golangci-lint-action from v0.1.7 to v2.3.0
2020-11-15 21:48:47 +01:00
Manfred Touron
bce6b1998b Merge pull request #220 from moul/dependabot/github_actions/actions/cache-v2.1.3
chore(deps): bump actions/cache from v1 to v2.1.3
2020-11-15 21:48:34 +01:00
dependabot[bot]
f7fa60da97 chore(deps): bump golangci/golangci-lint-action from v0.1.7 to v2.3.0
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from v0.1.7 to v2.3.0.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v0.1.7...e868220d9fd3b523f1a8fcfb69749e8c7521ba14)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-15 20:40:22 +00:00
Manfred Touron
d2cd6b64a3 Merge pull request #215 from moul/renovate/all
chore(deps): update all
2020-11-15 21:39:39 +01:00
Manfred Touron
1ef0cc8725 Merge pull request #225 from moul/renovate/docker-all
chore(deps): update all docker tags to v1.15.5
2020-11-15 21:38:37 +01:00
Renovate Bot
d894005c3f chore(deps): update all docker tags to v1.15.5 2020-11-13 01:41:26 +00:00
Renovate Bot
af7206d114 chore(deps): update all 2020-11-12 16:47:33 +00:00
dependabot[bot]
1f9d962cd6 chore(deps): bump actions/cache from v1 to v2.1.3
Bumps [actions/cache](https://github.com/actions/cache) from v1 to v2.1.3.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v1...0781355a23dac32fd3bac414512f4b903437991a)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-11 01:04:31 +00:00
Manfred Touron
460041c6e3 Merge pull request #219 from moul/dev/moul/maintenance
chore: repo maintenance 🤖
2020-11-11 02:04:08 +01:00
moul-bot
7068565ab1 chore: repo maintenance 🤖
more details: https://github.com/moul/repoman

Signed-off-by: moul-bot <bot@moul.io>
2020-11-09 22:53:21 +01:00
Manfred Touron
74bd885c1d Merge pull request #218 from moul/renovate/docker-all
chore(deps): update all docker tags to v1.15.4
2020-11-07 16:54:04 +01:00
Renovate Bot
9317f206d1 chore(deps): update all docker tags to v1.15.4 2020-11-07 00:30:51 +00:00
Jonathan Lestrelin
6c3f803dc6 Add generation of ecdsa and ed25519 keys.
Make RSA keys use value from --length parameter.
Set default length when --length is unspecified based on key type.
Change default key format to ed25519 both in shell and for keys created
at initialization.
2020-10-10 04:21:11 +02:00
Renovate Bot
9c3d29eb83 chore(deps): update module gliderlabs/ssh to v0.3.1 2020-10-07 19:19:15 +00:00
Manfred Touron
e339a73931 Merge pull request #214 from moul/dev/moul/bump-deps4
chore: bump deps
2020-10-04 10:51:32 +02:00
Manfred Touron
0dcab1b380 chore: bump deps 2020-10-04 10:18:07 +02:00
Sergey Yashchuk
c697c9aaeb dev: ACLs external command hook 2020-09-18 01:24:51 +07:00
Manfred Touron
032f802348 Merge pull request #208 from moul/renovate/docker-all
chore(deps): update all docker tags to v1.15.2
2020-09-14 20:57:45 +02:00
Renovate Bot
7fd9be9058 chore(deps): update all docker tags to v1.15.2 2020-09-10 00:31:00 +00:00
Manfred Touron
83b54aeeff Merge pull request #205 from moul/dev/moul/go115
chore: go1.15
2020-08-19 20:28:55 +02:00
Manfred Touron
2323d6fd1e chore: go1.15 2020-08-19 19:33:18 +02:00
Manfred Touron
4c947ce391 Merge pull request #204 from GreyOBox/increase-size-of-name-fields
fix: increase size of name fields
2020-08-19 18:27:33 +02:00
Sergey Yashchuk
44559f0547 fix: increase size of name fields 2020-08-19 18:23:36 +02:00
Manfred Touron
8234119cd4 Merge pull request #197 from moul/renovate/all
chore(deps): update golang.org/x/crypto commit hash to 123391f
2020-08-10 18:22:45 +02:00
Manfred Touron
7a75c13ac4 Merge pull request #200 from moul/renovate/docker-all
chore(deps): update all docker tags to v1.14.7
2020-08-10 18:22:15 +02:00
Manfred Touron
4b10131790 Merge pull request #201 from moul/imgbot
[ImgBot] Optimize images
2020-08-09 17:14:22 +02:00
Manfred Touron
a29c6e8338 chore: add intro image 2020-08-09 00:30:36 +02:00
ImgBotApp
198e0717b5 [ImgBot] Optimize images
*Total -- 887.71kb -> 587.48kb (33.82%)

/.assets/bastion.jpg -- 503.44kb -> 249.40kb (50.46%)
/.assets/flow-diagram.png -- 104.11kb -> 79.45kb (23.69%)
/.assets/overview.png -- 32.65kb -> 26.50kb (18.82%)
/.assets/cluster-mysql.svg -- 8.50kb -> 7.08kb (16.74%)
/.assets/overview.svg -- 9.23kb -> 8.03kb (13.03%)
/.assets/flow-diagram.svg -- 13.85kb -> 12.39kb (10.51%)
/.assets/sql-schema.svg -- 36.89kb -> 33.99kb (7.85%)
/.assets/demo.gif -- 179.03kb -> 170.63kb (4.69%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
2020-08-08 22:28:50 +00:00
Manfred Touron
d8fa2f6925 Add files via upload 2020-08-09 00:28:34 +02:00
Renovate Bot
16c8c0092e chore(deps): update all docker tags to v1.14.7 2020-08-08 00:28:27 +00:00
Renovate Bot
b0dfff2d90 chore(deps): update golang.org/x/crypto commit hash to 123391f 2020-07-28 20:38:36 +00:00
Manfred Touron
9d2badf253 Merge pull request #196 from moul/dev/moul/pr-194 2020-07-23 18:55:42 +02:00
Konstantin Bakaras
428344da17 feat: MySQL, Postgres support
Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com>
2020-07-23 18:50:42 +02:00
Konstantin Bakaras
0c07ac790a feat: ACL Check with inception and expiration
Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com>
2020-07-23 18:50:35 +02:00
Konstantin Bakaras
365a37959a chore: Model and edit
Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com>
2020-07-23 18:50:35 +02:00
Manfred Touron
90fd6057cf Merge pull request #193 from moul/renovate/docker-all
chore(deps): update all docker tags to v1.14.6
2020-07-23 18:24:07 +02:00
Manfred Touron
4220f3fb89 Merge pull request #190 from moul/renovate/all
chore(deps): update all
2020-07-23 17:22:27 +02:00
Renovate Bot
3e2acfc992 chore(deps): update all 2020-07-20 06:19:00 +00:00
Renovate Bot
9c464b2610 chore(deps): update all docker tags to v1.14.6 2020-07-18 00:55:40 +00:00
Manfred Touron
5760aece65 Merge pull request #192 from moul/dev/moul/maintenance
chore: repo maintenance 🤖
2020-07-12 14:08:51 +02:00
moul-bot
a24e20252a chore: repo maintenance 🤖
more details: https://github.com/moul/repoman

Signed-off-by: moul-bot <bot@moul.io>
2020-07-12 14:04:09 +02:00
Manfred Touron
37a7fa1917 Merge pull request #189 from moul/renovate/all
chore(deps): update all
2020-07-09 17:47:46 +02:00
Renovate Bot
f1b28b0363 chore(deps): update all 2020-07-08 08:14:45 +00:00
Manfred Touron
e43bb55e70 Merge pull request #188 from moul/dev/moul/fix-166 2020-07-08 10:12:52 +02:00
Manfred Touron
763ced7524 feat: host logging modes (disabled, commands, everything)
Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com>
2020-07-04 22:16:46 +02:00
Manfred Touron
54128beb12 chore: point CHANGELOG.md to releases page
Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com>
2020-07-04 21:53:26 +02:00
Manfred Touron
64ba179cc7 chore: add .gitattributes
Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com>
2020-07-04 21:53:26 +02:00
Manfred Touron
bbdb4851a5 Merge pull request #187 from moul/dev/moul/maintenance
chore: repo maintenance 🤖
2020-07-04 01:59:48 +02:00
moul-bot
63719ec00e chore: repo maintenance 🤖
more details: https://github.com/moul/repoman

Signed-off-by: moul-bot <bot@moul.io>
2020-07-02 00:41:50 +02:00
Manfred Touron
0722497336 Update README.md 2020-07-01 14:51:57 +02:00
Manfred Touron
e74f7221b5 Merge pull request #168 from moul/dev/moul/linters
fix: add more linters
2020-07-01 14:51:03 +02:00
Manfred Touron
f4fc3a90bc fix: add more linters
Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com>
2020-07-01 14:46:23 +02:00
Manfred Touron
df3aa6e165 Merge pull request #181 from moul/renovate/all
chore(deps): update all
2020-07-01 14:46:01 +02:00
Manfred Touron
986bcd7971 Merge pull request #185 from moul/dev/moul/windows 2020-07-01 14:17:50 +02:00
Manfred Touron
7f3ea431a1 Merge pull request #183 from jrrdev/exit_fix 2020-07-01 14:16:14 +02:00
Manfred Touron
dae0252857 chore: small fix for build on windows 2020-07-01 14:10:50 +02:00
Manfred Touron
33b8e5272c Merge pull request #184 from moul/dev/moul/renovate-tidy
chore: tell renovate to run go mod tidy
2020-07-01 14:10:15 +02:00
Manfred Touron
21e73757ac chore: tell renovate to run go mod tidy 2020-07-01 14:01:14 +02:00
jerard@alfa-safety.fr
bcb5d3b7ef fixup! Fix early closure of data stream. 2020-06-30 18:28:47 +02:00
jerard@alfa-safety.fr
d2f3f460b2 Fix early closure of data stream.
Closes moul/sshportal#55 and closes moul/sshportal#127
2020-06-30 10:28:39 +02:00
Renovate Bot
e06fe6f5a3 chore(deps): update all 2020-06-22 22:25:28 +00:00
Manfred Touron
fb9dabfe6b chore: bump deps 2020-06-09 12:11:41 +02:00
Manfred Touron
0e0cd8fed5 Merge pull request #180 from moul/dev/moul/update-project-layout
fix: update project layout
2020-06-09 12:10:17 +02:00
Manfred Touron
8959e1782f fix: update project layout
Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com>
2020-06-09 11:45:45 +02:00
Manfred Touron
33151105e0 Merge pull request #173 from moul/renovate/all
chore(deps): update all
2020-06-09 11:07:15 +02:00
Manfred Touron
77b40eb9ed Merge pull request #177 from NocFlame/master
Update README.md; Info about missing host keys
2020-06-09 11:07:01 +02:00
Manfred Touron
075dfd0aa7 Merge pull request #176 from Zatte/update_readme_shell_conditions
Update readme; special host names
2020-06-09 11:06:38 +02:00
Manfred Touron
5cf6b1c218 Merge pull request #174 from moul/renovate/docker-all
chore(deps): update all docker tags to v1.14.4
2020-06-06 18:21:26 +02:00
Renovate Bot
6527746a91 chore(deps): update all 2020-06-04 21:46:38 +00:00
Renovate Bot
020ca9c6b3 chore(deps): update all docker tags to v1.14.4 2020-06-03 00:16:26 +00:00
NocFlame
8c7831480b Update README.md
*What this PR does / why we need it:*
Current readme does not inform user of how to fix the behaviour of password promt when linking admin account

*Which issue this PR fixes: fixes #128*
Updated the readme to inform the user of a quick fix (ssh-keygen -t rsa) when association fails and user is asked for password
2020-05-28 11:12:28 +02:00
Mikael Rapp
e399dfd8e4 Update readme; special host names 2020-05-27 23:30:28 +02:00
Manfred Touron
be83c7148d Merge pull request #172 from moul/dev/moul/bump-deps4 2020-05-12 03:35:22 +02:00
Manfred Touron
ce187e8675 fix: bump deps 2020-05-12 03:30:27 +02:00
Manfred Touron
f13ede4ba7 Update README.md 2020-05-12 03:27:15 +02:00
Manfred Touron
fb061ed419 Merge pull request #138 from moul/renovate/all
Update all
2020-05-10 15:45:44 +02:00
Renovate Bot
b4a377f269 Update all 2020-05-02 08:14:28 +00:00
Manfred Touron
de6f37aa64 Merge pull request #170 from moul/renovate/docker-all
Update all Docker tags to v1.14.2
2020-04-10 10:11:05 +02:00
Renovate Bot
32219577b8 Update all Docker tags to v1.14.2 2020-04-10 00:21:11 +00:00
Manfred Touron
abc7329a71 Merge pull request #167 from moul/renovate/docker-all
Update all Docker tags to v1.14.1
2020-03-22 14:58:30 +01:00
Renovate Bot
675942e967 Update all Docker tags to v1.14.1 2020-03-21 00:28:54 +00:00
Manfred Touron
5b20cd501e Update README.md
See https://github.com/moul/sshportal/issues/164
2020-03-10 11:01:26 +01:00
Manfred Touron
b6aaf4d7cf Update README.md 2020-03-05 15:33:52 +01:00
Manfred Touron
972e232559 Merge pull request #143 from moul/renovate/docker-all
chore(deps): update all docker tags to v1.14.0
2020-03-01 17:17:37 +01:00
Manfred Touron
851a91b1a0 fix: fix deps 2020-02-28 22:12:46 +01:00
Renovate Bot
6a068dc430 chore(deps): update all docker tags to v1.14.0 2020-02-28 22:05:37 +01:00
Manfred Touron
2cdfcf60fe Merge pull request #163 from MitaliBo/master
Security fix for github.com/moby/moby
2020-02-28 22:04:24 +01:00
Manfred Touron
5d9e0c367a feat: use secure rand seed 2020-02-25 11:49:38 +01:00
MitaliBo
cbf8263033 Update go.mod 2020-02-24 14:27:29 -08:00
Manfred Touron
846c73d9bc Merge pull request #160 from moul/dev/moul/bump-deps
chore: bump-deps
2020-02-22 20:51:29 +01:00
Manfred Touron
e0b43b1976 Merge pull request #162 from moul/dev/moul/semantic-release
feat: set up semantic release
2020-02-22 20:48:51 +01:00
Manfred Touron
6a6e788968 feat: set up semantic release 2020-02-22 20:43:54 +01:00
Manfred Touron
4754cad42a chore: bump-deps 2020-02-18 00:12:56 +01:00
Manfred Touron
db58e53f3b Merge pull request #156 from jeanlouisferey/master
Little correction
2020-02-18 00:06:27 +01:00
Manfred Touron
b31acb4348 Merge pull request #148 from jle64/fix_shell_connection_log
Fix db user id type in shell connection log.
2020-02-18 00:06:20 +01:00
Manfred Touron
c794c2c076 chore: add funding + update go.sum 2020-02-18 00:05:35 +01:00
Jonathan Lestrelin
42d6cd44bb Fix db user id type in shell connection log. 2020-02-17 23:55:35 +01:00
Manfred Touron
f9057ca56a Merge pull request #157 from bozzo/master
Add Helm chart to deploy SSHPortal on Kubernetes
2020-02-17 23:54:39 +01:00
bozzo
c2f1999037 Set theme jekyll-theme-slate 2020-02-17 09:08:51 +01:00
bozzo
44b386f7a7 Add Helm chart to deploy SSHPortal on Kubernetes
Support HA with Mysql database
Support autoscaling
2020-02-08 23:06:19 +01:00
Jean-Louis Férey
89b296db4e Little correction 2020-01-29 16:35:39 +01:00
Manfred Touron
c16403fb3f Merge pull request #146 from opencollective/opencollective
Activating Open Collective
2019-09-18 09:11:08 +02:00
Jess
5e21fb72e6 Added financial contributors to the README 2019-09-17 18:02:58 -07:00
Manfred Touron
c5681bf880 chore: post-release version bump 2019-06-24 11:43:32 +02:00
Manfred Touron
db85d6545d v1.10.0 2019-06-24 11:31:32 +02:00
Manfred Touron
9912c3deba chore: setup goreleaser 2019-06-24 10:57:47 +02:00
Manfred Touron
fc5c342e40 chore: bump github.com/gliderlabs/ssh to v0.2.2 (#141)
chore: bump github.com/gliderlabs/ssh to v0.2.2
2019-06-24 10:28:51 +02:00
Manfred Touron
60707b3faa chore: update CHANGELOG 2019-06-24 10:25:04 +02:00
Manfred Touron
f36845ac6b chore: bump github.com/gliderlabs/ssh to v0.2.2 2019-06-24 10:25:04 +02:00
Manfred Touron
9f76bd6cad Merge pull request #131 from moul/renovate/all
Update all
2019-06-14 11:26:07 +02:00
Manfred Touron
c53d5d9964 Merge pull request #137 from moul/renovate/docker-all
Update all Docker tags to v1.12.6
2019-06-14 11:25:21 +02:00
Renovate Bot
171d461ea5 Update all 2019-06-14 08:29:13 +00:00
Renovate Bot
b2b04a1155 Update all Docker tags to v1.12.6 2019-06-13 00:27:25 +00:00
Manfred Touron
671ba03b78 Update README.md 2019-06-10 09:57:20 +02:00
Manfred Touron
9095725778 Update README.md 2019-06-10 09:57:05 +02:00
Manfred Touron
8b2e5daba3 Set log files mode to 440 instead of 640. (#134)
Set log files mode to 440 instead of 640.
2019-06-10 09:54:53 +02:00
Jonathan Lestrelin
75b7a5f571 Set log files mode to 440 instead of 640. 2019-06-10 09:40:25 +02:00
Manfred Touron
4b9e881ad0 Merge pull request #135 from jle64/accept_ip_as_hostname
Allow to create a host using an ip as name
2019-06-09 15:39:41 +02:00
Manfred Touron
59f8f52cca Merge pull request #133 from jle64/better_logfile_name
Add username and session ID to session log filename.
2019-06-09 15:38:08 +02:00
Jonathan Lestrelin
4adaf83fd3 Accept to create a host using an ip as name by relaxing unix_user constraint on name and not splitting on . if an ip is detected. 2019-06-07 18:15:25 +02:00
Jonathan Lestrelin
84464a4ea6 Add username and session ID to session log filename. 2019-06-07 16:58:41 +02:00
Manfred Touron
cafac0b8b5 chore: fix CI (#132)
chore: fix CI
2019-06-06 16:35:45 +02:00
Manfred Touron
5346300a64 chore: fix CI 2019-06-06 16:32:49 +02:00
Manfred Touron
1d4554eabc chore: fixup 2019-06-06 16:32:22 +02:00
Manfred Touron
50bdba8b70 Update all (#110)
Update all
2019-05-30 20:40:22 +02:00
Manfred Touron
8c785f6dea Update all Docker tags to v1.12.5 (#130)
Update all Docker tags to v1.12.5
2019-05-30 20:40:01 +02:00
Renovate Bot
93e6abc9ba Update all 2019-05-28 19:27:01 +00:00
Renovate Bot
60d7c85c11 Update all Docker tags to v1.12.5 2019-05-21 20:28:40 +00:00
Manfred Touron
883bad2ee5 Merge pull request #124 from welderpb/master
[fix] unable to use encrypted ssh private keys
2019-03-29 07:11:08 +01:00
Manfred Touron
7d68e144f8 Merge pull request #116 from moul/renovate/docker-all
chore(deps): update all docker tags to v1.12.1
2019-03-29 07:08:36 +01:00
Manfred Touron
7f32e38cf8 Merge pull request #123 from vdaviot/master
[FIX] Format of id in new session | Closing channel if host unreachable
2019-03-29 07:07:36 +01:00
Renovate Bot
43a96d1636 chore(deps): update all docker tags to v1.12.1 2019-03-16 00:20:45 +00:00
welderpb
00e7d2e45d fix for encrypted ssh private keys 2019-03-05 17:04:05 +01:00
Valentin Daviot
2e711c3591 fixed format of id for new bastion session | applied fix from issue #56 to permit the closing of channel in case of unreachable host
Signed-off-by: Valentin Daviot <valentin.daviot@alterway.fr>
2019-03-01 16:04:27 +01:00
Manfred Touron
5d147fc03b Merge pull request #118 from nellyasher/master
Readme updates
2019-02-01 13:38:07 +01:00
Manfred Touron
3dccefbbcb chore: use png + add toc 2019-02-01 13:36:36 +01:00
Nelly Asher
7c4995fa4a Update README.md 2019-02-01 13:34:18 +01:00
Nelly Asher
2b8f051414 TOC added, chapters rearranged, "Use cases" added. 2019-02-01 13:34:18 +01:00
Manfred Touron
4e17c81d63 chore: switch to png for images in the readme 2019-02-01 12:57:18 +01:00
Manfred Touron
8b4b677d6a chore: update flow diagram + add png outputs 2019-02-01 12:56:07 +01:00
Manfred Touron
47229bf473 chore: update flow diagram style 2019-02-01 11:23:02 +01:00
Manfred Touron
ec5b567da9 Update README.md 2019-01-04 01:11:10 +01:00
Manfred Touron
03b59fae1c Merge pull request #113 from ahhx/master
refactor: split package main
2019-01-04 00:58:24 +01:00
Manfred Touron
ede8b3ecf2 chore: fix lint issues 2019-01-04 00:56:06 +01:00
Manfred Touron
7ae90b9199 build: fix deps checksums 2019-01-04 00:51:54 +01:00
ahh
a651da451e refactor: split package main
sshportal refactor. Focused on splitting up package main into packages
main, dbmodels, crypto, and bastion.
2019-01-03 21:11:43 +01:00
Manfred Touron
f220af5c54 Merge pull request #111 from shawn111/missing_authorized_key
Fix userkey create.
2019-01-03 19:57:19 +01:00
Shawn Wang
eebf987900 Fix UserKey.Key for govalidator.ValidateStruct.
UserKey.Key is a []byte field.
The valid: "required" would cause wrong Validation.
2018-12-13 10:46:05 +00:00
Shawn Wang
3d5101011f Fix userkey create, missing AuthorizedKey 2018-12-12 23:39:25 +08:00
Manfred Touron
2cdc19dfdd Merge pull request #109 from moul/moul-patch-1
Update renovate.json
2018-12-07 14:53:50 +01:00
Manfred Touron
d8a7b1e16c Update renovate.json 2018-12-07 14:49:50 +01:00
Manfred Touron
d7490d089c Merge pull request #102 from moul/dev/mou/bump-deps2
chore: bump deps
2018-11-25 09:46:57 +01:00
Manfred Touron
774c6c0f64 chore: bump deps 2018-11-25 09:43:28 +01:00
Manfred Touron
92d11c53de Merge pull request #98 from Raerten/env-variables
use Environment Variables for settings
2018-11-25 09:41:09 +01:00
Manfred Touron
c509f65a27 Merge pull request #101 from moul/dev/moul/windows-support
fix a build constraint
2018-11-25 09:40:00 +01:00
Дмитрий Шульгачик
20b9e839d3 fix a build constraint 2018-11-25 09:34:41 +01:00
Дмитрий Шульгачик
6f4fb24cd0 update mysql example using ENV 2018-11-19 14:56:24 +03:00
Дмитрий Шульгачик
23a89fe1de Add ability to set DB driver and DB credentials from environment variables 2018-11-19 14:49:31 +03:00
Manfred Touron
559df1f523 Merge pull request #96 from moul/dev/moul/bump-deps
chore: bump deps
2018-11-18 15:58:07 +01:00
Manfred Touron
ad2b8ebc38 chore: bump deps 2018-11-18 15:54:11 +01:00
Manfred Touron
3824629d4d Post-release version bump 2018-11-18 15:48:42 +01:00
Manfred Touron
38224714e1 v1.9.0 2018-11-18 15:46:55 +01:00
Manfred Touron
a9f4227bba Merge pull request #90 from alenn-m/master
fix #69
2018-11-18 15:32:37 +01:00
Alen Masic
06bde77f51 fixed the issue when Author didn't update 2018-11-17 10:39:34 +01:00
Alen Masic
2a2554e7a3 fix #69 2018-11-16 17:31:59 +01:00
Manfred Touron
5d835011e6 Merge pull request #87 from moul/dev/moul/golangci-lint
build: switch to golangci-lint
2018-11-16 17:26:26 +01:00
Manfred Touron
0f294cd62d build: switch to golangci-lint 2018-11-16 17:20:39 +01:00
Manfred Touron
8e62d21c25 Merge pull request #86 from moul/dev/moul/canonical
Switching to moul.io/sshportal (canonical url)
2018-11-16 15:42:38 +01:00
Manfred Touron
2a5dd63e87 chore: use moul.io/sshportal canonical url 2018-11-16 15:36:14 +01:00
Manfred Touron
06cb424b8f chore: bump deps 2018-11-16 15:29:51 +01:00
Manfred Touron
7c5864a9c3 Merge pull request #83 from moul/dev/moul/gomod
build: switch to go modules
2018-11-16 15:17:22 +01:00
Manfred Touron
668e34ccab build: switch to go modules 2018-11-16 15:02:47 +01:00
Manfred Touron
95477715fc Merge pull request #75 from vdaviot/closed_session
[FIX] session status and duration displaying properly
2018-11-16 10:19:38 +01:00
Manfred Touron
ecc004a485 Merge pull request #74 from vdaviot/unset_hop
[FIX] unsetting hop on host
2018-11-16 10:18:10 +01:00
valentin.daviot
9f0657374b unsetting work properly 2018-11-16 10:12:49 +01:00
Manfred Touron
61b7f72e94 Merge pull request #76 from vdaviot/active_session_list
[FIX] Session ls on big databases
2018-11-16 10:10:30 +01:00
Manfred Touron
db000baaa5 Merge pull request #67 from moul/renovate/configure
Configure Renovate
2018-11-16 10:05:42 +01:00
valentin.daviot
a1a3a29d00 session status and duration displaying properly 2018-11-16 10:03:37 +01:00
valentin.daviot
2ea73a941f listing active session 2018-11-16 10:01:23 +01:00
valentin.daviot
e860b60d20 session status and duration displaying properly 2018-11-16 10:01:23 +01:00
Manfred Touron
d6be01b9b7 build: temporarily disable gometalinter before changing to another solution 2018-11-16 09:59:18 +01:00
Manfred Touron
64c8e01c33 Merge pull request #80 from ahhx/idle-timeout
add idle connection timeout and idle-timeout flag
2018-11-16 09:53:02 +01:00
ahh
acce797e55 add logging 2018-11-15 13:56:10 -05:00
ahh
175fc8d68b add timeout and flag 2018-11-15 13:38:18 -05:00
Renovate Bot
b9d1cf69c7 Add renovate.json 2018-08-19 01:32:26 +00:00
Manfred Touron
41eeb364f8 Ignore some circle CI tests 2018-08-18 23:43:48 +02:00
Manfred Touron
a22f8f0b7b Merge pull request #58 from adyxax/master
Added `hostgroup update` and `usergroup update` features
2018-04-06 09:34:17 +02:00
Julien Dessaux
bd1c3609a7 Added hostgroup update and usergroup update features 2018-04-05 16:25:43 +02:00
Manfred Touron
c5e75df64f Post-release version bump 2018-04-02 22:36:07 +02:00
Manfred Touron
6b181dd291 v1.8.0 2018-04-02 22:36:06 +02:00
Manfred Touron
4ab88cad10 fix merge 2018-04-02 22:36:06 +02:00
Manfred Touron
b902953df4 Update changelog 2018-04-02 22:36:06 +02:00
Manuel Sabban
e141368734 Add log for exec request. 2018-04-02 22:36:06 +02:00
Manfred Touron
980da40988 Update Readme and Changelog 2018-04-02 22:28:10 +02:00
Manfred Touron
22d25f1e70 Merge pull request #44 from sabban/tunnel
Logtunnel
2018-03-24 00:02:31 +01:00
Manfred Touron
84d77d0a9f Merge pull request #52 from adyxax/master
Added ssh key import feature in "key import"
2018-03-23 23:29:37 +01:00
Julien Dessaux
b0afdf933a Added ssh key import feature in "key import" 2018-03-21 17:48:11 +01:00
Manuel
e9eef9a49e add an acceptable error management. 2018-03-19 18:06:03 +01:00
Manfred Touron
6f2b58cbdc chore: esthetics + update changelog 2018-03-14 18:17:40 +01:00
Manfred Touron
09ac2c35f3 Merge pull request #54 from jle64/dont_crash_on_missing_user
Show 'n/a' in case of missing information to avoid crashing.
2018-03-14 18:13:08 +01:00
Jonathan Lestrelin
47a6fc9906 Show 'n/a' in case of missing information to avoid crashing. 2018-03-14 17:40:48 +01:00
Manuel
c3d49fde95 Merge branch 'master' of https://github.com/moul/sshportal into tunnel 2018-03-12 12:31:17 +01:00
Manfred Touron
ec1e4d5c8a Update README and CHANGELOG 2018-02-28 17:22:59 +01:00
Manfred Touron
e65ef7ccc1 Merge pull request #47 from mathieui/multi-hops
Implement proxied connections
2018-02-28 17:20:18 +01:00
Manfred Touron
68e7fd2090 Merge pull request #49 from moul/dev/moul/fix-mysql-delete
Fix `--assign` commands when using MySQL driver
2018-02-28 16:56:29 +01:00
Manfred Touron
b958f8461f Fix commands when using MySQL driver ([#45](https://github.com/moul/sshportal/issues/45)) 2018-02-28 16:54:32 +01:00
Manfred Touron
a08d84e7ed Merge pull request #48 from moul/dev/moul/fix-make-dev-cmd
Small fixes
2018-02-28 16:29:36 +01:00
Manfred Touron
2b66d8d56a Ingore /log directory 2018-02-28 14:35:38 +01:00
Manfred Touron
a40789e1f2 Fix 'make dev' rule 2018-02-28 14:35:06 +01:00
Mathieu Pasquet
63571af252 Add hops management in "host update"
- allow changing the hop set for this host
- allow removing hops altogether
2018-02-27 17:54:57 +01:00
Mathieu Pasquet
75c6840ecd Implement proxied connections
The feature is implemented as follows:
- when creating a host, there is a possiblity to add a "hop"
- hops are referend them with the name of the host in sshportal
- the hop ID is then saved in the DB in the hosts table
- when connecting to a host, sshportal will recurse through all the
  possible hops of a host (allowing chained proxies)
2018-02-22 18:07:41 +01:00
Manfred Touron
e6a02a85f0 Fix typo in template 2018-02-03 00:45:05 +01:00
Manuel Sabban
2c3de75f3d Logtunnel (#1)
* * When a new channel is opened we got stuck in the select loop in
bastionsession.go, and we couldn't open a new channel. The fix is
easy it calls the bastionsession.ChannelHandler in a goroutine,
at the cost of some error management. I think this is ok because
we can allow a channel to fail on his own. This seems to be
* This add the tunnel feature, which use a new concurrent channel.
* This add some pcap logging for tunnel.
For now it is logged only one way, and the logged ip packet seems
buggy.

* Add logtunnuel as a package.
The logfile format is a tweaked version of ttyrec format file as it will be easy to review the use of human readable tunnel...

To get the ChannelHandler work as a go routine I had to deactivate lint errcheck for logcahnnel. I think this could be a problem. What is your thoughts about this ?
2018-01-18 11:20:37 +01:00
Manfred Touron
7c4aab34ed Merge pull request #39 from moul/moul/alt/gh-tmpl
Add GitHub templates
2018-01-11 13:15:37 +01:00
Manfred Touron
a8480f82e0 Merge pull request #38 from QuentinPerez/split-main
cleanup main
2018-01-11 13:15:09 +01:00
Manfred Touron
a5dacca9a1 Create ISSUE_TEMPLATE.md 2018-01-08 10:04:09 +01:00
Manfred Touron
31ba233b34 Create PULL_REQUEST_TEMPLATE.md 2018-01-08 09:41:10 +01:00
Quentin Perez
5720123576 main: remove globalContext, and move some functions outside of the main 2018-01-07 14:09:43 +01:00
Manfred Touron
9cc09b320d Merge pull request #36 from moul/sabban
Add audit feature
2018-01-05 11:09:26 +01:00
Manfred Touron
cb3c1056e5 Small fixes 2018-01-05 11:05:42 +01:00
Manfred Touron
82f96e457c Merge branch 'master' into sabban 2018-01-05 10:39:04 +01:00
Manfred Touron
062e2b4b8f Merge pull request #35 from moul/dev/moul/homebrew
Add homebrew config
2018-01-05 10:28:11 +01:00
Manfred Touron
9de51acbcc Add homebrew config 2018-01-05 10:24:43 +01:00
Manfred Touron
6d3a97cdbc Merge pull request #34 from moul/dev/moul/telnet
Add telnet support
2018-01-05 10:18:25 +01:00
Manfred Touron
3ebcdd9c3d Add telnet support 2018-01-05 10:14:02 +01:00
Manfred Touron
a9f86d1d01 Remove gliderlabs/ssh from vendor.json to avoid updating it 2018-01-05 10:13:45 +01:00
Manfred Touron
2a68fc3114 Support having different host.Scheme 2018-01-05 10:13:45 +01:00
Manfred Touron
2352a53e6e Add telnet dependencies 2018-01-05 10:13:45 +01:00
Manuel
fcc94c58d9 get rid of this package as we use it from its home location. 2018-01-04 14:15:05 +01:00
Manuel
da9c4920ab add log directory creation if it does not exist. 2018-01-04 13:41:14 +01:00
Manuel
0295eedb6e fix log location 2018-01-04 11:49:24 +01:00
Manuel
7f26cc1dbb Fix the default log path to ./log 2018-01-04 11:45:05 +01:00
Manuel
9e1c395810 add fatal error when record file cannot be opened. 2018-01-04 11:43:44 +01:00
Manuel
9db4b92d4e Use of govendor and add "github.com/arkan/bastion/pkg/logchannel" pkg. 2018-01-04 11:32:51 +01:00
Manuel
ff46ee89d9 logs_location -> logsLocation 2018-01-04 11:31:51 +01:00
Manfred Touron
b9af077ef4 Merge pull request #33 from moul/dev/moul/default-username
Dynamic username for the first created account
2018-01-03 19:54:21 +01:00
Manfred Touron
b23ee4144d The default created user now has the same username as the user starting sshportal (was hardcoded admin) 2018-01-03 19:00:52 +01:00
Manuel
57f894bfca Merge branch 'master' of https://github.com/moul/sshportal into sabban
pull from master.
2018-01-03 14:22:28 +01:00
Manuel
58e2abca8c Fix when error on session file creation. 2018-01-03 14:06:05 +01:00
Manuel
ed676b0d7e add the pkg 2018-01-03 10:56:49 +01:00
Manfred Touron
ed42f343d2 Post-release version bump 2018-01-03 00:27:07 +01:00
Manfred Touron
2555c478b4 v1.7.1 2018-01-03 00:26:38 +01:00
Manfred Touron
6152e55e7d Merge pull request #30 from moul/dev/moul/more-integration-tests
More integration tests
2018-01-03 00:25:45 +01:00
Manfred Touron
023cdd1bb3 Test bastion in integration 2018-01-03 00:23:46 +01:00
Manfred Touron
5efe250466 hotfix: repair invite system (broken in v1.7.0) 2018-01-03 00:23:46 +01:00
Manfred Touron
695ddc91dd Return non-null exit-code on authentication error 2018-01-03 00:23:46 +01:00
Manfred Touron
7b30017a14 Complete list of features 2018-01-03 00:23:45 +01:00
Manfred Touron
e5542ae266 Update graphs 2018-01-03 00:23:45 +01:00
Manfred Touron
d19b8a53f2 Add dependencies 2018-01-03 00:23:45 +01:00
Manfred Touron
2e39f70cd5 Add '_test_server' hidden handler 2018-01-03 00:23:45 +01:00
Manuel
26c0bb8b1a typo 2018-01-02 17:43:53 +01:00
Manuel
12b0db07da add audit feature. 2018-01-02 16:31:34 +01:00
Manfred Touron
7aace9109a Update Changelog 2018-01-02 05:58:54 +01:00
Manfred Touron
6c4caea26f Post-release version bump 2018-01-02 05:57:13 +01:00
Manfred Touron
13c0726849 v1.7.0 2018-01-02 05:56:24 +01:00
Manfred Touron
1b52673603 Add missing ch.Close() on premature error 2018-01-02 05:55:01 +01:00
Manfred Touron
7ea7237d19 Merge pull request #25 from moul/dev/moul/fix-24
Support putty
2018-01-02 05:46:10 +01:00
Manfred Touron
d6bb5e44a1 Refactor bastion handler to forward every requests properly 2018-01-02 05:37:24 +01:00
Manfred Touron
072464928b Refactor sshportal: create a custom bastion session handler 2018-01-01 22:15:28 +01:00
Manfred Touron
4125bc2768 Refactor gliderlabs/ssh to support custom handlers 2018-01-01 22:14:44 +01:00
Manfred Touron
ee29310ed3 Update gliderlabs/ssh dependency to latest upstream commit hash 2018-01-01 22:14:44 +01:00
Manfred Touron
0e6917ae2a Merge pull request #29 from moul/dev/moul/docker-healthcheck
Add Docker healthcheck helper
2018-01-01 11:08:46 +01:00
Manfred Touron
57dd2c6c01 Add healthcheck --wait and --quiet options 2018-01-01 11:05:05 +01:00
Manfred Touron
6494e69632 Add Docker healthcheck helper 2018-01-01 10:58:43 +01:00
Manfred Touron
d6ea80dab1 Merge pull request #28 from moul/dev/moul/ci-integration
Run integration tests on CI
2018-01-01 09:30:35 +01:00
Manfred Touron
fbb3e7134f Run integration tests in parallel in CI 2018-01-01 09:28:34 +01:00
Manfred Touron
9fdb36c6ca Refactor 'make integration' to be runnable with docker-compose 2018-01-01 09:16:09 +01:00
Manfred Touron
9bc545b4bb Add config backup --ignore-events option 2018-01-01 09:07:41 +01:00
Manfred Touron
457f60f815 Use sshportal server instead of sshportal to start a new server 2018-01-01 09:07:41 +01:00
Manfred Touron
78db26a532 Merge pull request #26 from moul/dev/moul/fix-22
Fix connection failure when sending too many environment variables
2017-12-28 23:29:41 +01:00
Manfred Touron
fb15225c35 Fix connection failure when sending too many environment variables (fix #22)
*Temporary fix*

A better solution requires a refactor of the ssh session handler (related with #24)

Related with this comment: https://github.com/gliderlabs/ssh/issues/47#issuecomment-340550104
2017-12-28 23:25:11 +01:00
Manfred Touron
c8fb103762 Merge pull request #21 from sabban/master
typo fix.
2017-12-21 15:37:55 +01:00
Manuel
585fd3a3ff typo fix. 2017-12-21 15:35:26 +01:00
Manfred Touron
0aefd4d093 Merge pull request #20 from sabban/master
fix typo.
2017-12-21 13:08:34 +01:00
Manuel
5f0c5b3375 fix typo. 2017-12-21 12:32:18 +01:00
Manfred Touron
6b0b22cb7b Merge pull request #19 from QuentinPerez/typo
fix typo 's/aes-keuy/aes-key'
2017-12-19 09:34:15 +01:00
Quentin Perez
0f84be8fa0 fix typo 's/aes-keuy/aes-key' 2017-12-19 09:10:37 +01:00
Manfred Touron
849a485621 Merge pull request #18 from alexanderturner/fix/vendor+shell
Small fixes
2017-12-19 07:31:21 +01:00
Alexander Turner
a721d4ff01 -Updated typo in shell /s/invite-/invite:
-Removed gliderlabs/ssh from vendor.json to force /vendor version until committed upstream
2017-12-19 12:24:51 +11:00
Manfred Touron
62c8fe2dbf Update .dockerignore 2017-12-17 10:25:55 +01:00
Manfred Touron
756c8f02e8 Add diagram flow 2017-12-17 10:11:34 +01:00
Manfred Touron
62db91b7be Merge pull request #16 from fossabot/master
Add license scan report and status
2017-12-16 10:10:36 +01:00
Manfred Touron
8543a1f01a Add goreport card + udpate features and limitations list 2017-12-16 10:06:46 +01:00
fossabot
db20c81a39 Add license scan report and status 2017-12-16 00:56:09 -08:00
Manfred Touron
395827afeb Merge pull request #15 from moul/dev/moul/doc
Improve doc
2017-12-16 08:56:48 +01:00
Manfred Touron
8329fd3ab7 Improve doc 2017-12-15 20:27:07 +01:00
Manfred Touron
e32f4200d1 Merge pull request #14 from moul/dev/moul/fix-13
Fix panic when entering empty command
2017-12-15 13:57:41 +01:00
Manfred Touron
7ed60f6908 Fix panic when entering empty command (fix #13) 2017-12-15 13:55:53 +01:00
Manfred Touron
a413aa86c2 Add sql-schema.svg 2017-12-13 17:23:34 +01:00
Manfred Touron
b51c90a0e9 Update README 2017-12-13 11:52:19 +01:00
Manfred Touron
7245508c76 Add experimental warning 2017-12-12 14:55:50 +01:00
Manfred Touron
905159f044 Update demo gif 2017-12-12 12:26:49 +01:00
Manfred Touron
ac8181474c Post-release version bump 2017-12-12 10:40:14 +01:00
Manfred Touron
554937dd7a v1.6.0 2017-12-12 10:39:36 +01:00
Manfred Touron
4aa9a227e8 Switch to a lonely tmux demo 2017-12-12 10:15:09 +01:00
Manfred Touron
6e4cbf5dd8 Add server/client gif example in README 2017-12-12 09:05:49 +01:00
Manfred Touron
44d1ac7f11 Add example gifs 2017-12-12 09:01:33 +01:00
Manfred Touron
1c32da7751 Merge pull request #12 from moul/dev/moul/key-show
Add 'key show KEY' command
2017-12-06 00:37:37 +01:00
Manfred Touron
999b740df6 Add 'key show KEY' command (#11) 2017-12-06 00:33:40 +01:00
Manfred Touron
6864b7ca10 govendor add github.com/mgutz/ansi + its deps 2017-12-06 00:33:40 +01:00
Manfred Touron
546b350a6c Create LICENSE 2017-12-04 16:45:42 +01:00
Manfred Touron
d70296cd95 Update README.md 2017-12-04 16:33:41 +01:00
Manfred Touron
10f4ad49d9 Improve logging 2017-12-04 11:14:20 +01:00
Manfred Touron
edb230b278 Update README.md 2017-12-04 10:41:25 +01:00
Manfred Touron
efbf66a0a4 Update README.md 2017-12-04 10:39:40 +01:00
Manfred Touron
0746458762 Merge pull request #10 from moul/dev/moul/circle
Dev/moul/circle
2017-12-04 10:37:24 +01:00
Manfred Touron
f2738e2bd1 Fix lint warns 2017-12-04 10:32:29 +01:00
Manfred Touron
b0d8180809 Add CircleCI config 2017-12-04 10:21:36 +01:00
Manfred Touron
f9d450ffaf Add healthcheck user 2017-12-04 09:34:52 +01:00
Manfred Touron
391a39d82c Add --latest and --quiet options to ls commands 2017-12-04 09:27:10 +01:00
Manfred Touron
7eb76c861f Lint code + fix tests 2017-12-03 18:18:17 +01:00
Manfred Touron
cd437a3a7b Post-release version bump 2017-12-02 01:12:29 +01:00
Manfred Touron
2accc7abd4 v1.5.0 2017-12-02 01:11:40 +01:00
Manfred Touron
3c10578584 Fix some backup/restore bugs + improve MySQL support 2017-12-02 00:01:31 +01:00
Manfred Touron
511470087b Host key checking shared across users 2017-12-01 22:19:22 +01:00
Manfred Touron
017ee2ab39 Add MySQL support 2017-11-29 14:07:59 +01:00
Manfred Touron
b093f61fb5 Switch to hard delete 2017-11-29 10:27:04 +01:00
Manfred Touron
bd158819d3 Add 'make dev EXTRA_RUN_OPTS' flag
make dev EXTRA_RUN_OPTS="--db-conn=root@/db?parseTime=true --db-driver=mysql"
2017-11-29 10:25:52 +01:00
Manfred Touron
86f6e87efe Add audit log 2017-11-29 09:17:19 +01:00
Manfred Touron
e377cac8e6 Ignore some errors when logging closed connections 2017-11-28 20:08:31 +01:00
Manfred Touron
0fbcc0dd41 Session management 2017-11-27 08:52:33 +01:00
Manfred Touron
1fdf37dc07 Create Session objects on each connections (history) 2017-11-27 08:22:13 +01:00
Manfred Touron
4cf73e3410 Moved demo code in the README as example 2017-11-27 08:09:22 +01:00
Manfred Touron
328bb0153b Add session model 2017-11-27 07:43:52 +01:00
Manfred Touron
1ddd6867b6 Post-release version bump 2017-11-24 15:22:50 +01:00
Manfred Touron
2becd5eec2 v1.4.0 2017-11-24 15:22:22 +01:00
Manfred Touron
571b37da6b Add option to encrypt sensitive data 2017-11-24 15:15:24 +01:00
Manfred Touron
01d464f4c5 Sort items by created_at in 'ls' commands 2017-11-24 07:27:38 +01:00
Manfred Touron
bf184c621d Merge branch 'dev/moul/timeago'
* dev/moul/timeago:
  Add Updated and Created fields in 'ls' commands
  govendor add github.com/dustin/go-humanize
2017-11-24 06:48:07 +01:00
Manfred Touron
f4309f843b Add Updated and Created fields in 'ls' commands 2017-11-24 06:47:39 +01:00
Manfred Touron
cbdc231cbf govendor add github.com/dustin/go-humanize 2017-11-24 06:46:54 +01:00
Manfred Touron
0f0a8dd9bb Add 'key setup' command (easy SSH key installation) 2017-11-24 05:04:22 +01:00
Manfred Touron
4189eb8154 Update README.md 2017-11-23 19:06:30 +01:00
Manfred Touron
1d6349767d Post-commit version bump 2017-11-23 19:04:57 +01:00
Manfred Touron
f6ba06298d v1.3.0 2017-11-23 19:04:00 +01:00
Manfred Touron
31a8cef59f Merge pull request #9 from moul/dev/moul/roles
Support multiple roles
2017-11-23 19:00:12 +01:00
Manfred Touron
beeba0551b Add 'listhosts' role (fix #5) 2017-11-23 18:59:59 +01:00
Manfred Touron
a36bb68957 Allow connecting to the shell mode with the registered username or email 2017-11-23 17:45:16 +01:00
Manfred Touron
9cd9152a91 Switch from IsAdmin boolean to Roles 2017-11-23 17:45:16 +01:00
Manfred Touron
09c1e0504e Add 'acl update' command (fix #4) 2017-11-23 12:01:17 +01:00
Manfred Touron
37d7c839dd Add 'user update' command (fix #3) 2017-11-23 11:36:24 +01:00
Manfred Touron
8ba418308e Merge pull request #8 from moul/dev/moul/wip-update
Add 'host update'
2017-11-23 10:36:34 +01:00
Manfred Touron
cfcf124d83 Improve CLI help messages 2017-11-23 10:35:51 +01:00
Manfred Touron
ccb0071d12 Add Add 'host update' command (fix #2) 2017-11-23 10:15:28 +01:00
Manfred Touron
681f59c1e6 More details in 'ls' commands 2017-11-23 10:05:13 +01:00
Manfred Touron
1bdee1a107 Refactor database helpers 2017-11-23 09:58:32 +01:00
Manfred Touron
a2f3a51fe5 Post-release version bump 2017-11-22 15:16:24 +01:00
Manfred Touron
98d4360a76 v1.2.0 2017-11-22 15:15:30 +01:00
Manfred Touron
15c58c99b2 Small typo 2017-11-20 11:12:58 +01:00
Manfred Touron
e6198e16e5 Testing backup/restore/backup/diff in integration 2017-11-20 10:24:30 +01:00
Manfred Touron
97d166ad8f Enable debug in integration 2017-11-19 01:30:37 +01:00
Manfred Touron
fb4ca3d219 Use a database migration system 2017-11-19 01:30:35 +01:00
Manfred Touron
62aea661cc Backup/restore settings 2017-11-19 01:30:16 +01:00
Manfred Touron
f33326db4d govendor add github.com/go-gormigrate/gormigrate 2017-11-19 01:30:15 +01:00
Manfred Touron
c5d00728d0 $> govendor add github.com/asaskevich/govalidator 2017-11-19 01:30:15 +01:00
Manfred Touron
1591cbc208 Use govalidator to perform more consistent input validation 2017-11-19 01:30:13 +01:00
Manfred Touron
58d9aef616 Test ACLs/hostgroups/usergroups 2017-11-15 19:17:14 +01:00
Manfred Touron
a087cdad09 Store and compare version in database 2017-11-15 19:17:14 +01:00
Manfred Touron
7f754e2ab9 Add basic integration tests 2017-11-15 19:17:14 +01:00
Manfred Touron
2261d27c94 Allow to set the first invite token (for testing) 2017-11-15 19:17:14 +01:00
Manfred Touron
f97c9f2878 Support assign multiple groups to hosts and users (#2) 2017-11-15 19:16:55 +01:00
Manfred Touron
d6a7a6702f Post-release version bump 2017-11-14 11:16:24 +01:00
Manfred Touron
abba8dc990 Show how to expose port with Docker 2017-11-14 10:15:40 +01:00
Manfred Touron
602514f517 Add Docker upgrade instructions 2017-11-14 10:12:25 +01:00
Manfred Touron
8db6afe8b9 Add 'make backup' dev helper 2017-11-14 09:57:59 +01:00
Manfred Touron
3e565365f6 v1.1.0 2017-11-14 01:29:59 +01:00
Manfred Touron
20fee4e98a Set random seed properly 2017-11-14 01:29:25 +01:00
Manfred Touron
2cc91bf681 Disable mysql support 2017-11-14 01:28:18 +01:00
Manfred Touron
9253b9dd60 Add CHANGELOG.md 2017-11-14 01:20:50 +01:00
Manfred Touron
20a2ffcd6c Add better versionning 2017-11-14 01:13:51 +01:00
Manfred Touron
0b137e1939 Use dynamic version 2017-11-14 00:38:23 +01:00
Manfred Touron
8d6a76a93b Add Docker instruction 2017-11-14 00:27:12 +01:00
Manfred Touron
5ad2d59b3b Add note about backup/restore 2017-11-14 00:21:19 +01:00
Manfred Touron
83ad730579 Add 'config restore' 2017-11-13 23:57:52 +01:00
Manfred Touron
f03cdc095c Add 'config backup' 2017-11-13 20:22:45 +01:00
Manfred Touron
0b9f9b43a8 Implement exit (fix #6) 2017-11-13 20:15:12 +01:00
512 changed files with 7669 additions and 405426 deletions

BIN
.assets/bastion.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

BIN
.assets/client.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

30
.assets/cluster-mysql.dot Normal file
View File

@@ -0,0 +1,30 @@
graph {
rankdir=LR;
subgraph cluster_sshportal {
label="sshportal cluster";
edge[style=dashed,color=grey,constraint=false];
sshportal1; sshportal2; sshportal3; sshportalN;
sshportal1 -- MySQL;
sshportal2 -- MySQL;
sshportal3 -- MySQL;
sshportalN -- MySQL;
}
subgraph cluster_hosts {
label="hosts";
host1; host2; host3; hostN;
}
subgraph cluster_users {
label="users";
user1; user2; user3; userN;
}
{
user1 -- sshportal1 -- host1[color=red,penwidth=3.0];
user2 -- sshportal2 -- host2[color=green,penwidth=3.0];
user3 -- sshportal3 -- host3[color=blue,penwidth=3.0];
user3 -- sshportal2 -- host1[color=purple,penwidth=3.0];
userN -- sshportalN -- hostN[style=dotted];
}
}

BIN
.assets/cluster-mysql.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
.assets/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

65
.assets/flow-diagram.dot Normal file
View File

@@ -0,0 +1,65 @@
digraph {
node[shape=record;style=rounded;fontname="helvetica-bold"];
graph[layout=dot;rankdir=LR;overlap=prism;splines=true;fontname="helvetica-bold"];
edge[arrowhead=none;fontname="helvetica"];
start[label="\$\> ssh sshportal";color=blue;fontcolor=blue;fontsize=18];
subgraph cluster_sshportal {
graph[fontsize=18;color=gray;fontcolor=black];
label="sshportal";
{
node[color=darkorange;fontcolor=darkorange];
known_user_key[label="known user key"];
unknown_user_key[label="unknown user key"];
invite_manager[label="invite manager"];
acl_manager[label="ACL manager"];
}
{
node[color=darkgreen;fontcolor=darkgreen];
builtin_shell[label="built-in\nconfig shell"];
ssh_proxy[label="SSH proxy\nJump-Host"];
learn_key[label="learn the\npub key"];
}
err_and_exit[label="\nerror\nand exit\n\n";color=red;fontcolor=red];
{ rank=same; ssh_proxy; builtin_shell; learn_key; err_and_exit; }
{ rank=same; known_user_key; unknown_user_key; }
}
subgraph cluster_hosts {
label="your hosts";
graph[fontsize=18;color=gray;fontcolor=black];
node[color=blue;fontcolor=blue];
host_1[label="root@host1"];
host_2[label="user@host2:2222"];
host_3[label="root@host3:1234"];
}
{
edge[color=blue];
start -> known_user_key;
start -> unknown_user_key;
ssh_proxy -> host_1;
ssh_proxy -> host_2;
ssh_proxy -> host_3;
}
{
edge[color=darkgreen;fontcolor=darkgreen];
known_user_key -> builtin_shell[label="user=admin"];
acl_manager -> ssh_proxy[label="authorized"];
invite_manager -> learn_key[label="valid token"];
}
{
edge[color=darkorange;fontcolor=darkorange];
known_user_key -> acl_manager[label="user matches an existing host"];
unknown_user_key -> invite_manager[label="user=invite:<token>";labelloc=b];
}
{
edge[color=red;fontcolor=red];
known_user_key -> err_and_exit[label="invalid user"];
acl_manager -> err_and_exit[label="unauthorized"];
unknown_user_key -> err_and_exit[label="any other user";constraint=false];
invite_manager -> err_and_exit[label="invalid token";constraint=false];
}
}

BIN
.assets/flow-diagram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

1
.assets/flow-diagram.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

36
.assets/overview.dot Normal file
View File

@@ -0,0 +1,36 @@
graph {
rankdir=LR;
node[shape=box,style=rounded,style=rounded,fillcolor=gray];
subgraph cluster_sshportal {
sshportal[penwidth=3.0,color=brown,fontcolor=brown,fontsize=20];
shell[label="built-in\nadmin shell",color=orange,fontcolor=orange];
db[color=gray,fontcolor=gray,shape=circle];
{ rank=same; db; sshportal; shell }
}
{
node[color="green"];
host1; host2; host3; hostN;
}
{
node[color="blue"];
user1; user2; user3; userN;
}
{
edge[penwidth=3.0];
user1 -- sshportal -- host1[color=red];
user2 -- sshportal -- host2[color=blue];
user3 -- sshportal -- host1[color=purple];
user2 -- sshportal -- host3[color=green];
user2 -- sshportal -- shell[color=orange,constraint=false];
}
userN -- sshportal[style=dotted];
sshportal -- hostN[style=dotted];
sshportal -- db[style=dotted,color=grey];
}

BIN
.assets/overview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

1
.assets/overview.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
.assets/server.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

1
.assets/sql-schema.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 34 KiB

40
.circleci/config.yml Normal file
View File

@@ -0,0 +1,40 @@
defaults: &defaults
working_directory: /go/src/moul.io/sshportal
docker:
- image: circleci/golang:1.17.5
environment:
GO111MODULE: "on"
install_retry: &install_retry
run:
name: install retry
command: |
command -v wget &>/dev/null && wget -O /tmp/retry "https://github.com/moul/retry/releases/download/v0.5.0/retry_$(uname -s)_$(uname -m)" || true
if [ ! -f /tmp/retry ]; then command -v curl &>/dev/null && curl -L -o /tmp/retry "https://github.com/moul/retry/releases/download/v0.5.0/retry_$(uname -s)_$(uname -m)"; fi
chmod +x /tmp/retry
/tmp/retry --version
version: 2
jobs:
docker.integration:
<<: *defaults
steps:
- checkout
- run:
name: Install Docker Compose
command: |
umask 022
curl -L https://github.com/docker/compose/releases/download/1.11.4/docker-compose-`uname -s`-`uname -m` > ~/docker-compose
- setup_remote_docker:
docker_layer_caching: true
version: 18.09.3 # https://github.com/golang/go/issues/40893
- *install_retry
- run: /tmp/retry -m 3 docker build -t moul/sshportal .
- run: /tmp/retry -m 3 make integration
workflows:
version: 2
build_and_integration:
jobs:
- docker.integration

View File

@@ -1 +1,6 @@
examples
# .git/ # should be kept for git-based versionning
examples/
.circleci/
.assets/
/sshportal

17
.gitattributes vendored Normal file
View File

@@ -0,0 +1,17 @@
# Auto detect text files and perform LF normalization
* text=auto
# Collapse vendored and generated files on GitHub
AUTHORS linguist-generated
vendor/* linguist-vendored
rules.mk linguist-vendored
*/vendor/* linguist-vendored
*.gen.* linguist-generated
*.pb.go linguist-generated
*.pb.gw.go linguist-generated
go.sum linguist-generated
go.mod linguist-generated
gen.sum linguist-generated
# Reduce conflicts on markdown files
*.md merge=union

6
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,6 @@
github: ["moul"]
patreon: moul
open_collective: sshportal
custom:
- "https://www.buymeacoffee.com/moul"
- "https://manfred.life/donate"

15
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,15 @@
### Actual Result / Problem
When I do Foo, Bar happens...
### Expected Result / Suggestion
I expect that Foobar happens...
### Some context
Any screenshot to share?
`sshportal --version`?
`ssh sshportal info`?
OS/Go version?
...

1
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1 @@
<!-- thank you for your contribution! ❤️ -->

20
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
version: 2
updates:
- package-ecosystem: docker
directory: "/"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
- package-ecosystem: gomod
directory: "/"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10

7
.github/renovate.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"extends": [
"config:base"
],
"groupName": "all",
"gomodTidy": true
}

87
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,87 @@
name: CI
on:
push:
tags:
- v*
branches:
- master
pull_request:
jobs:
docker-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.1.0
- name: Build the Docker image
run: docker build . --file Dockerfile
golangci-lint:
name: golangci-lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.1.0
- name: lint
uses: golangci/golangci-lint-action@v2.5.2
with:
version: v1.38
github-token: ${{ secrets.GITHUB_TOKEN }}
tests-on-windows:
needs: golangci-lint # run after golangci-lint action to not produce duplicated errors
runs-on: windows-latest
strategy:
matrix:
golang:
- 1.16.x
steps:
- uses: actions/checkout@v3.1.0
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.golang }}
- name: Run tests on Windows
run: make.exe unittest
continue-on-error: true
tests-on-mac:
needs: golangci-lint # run after golangci-lint action to not produce duplicated errors
runs-on: macos-latest
strategy:
matrix:
golang:
- 1.16.x
steps:
- uses: actions/checkout@v3.1.0
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.golang }}
- uses: actions/cache@v2.1.7
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ matrix.golang }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ matrix.golang }}-
- name: Run tests on Unix-like operating systems
run: make unittest
tests-on-linux:
needs: golangci-lint # run after golangci-lint action to not produce duplicated errors
runs-on: ubuntu-latest
strategy:
matrix:
golang:
- 1.13.x
- 1.14.x
- 1.15.x
- 1.16.x
steps:
- uses: actions/checkout@v3.1.0
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.golang }}
- uses: actions/cache@v2.1.7
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ matrix.golang }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ matrix.golang }}-
- name: Run tests on Unix-like operating systems
run: make unittest

13
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: Semantic Release
on: push
jobs:
semantic-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.1.0
- uses: codfish/semantic-release-action@v1.9.0
if: github.ref == 'refs/heads/master'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

21
.github/workflows/semgrep.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
on:
pull_request: {}
push:
branches:
- master
paths:
- .github/workflows/semgrep.yml
schedule:
- cron: '0 0 * * 0'
name: Semgrep
jobs:
semgrep:
name: Scan
runs-on: ubuntu-20.04
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
container:
image: returntocorp/semgrep
steps:
- uses: actions/checkout@v3.1.0
- run: semgrep ci

12
.gitignore vendored
View File

@@ -1,2 +1,12 @@
coverage.txt
dist/
*~
*#
.*#
.DS_Store
/log/
/sshportal
*.db
*.db
/data
sshportal.history
.idea

55
.golangci.yml Normal file
View File

@@ -0,0 +1,55 @@
run:
deadline: 1m
tests: false
skip-files:
- "testing.go"
- ".*\\.pb\\.go"
- ".*\\.gen\\.go"
linters-settings:
golint:
min-confidence: 0
maligned:
suggest-new: true
goconst:
min-len: 5
min-occurrences: 4
misspell:
locale: US
linters:
disable-all: true
enable:
- bodyclose
- deadcode
- depguard
- dogsled
#- dupl
- errcheck
#- funlen
- gochecknoinits
#- gocognit
- goconst
- gocritic
#- gocyclo
- gofmt
- goimports
- golint
- gosimple
- govet
- ineffassign
- interfacer
#- maligned
- misspell
- nakedret
- prealloc
- scopelint
- staticcheck
- structcheck
#- stylecheck
- typecheck
- unconvert
- unparam
- unused
- varcheck
- whitespace

29
.goreleaser.yml Normal file
View File

@@ -0,0 +1,29 @@
builds:
-
goos: [linux, darwin]
goarch: [386, amd64, arm, arm64]
ldflags:
- -s -w -X main.GitSha={{.ShortCommit}} -X main.GitBranch=master -X main.GitTag={{.Version}}
archives:
- wrap_in_directory: true
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
brews:
-
name: sshportal
github:
owner: moul
name: homebrew-moul
commit_author:
name: moul-bot
email: "m+bot@42.am"
homepage: https://manfred.life/sshportal
description: "Simple, fun and transparent SSH (and telnet) bastion"

8
.releaserc.js Normal file
View File

@@ -0,0 +1,8 @@
module.exports = {
branch: 'master',
plugins: [
'@semantic-release/commit-analyzer',
'@semantic-release/release-notes-generator',
'@semantic-release/github',
],
};

39
AUTHORS generated Normal file
View File

@@ -0,0 +1,39 @@
# This file lists all individuals having contributed content to the repository.
# For how it is generated, see 'https://github.com/moul/rules.mk'
ahh <ahamidullah@gmail.com>
Alen Masic <alenn.masic@gmail.com>
Alexander Turner <me@alexturner.co>
bozzo <bozzo@users.noreply.github.com>
Darko Djalevski <darko.djalevski@inplayer.com>
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fossabot <badges@fossa.io>
ImgBotApp <ImgBotHelp@gmail.com>
Jason Wessel <jason.wessel@windriver.com>
Jean-Louis Férey <jeanlouis.ferey@orange.com>
jerard@alfa-safety.fr <jrrdev@users.noreply.github.com>
Jess <jessachandler@gmail.com>
Jonathan Lestrelin <jonathan.lestrelin@gmail.com>
Julien Dessaux <julien.dessaux@adyxax.org>
Konstantin Bakaras <k.bakaras@voskhod.ru>
Manfred Touron <94029+moul@users.noreply.github.com>
Manfred Touron <m@42.am>
Manuel <manuel.sabban@nbs-system.com>
Manuel Sabban <manu@sabban.eu>
Manuel Sabban <msa@nbs-system.com>
Mathieu Pasquet <mathieu.pasquet@alterway.fr>
matteyeux <matteyeux@users.noreply.github.com>
Mikael Rapp <micke.rapp@gmail.com>
MitaliBo <mitali.bisht14@gmail.com>
moul-bot <bot@moul.io>
Nelly Asher <karmelylle@rambler.ru>
NocFlame <aad@nocflame.se>
Quentin Perez <qperez42@gmail.com>
Renovate Bot <bot@renovateapp.com>
Sergey Yashchuk <11705746+GreyOBox@users.noreply.github.com>
Sergey Yashchuk <sergey.yashchuk@coins.ph>
Shawn Wang <shawn111@gmail.com>
Valentin Daviot <valentin.daviot@alterway.fr>
valentin.daviot <valentin.daviot@alterway.fr>
welderpb <welderpb@users.noreply.github.com>
Дмитрий Шульгачик <tech@uniplug.ru>

3
CHANGELOG.md Normal file
View File

@@ -0,0 +1,3 @@
# Changelog
Here: https://github.com/moul/sshportal/releases

View File

@@ -1,10 +1,16 @@
# build
FROM golang:1.9 as builder
COPY . /go/src/github.com/moul/sshportal
WORKDIR /go/src/github.com/moul/sshportal
RUN CGO_ENABLED=1 go build -tags netgo -ldflags '-extldflags "-static"' -v -o /go/bin/sshportal
FROM golang:1.18.0 as builder
ENV GO111MODULE=on
WORKDIR /go/src/moul.io/sshportal
COPY go.mod go.sum ./
RUN go mod download
COPY . ./
RUN make _docker_install
# minimal runtime
FROM scratch
FROM alpine
COPY --from=builder /go/bin/sshportal /bin/sshportal
ENTRYPOINT ["/bin/sshportal"]
CMD ["server"]
EXPOSE 2222
HEALTHCHECK CMD /bin/sshportal healthcheck --wait

View File

@@ -1,7 +1,6 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
@@ -176,13 +175,24 @@
END OF TERMS AND CONDITIONS
Copyright 2013-2017 Docker, Inc.
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2017-2021 Manfred Touron <m@42.am>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,

View File

@@ -1,13 +1,47 @@
.PHONY: install
install:
go install .
GOPKG ?= moul.io/sshportal
GOBINS ?= .
DOCKER_IMAGE ?= moul/sshportal
VERSION ?= `git describe --tags --always`
VCS_REF ?= `git rev-parse --short HEAD`
GO_INSTALL_OPTS = -ldflags="-X main.GitSha=$(VCS_REF) -X main.GitTag=$(VERSION)"
PORT ?= 2222
include rules.mk
DB_VERSION ?= v$(shell grep -E 'ID: "[0-9]+",' pkg/bastion/dbinit.go | tail -n 1 | cut -d'"' -f2)
AES_KEY ?= my-dummy-aes-key
.PHONY: integration
integration:
cd ./examples/integration && make
.PHONY: _docker_install
_docker_install:
CGO_ENABLED=1 $(GO) build -ldflags '-extldflags "-static" $(LDFLAGS)' -tags netgo -v -o /go/bin/sshportal
.PHONY: dev
dev:
-go get github.com/githubnemo/CompileDaemon
CompileDaemon -exclude-dir=.git -exclude=".#*" -color=true -command="./sshportal --demo --debug" .
-$(GO) get github.com/githubnemo/CompileDaemon
CompileDaemon -exclude-dir=.git -exclude=".#*" -color=true -command="./sshportal server --debug --bind-address=:$(PORT) --aes-key=$(AES_KEY) $(EXTRA_RUN_OPTS)" .
.PHONY: test
test:
go test -i .
go test -v .
.PHONY: backup
backup:
mkdir -p data/backups
cp sshportal.db data/backups/$(shell date +%s)-$(DB_VERSION)-sshportal.sqlite
doc:
dot -Tsvg ./.assets/overview.dot > ./.assets/overview.svg
dot -Tsvg ./.assets/cluster-mysql.dot > ./.assets/cluster-mysql.svg
dot -Tsvg ./.assets/flow-diagram.dot > ./.assets/flow-diagram.svg
dot -Tpng ./.assets/overview.dot > ./.assets/overview.png
dot -Tpng ./.assets/cluster-mysql.dot > ./.assets/cluster-mysql.png
dot -Tpng ./.assets/flow-diagram.dot > ./.assets/flow-diagram.png
.PHONY: goreleaser
goreleaser:
GORELEASER_GITHUB_TOKEN=$(GORELEASER_GITHUB_TOKEN) GITHUB_TOKEN=$(GITHUB_TOKEN) goreleaser --rm-dist
.PHONY: goreleaser-dry-run
goreleaser-dry-run:
goreleaser --snapshot --skip-publish --rm-dist

487
README.md
View File

@@ -1,43 +1,51 @@
# sshportal
[![CircleCI](https://circleci.com/gh/moul/sshportal.svg?style=svg)](https://circleci.com/gh/moul/sshportal)
[![Go Report Card](https://goreportcard.com/badge/moul.io/sshportal)](https://goreportcard.com/report/moul.io/sshportal)
[![GoDoc](https://godoc.org/moul.io/sshportal?status.svg)](https://godoc.org/moul.io/sshportal)
[![Financial Contributors on Open Collective](https://opencollective.com/sshportal/all/badge.svg?label=financial+contributors)](https://opencollective.com/sshportal) [![License](https://img.shields.io/github/license/moul/sshportal.svg)](https://github.com/moul/sshportal/blob/master/LICENSE)
[![GitHub release](https://img.shields.io/github/release/moul/sshportal.svg)](https://github.com/moul/sshportal/releases)
<!-- temporarily broken? [![Docker Build Status](https://img.shields.io/docker/build/moul/sshportal.svg)](https://hub.docker.com/r/moul/sshportal/) -->
Jump host/Jump server without the jump, a.k.a Transparent SSH bastion
```
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
DMZ │
┌────────┐ │ ┌────────┐
│ homer │───▶╔═════════════════╗───▶│ host1 │ │
└────────┘ ║ ║ └────────┘
┌────────┐ ║ ║ ┌────────┐ │
│ bart │───▶║ sshportal ║───▶│ host2 │
└────────┘ ║ ║ └────────┘ │
┌────────┐ ║ ║ ┌────────┐
│ lisa │───▶╚═════════════════╝───▶│ host3 │ │
└────────┘ │ └────────┘
┌────────┐ ┌────────┐ │
│ ... │ │ │ ... │
└────────┘ └────────┘ │
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
```
<img src="https://raw.githubusercontent.com/moul/sshportal/master/.assets/bastion.jpg" width="50%">
## Features
Features include: independence of users and hosts, convenient user invite system, connecting to servers that don't support SSH keys, various levels of access, and many more. Easy to install, run and configure.
* Host management
* User management
* User Group management
* Host Group management
* Host Key management
* User Key management
* ACL management
* Connect to host using key or password
* Admin commands can be run directly or in an interactive shell
![Flow Diagram](https://raw.githubusercontent.com/moul/sshportal/master/.assets/flow-diagram.png)
## Usage
---
## Contents
<!-- toc -->
- [Installation and usage](#installation-and-usage)
- [Use cases](#use-cases)
- [Features and limitations](#features-and-limitations)
- [Docker](#docker)
- [Manual Install](#manual-install)
- [Backup / Restore](#backup--restore)
- [built-in shell](#built-in-shell)
- [Demo data](#demo-data)
- [Shell commands](#shell-commands)
- [Healthcheck](#healthcheck)
- [portal alias (.ssh/config)](#portal-alias-sshconfig)
- [Scaling](#scaling)
- [Under the hood](#under-the-hood)
- [Testing](#testing)
<!-- tocstop -->
---
## Installation and usage
Start the server
```console
$ sshportal
$ sshportal server
2017/11/13 10:58:35 Admin user created, use the user 'invite:BpLnfgDsc2WD8F2q' to associate a public key with this account
2017/11/13 10:58:35 SSH Server accepting connections on :2222
```
@@ -46,13 +54,15 @@ Link your SSH key with the admin account
```console
$ ssh localhost -p 2222 -l invite:BpLnfgDsc2WD8F2q
Welcome Administrator!
Welcome admin!
Your key is now associated with the user "admin@sshportal".
Shared connection to localhost closed.
$
```
If the association fails and you are promted for a password, verify that the host you're connecting from has a SSH key set up or generate one with ```ssh-keygen -t rsa```
Drop an interactive administrator shell
```console
@@ -80,28 +90,18 @@ List hosts
```console
config> host ls
ID | NAME | URL | KEY | PASS | GROUPS | COMMENT
+----+------+-------------------------+---------+------+--------+---------+
1 | foo | bart@foo.example.org:22 | default | | 1 |
ID | NAME | URL | KEY | PASS | GROUPS | COMMENT
+----+------+-------------------------+---------+------+---------+---------+
1 | foo | bart@foo.example.org:22 | default | | default |
Total: 1 hosts.
config>
```
Get the default key in authorized_keys format
Add the key to the server
```console
config> key inspect default
[...]
"PubKey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvUP/8FedyIe+a+RWU4KvJ1+iZwtWmY9czJubLwN4RcjKHQMzLqWC7pKZHAABCZjLJjVD/3Zb53jZwbh7mysAkocundMpvUL5+Yb4a8lDiflXkdXT9fZCx+ibJBk4jRnKLGIneSzVtFEerEwQKKnKQoCgPkZwCDaL/jHhDlOmAvxqAJrjiy42HXwppX2UuF8zujs6OKHRYJ/Q1vo0caa6/o1eoyXE9OrOwIk+IcAN3YIQi/B1BOlZOQBzHIZz83AFlD2TcPhyYcbxPyKGih84Zr3rQaaP1WiaiPqxzp3s5OhTLthc5XtCSLzmRSLvgC2eFdNhBDB5KLtO2khBkz5ID",
[...]
config>
```
Add this key to the server
```console
$ ssh bart@foo.example.org
> umask 077; mkdir -p .ssh; echo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvUP/8FedyIe+a+RWU4KvJ1+iZwtWmY9czJubLwN4RcjKHQMzLqWC7pKZHAABCZjLJjVD/3Zb53jZwbh7mysAkocundMpvUL5+Yb4a8lDiflXkdXT9fZCx+ibJBk4jRnKLGIneSzVtFEerEwQKKnKQoCgPkZwCDaL/jHhDlOmAvxqAJrjiy42HXwppX2UuF8zujs6OKHRYJ/Q1vo0caa6/o1eoyXE9OrOwIk+IcAN3YIQi/B1BOlZOQBzHIZz83AFlD2TcPhyYcbxPyKGih84Zr3rQaaP1WiaiPqxzp3s5OhTLthc5XtCSLzmRSLvgC2eFdNhBDB5KLtO2khBkz5ID >> .ssh/authorized_keys
$ ssh bart@foo.example.org "$(ssh localhost -p 2222 -l admin key setup default)"
$
```
Profit
@@ -113,18 +113,168 @@ bart@foo>
Invite friends
*This command doesn't create a user on the remote server, it only creates an account in the sshportal database.*
```console
config> user invite bob@example.com
User 2 created.
To associate this account with a key, use the following SSH user: 'invite-NfHK5a84jjJkwzDk'.
To associate this account with a key, use the following SSH user: 'invite:NfHK5a84jjJkwzDk'.
config>
```
## CLI
Demo gif:
![sshportal demo](https://github.com/moul/sshportal/raw/master/.assets/demo.gif)
sshportal embeds a configuration CLI.
---
By default, the configuration user is `admin`, (can be changed using `--config-user=<value>` when starting the server.
## Use cases
Used by educators to provide temporary access to students. [Feedback from a teacher](https://github.com/moul/sshportal/issues/64). The author is using it in one of his projects, *pathwar*, to dynamically configure hosts and users, so that he can give temporary accesses for educational purposes.
*vptech*, the vente-privee.com technical team (a group of over 6000 people) is using it internally to manage access to servers/routers, saving hours on configuration management and not having to share the configuration information.
There are companies who use a jump host to monitor connections at a single point.
A hosting company is using SSHportal for its “logging” feature, among the others. As every session is logged and introspectable, they have a detailed history of who performed which action. This company made its own contribution on the project, allowing the support of [more than 65.000 sessions in the database](https://github.com/moul/sshportal/pull/76).
The project has also received [multiple contributions from a security researcher](https://github.com/moul/sshportal/pulls?q=is%3Apr+author%3Asabban+sort%3Aupdated-desc) that made a thesis on quantum cryptography. This person uses SSHportal in their security-hardened hosting company.
If you need to invite multiple people to an event (hackathon, course, etc), the day before the event you can create multiple accounts at once, print the invite, and distribute the paper.
---
## Features and limitations
* Single autonomous binary (~10-20Mb) with no runtime dependencies (embeds ssh server and client)
* Portable / Cross-platform (regularly tested on linux and OSX/darwin)
* Store data in [Sqlite3](https://www.sqlite.org/) or [MySQL](https://www.mysql.com) (probably easy to add postgres, mssql thanks to gorm)
* Stateless -> horizontally scalable when using [MySQL](https://www.mysql.com) as the backend
* Connect to remote host using key or password
* Admin commands can be run directly or in an interactive shell
* Host management
* User management (invite, group, stats)
* Host Key management (create, remove, update, import)
* Automatic remote host key learning
* User Key management (multile keys per user)
* ACL management (acl+user-groups+host-groups)
* User roles (admin, trusted, standard, ...)
* User invitations (no more "give me your public ssh key please")
* Easy server installation (generate shell command to setup `authorized_keys`)
* Sensitive data encryption
* Session management (see active connections, history, stats, stop)
* Audit log (logging every user action)
* Record TTY Session (with [ttyrec](https://en.wikipedia.org/wiki/Ttyrec) format, use `ttyplay` for replay)
* Tunnels logging
* Host Keys verifications shared across users
* Healthcheck user (replying OK to any user)
* SSH compatibility
* ipv4 and ipv6 support
* [`scp`](https://linux.die.net/man/1/scp) support
* [`rsync`](https://linux.die.net/man/1/rsync) support
* [tunneling](https://www.ssh.com/ssh/tunneling/example) (local forward, remote forward, dynamic forward) support
* [`sftp`](https://www.ssh.com/ssh/sftp/) support
* [`ssh-agent`](https://www.ssh.com/ssh/agent) support
* [`X11 forwarding`](http://en.tldp.org/HOWTO/XDMCP-HOWTO/ssh.html) support
* Git support (can be used to easily use multiple user keys on GitHub, or access your own firewalled gitlab server)
* Do not require any SSH client modification or custom `.ssh/config`, works with every tested SSH programming libraries and every tested SSH clients
* SSH to non-SSH proxy
* [Telnet](https://www.ssh.com/ssh/telnet) support
**(Known) limitations**
* Does not work (yet?) with [`mosh`](https://mosh.org/)
* It is not possible for a user to access a host with the same name as the user. This is easily circumvented by changing the user name, especially since the most common use cases does not expose it.
* It is not possible access a host named `healthcheck` as this is a built in command.
---
## Docker
Docker is the recommended way to run sshportal.
An [automated build is setup on the Docker Hub](https://hub.docker.com/r/moul/sshportal/tags/).
```console
# Start a server in background
# mount `pwd` to persist the sqlite database file
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.10.0
# check logs (mandatory on first run to get the administrator invite token)
docker logs -f sshportal
```
The easier way to upgrade sshportal is to do the following:
```sh
# we consider you were using an old version and you want to use the new version v1.10.0
# stop and rename the last working container + backup the database
docker stop sshportal
docker rename sshportal sshportal_old
cp sshportal.db sshportal.db.bkp
# run the new version
docker run -p 2222:2222 -d --name=sshportal -v "$(pwd):$(pwd)" -w "$(pwd)" moul/sshportal:v1.10.0
# check the logs for migration or cross-version incompabitility errors
docker logs -f sshportal
```
Now you can test ssh-ing to sshportal to check if everything looks OK.
In case of problem, you can rollback to the latest working version with the latest working backup, using:
```sh
docker stop sshportal
docker rm sshportal
cp sshportal.db.bkp sshportal.db
docker rename sshportal_old sshportal
docker start sshportal
docker logs -f sshportal
```
---
## Manual Install
Get the latest version using GO.
```sh
GO111MODULE=on go get -u moul.io/sshportal
```
---
## Backup / Restore
sshportal embeds built-in backup/restore methods which basically import/export JSON objects:
```sh
# Backup
ssh portal config backup > sshportal.bkp
# Restore
ssh portal config restore < sshportal.bkp
```
This method is particularly useful as it should be resistant against future DB schema changes (expected during development phase).
I suggest you to be careful during this development phase, and use an additional backup method, for example:
```sh
# sqlite dump
sqlite3 sshportal.db .dump > sshportal.sql.bkp
# or just the immortal cp
cp sshportal.db sshportal.db.bkp
```
---
## built-in shell
`sshportal` embeds a configuration CLI.
By default, the configuration user is `admin`, (can be changed using `--config-user=<value>` when starting the server. The shell is also accessible through `ssh [username]@portal.example.org`.
Each commands can be run directly by using this syntax: `ssh admin@portal.example.org <command> [args]`:
@@ -134,62 +284,245 @@ ssh admin@portal.example.org host inspect toto
You can enter in interactive mode using this syntax: `ssh admin@portal.example.org`
![sshportal overview](https://raw.github.com/moul/sshportal/master/.assets/overview.png)
### Synopsis
---
## Demo data
The following servers are freely available, without external registration,
it makes it easier to quickly test `sshportal` without configuring your own servers to accept sshportal connections.
```
ssh portal host create new@sdf.org
ssh sdf@portal
ssh portal host create test@whoami.filippo.io
ssh whoami@portal
ssh portal host create test@chat.shazow.net
ssh chat@portal
```
---
## Shell commands
```sh
# acl management
acl help
acl create [-h] [--hostgroup=<value>...] [--usergroup=<value>...] [--pattern=<value>] [--comment=<value>] [--action=<value>] [--weight=value]
acl inspect [-h] <id> [<id> [<id>...]]
acl ls [-h]
acl rm [-h] <id> [<id> [<id>...]]
acl create [-h] [--hostgroup=HOSTGROUP...] [--usergroup=USERGROUP...] [--pattern=<value>] [--comment=<value>] [--action=<value>] [--weight=value]
acl inspect [-h] ACL...
acl ls [-h] [--latest] [--quiet]
acl rm [-h] ACL...
acl update [-h] [--comment=<value>] [--action=<value>] [--weight=<value>] [--assign-hostgroup=HOSTGROUP...] [--unassign-hostgroup=HOSTGROUP...] [--assign-usergroup=USERGROUP...] [--unassign-usergroup=USERGROUP...] ACL...
# config management
config help
config backup [-h] [--indent] [--decrypt]
config restore [-h] [--confirm] [--decrypt]
# event management
event help
event ls [-h] [--latest] [--quiet]
event inspect [-h] EVENT...
# host management
host help
host create [-h] [--name=<value>] [--password=<value>] [--fingerprint=<value>] [--comment=<value>] [--key=<value>] [--group=<value>] <user>[:<password>]@<host>[:<port>]
host inspect [-h] <id or name> [<id or name> [<id or name>...]]
host ls [-h]
host rm [-h] <id or name> [<id or name> [<id or name>...]]
host create [-h] [--name=<value>] [--password=<value>] [--comment=<value>] [--key=KEY] [--group=HOSTGROUP...] [--hop=HOST] [--logging=MODE] <username>[:<password>]@<host>[:<port>]
host inspect [-h] [--decrypt] HOST...
host ls [-h] [--latest] [--quiet]
host rm [-h] HOST...
host update [-h] [--name=<value>] [--comment=<value>] [--key=KEY] [--assign-group=HOSTGROUP...] [--unassign-group=HOSTGROUP...] [--logging-MODE] [--set-hop=HOST] [--unset-hop] HOST...
# hostgroup management
hostgroup help
hostgroup create [-h] [--name=<value>] [--comment=<value>]
hostgroup inspect [-h] <id or name> [<id or name> [<id or name>...]]
hostgroup ls [-h]
hostgroup rm [-h] <id or name> [<id or name> [<id or name>...]]
hostgroup inspect [-h] HOSTGROUP...
hostgroup ls [-h] [--latest] [--quiet]
hostgroup rm [-h] HOSTGROUP...
# key management
key help
key create [-h] [--name=<value>] [--type=<value>] [--length=<value>] [--comment=<value>]
key inspect [-h] <id or name> [<id or name> [<id or name>...]]
key ls [-h]
key rm [-h] <id or name> [<id or name> [<id or name>...]]
key import [-h] [--name=<value>] [--comment=<value>]
key inspect [-h] [--decrypt] KEY...
key ls [-h] [--latest] [--quiet]
key rm [-h] KEY...
key setup [-h] KEY
key show [-h] KEY
# session management
session help
session ls [-h] [--latest] [--quiet]
session inspect [-h] SESSION...
# user management
user help
user invite [-h] [--name=<value>] [--comment=<value>] [--group=<value>] <email>
user inspect [-h] <id or email> [<id or email> [<id or email>...]]
user ls [-h]
user rm [-h] <id or email> [<id or email> [<id or email>...]]
user invite [-h] [--name=<value>] [--comment=<value>] [--group=USERGROUP...] <email>
user inspect [-h] USER...
user ls [-h] [--latest] [--quiet]
user rm [-h] USER...
user update [-h] [--name=<value>] [--email=<value>] [--set-admin] [--unset-admin] [--assign-group=USERGROUP...] [--unassign-group=USERGROUP...] USER...
# usergroup management
usergroup help
hostgroup create [-h] [--name=<value>] [--comment=<value>]
usergroup inspect [-h] <id or name> [<id or name> [<id or name>...]]
usergroup ls [-h]
usergroup rm [-h] <id or name> [<id or name> [<id or name>...]]
usergroup create [-h] [--name=<value>] [--comment=<value>]
usergroup inspect [-h] USERGROUP...
usergroup ls [-h] [--latest] [--quiet]
usergroup rm [-h] USERGROUP...
# other
exit [-h]
help, h
info [-h]
version [-h]
```
## Install
---
Get the latest version using GO (recommended way):
## Healthcheck
```sh
go get -u github.com/moul/sshportal
By default, `sshportal` will return `OK` to anyone sshing using the `healthcheck` user without checking for authentication.
```console
$ ssh healthcheck@sshportal
OK
$
```
the `healtcheck` user can be changed using the `healthcheck-user` option.
---
Alternatively, you can run the built-in healthcheck helper (requiring no ssh client nor ssh key):
Usage: `sshportal healthcheck [--addr=host:port] [--wait] [--quiet]
```console
$ sshportal healthcheck --addr=localhost:2222; echo $?
$ 0
```
---
Wait for sshportal to be healthy, then connect
```console
$ sshportal healthcheck --wait && ssh sshportal -l admin
config>
```
---
## portal alias (.ssh/config)
Edit your `~/.ssh/config` file (create it first if needed)
```ini
Host portal
User admin
Port 2222 # portal port
HostName 127.0.0.1 # portal hostname
```
```bash
# you can now run a shell using this:
ssh portal
# instead of this:
ssh localhost -p 2222 -l admin
# or connect to hosts using this:
ssh hostname@portal
# instead of this:
ssh localhost -p 2222 -l hostname
```
---
## Scaling
`sshportal` is stateless but relies on a database to store configuration and logs.
By default, `sshportal` uses a local [sqlite](https://www.sqlite.org/) database which isn't scalable by design.
You can run multiple instances of `sshportal` sharing a same [MySQL](https://www.mysql.com) database, using `sshportal --db-conn=user:pass@host/dbname?parseTime=true --db-driver=mysql`.
![sshportal cluster with MySQL backend](https://raw.github.com/moul/sshportal/master/.assets/cluster-mysql.png)
See [examples/mysql](http://github.com/moul/sshportal/tree/master/examples/mysql).
---
## Under the hood
* Docker first (used in dev, tests, by the CI and in production)
* Backed by (see [dep graph](https://godoc.org/github.com/moul/sshportal?import-graph&hide=2)):
* SSH
* https://github.com/gliderlabs/ssh: SSH server made easy (well-designed golang library to build SSH servers)
* https://godoc.org/golang.org/x/crypto/ssh: both client and server SSH protocol and helpers
* Database
* https://github.com/jinzhu/gorm/: SQL orm
* https://github.com/go-gormigrate/gormigrate: Database migration system
* Built-in shell
* https://github.com/olekukonko/tablewriter: Ascii tables
* https://github.com/asaskevich/govalidator: Valide user inputs
* https://github.com/dustin/go-humanize: Human-friendly representation of technical data (time ago, bytes, ...)
* https://github.com/mgutz/ansi: Terminal color helpers
* https://github.com/urfave/cli: CLI flag parsing with subcommands support
![sshportal data model](https://raw.github.com/moul/sshportal/master/.assets/sql-schema.png)
---
## Testing
[Install golangci-lint](https://golangci-lint.run/usage/install/#local-installation) and run this in project root:
```
golangci-lint run
```
---
Perform integration tests
```
make integration
```
---
Perform unit tests
```
make unittest
```
---
## Contributors
### Code Contributors
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
<a href="https://github.com/moul/sshportal/graphs/contributors"><img src="https://opencollective.com/sshportal/contributors.svg?width=890&button=false" /></a>
### Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/sshportal/contribute)]
#### Individuals
<a href="https://opencollective.com/sshportal"><img src="https://opencollective.com/sshportal/individuals.svg?width=890"></a>
#### Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/sshportal/contribute)]
<a href="https://opencollective.com/sshportal/organization/0/website"><img src="https://opencollective.com/sshportal/organization/0/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/1/website"><img src="https://opencollective.com/sshportal/organization/1/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/2/website"><img src="https://opencollective.com/sshportal/organization/2/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/3/website"><img src="https://opencollective.com/sshportal/organization/3/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/4/website"><img src="https://opencollective.com/sshportal/organization/4/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/5/website"><img src="https://opencollective.com/sshportal/organization/5/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/6/website"><img src="https://opencollective.com/sshportal/organization/6/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/7/website"><img src="https://opencollective.com/sshportal/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/8/website"><img src="https://opencollective.com/sshportal/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/sshportal/organization/9/website"><img src="https://opencollective.com/sshportal/organization/9/avatar.svg"></a>
### Stargazers over time
[![Stargazers over time](https://starchart.cc/moul/sshportal.svg)](https://starchart.cc/moul/sshportal)

1
_config.yml Normal file
View File

@@ -0,0 +1 @@
theme: jekyll-theme-slate

40
acl.go
View File

@@ -1,40 +0,0 @@
package main
import "sort"
type ByWeight []ACL
func (a ByWeight) Len() int { return len(a) }
func (a ByWeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByWeight) Less(i, j int) bool { return a[i].Weight < a[j].Weight }
func CheckACLs(user User, host Host) (string, error) {
// shared ACLs between user and host
aclMap := map[uint]ACL{}
for _, userGroup := range user.Groups {
for _, userGroupACL := range userGroup.ACLs {
for _, hostGroup := range host.Groups {
for _, hostGroupACL := range hostGroup.ACLs {
if userGroupACL.ID == hostGroupACL.ID {
aclMap[userGroupACL.ID] = userGroupACL
}
}
}
}
}
// FIXME: add ACLs that match host pattern
// deny by default if no shared ACL
if len(aclMap) == 0 {
return "deny", nil // default action
}
// transofrm map to slice and sort it
acls := []ACL{}
for _, acl := range aclMap {
acls = append(acls, acl)
}
sort.Sort(ByWeight(acls))
return acls[0].Action, nil
}

View File

@@ -1,43 +0,0 @@
package main
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/jinzhu/gorm"
. "github.com/smartystreets/goconvey/convey"
)
func TestCheckACLs(t *testing.T) {
Convey("Testing CheckACLs", t, func() {
// create tmp dir
tempDir, err := ioutil.TempDir("", "sshportal")
So(err, ShouldBeNil)
defer os.RemoveAll(tempDir)
// create sqlite db
db, err := gorm.Open("sqlite3", filepath.Join(tempDir, "sshportal.db"))
db.LogMode(false)
So(dbInit(db), ShouldBeNil)
// create dummy objects
hostGroup, err := FindHostGroupByIdOrName(db, "default")
So(err, ShouldBeNil)
db.Create(&Host{Groups: []HostGroup{*hostGroup}})
//. load db
var (
hosts []Host
users []User
)
db.Preload("Groups").Preload("Groups.ACLs").Find(&hosts)
db.Preload("Groups").Preload("Groups.ACLs").Find(&users)
// test
action, err := CheckACLs(users[0], hosts[0])
So(err, ShouldBeNil)
So(action, ShouldEqual, "allow")
})
}

View File

@@ -1,49 +0,0 @@
package main
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"strings"
gossh "golang.org/x/crypto/ssh"
)
func NewSSHKey(keyType string, length uint) (*SSHKey, error) {
key := SSHKey{
Type: keyType,
Length: length,
}
// generate the private key
if keyType != "rsa" {
return nil, fmt.Errorf("key type not supported: %q", key.Type)
}
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
// convert priv key to x509 format
var pemKey = &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
}
buf := bytes.NewBufferString("")
if err = pem.Encode(buf, pemKey); err != nil {
return nil, err
}
key.PrivKey = buf.String()
// generte authorized-key formatted pubkey output
pub, err := gossh.NewPublicKey(&privateKey.PublicKey)
if err != nil {
return nil, err
}
key.PubKey = strings.TrimSpace(string(gossh.MarshalAuthorizedKey(pub)))
return &key, nil
}

412
db.go
View File

@@ -1,412 +0,0 @@
package main
import (
"fmt"
"log"
"net/url"
"strings"
"github.com/gliderlabs/ssh"
"github.com/jinzhu/gorm"
)
type SSHKey struct {
// FIXME: use uuid for ID
gorm.Model
Name string // FIXME: govalidator: min length 3, alphanum
Type string
Length uint
Fingerprint string
PrivKey string `sql:"size:10000;"`
PubKey string `sql:"size:10000;"`
Hosts []Host
Comment string
}
type Host struct {
// FIXME: use uuid for ID
gorm.Model
Name string `gorm:"size:63"` // FIXME: govalidator: min length 3, alphanum
Addr string
User string
Password string
SSHKey *SSHKey
SSHKeyID uint `gorm:"index"`
Groups []HostGroup `gorm:"many2many:host_host_groups;"`
Fingerprint string // FIXME: replace with hostkey ?
Comment string
}
type UserKey struct {
gorm.Model
Key []byte `sql:"size:10000;"`
UserID uint
User *User
Comment string
}
type User struct {
// FIXME: use uuid for ID
gorm.Model
IsAdmin bool
Email string // FIXME: govalidator: email
Name string // FIXME: govalidator: min length 3, alphanum
Keys []UserKey
Groups []UserGroup `gorm:"many2many:user_user_groups;"`
Comment string
InviteToken string
}
type UserGroup struct {
gorm.Model
Name string
Users []User `gorm:"many2many:user_user_groups;"`
ACLs []ACL `gorm:"many2many:user_group_acls;"`
Comment string
}
type HostGroup struct {
gorm.Model
Name string
Hosts []Host `gorm:"many2many:host_host_groups;"`
ACLs []ACL `gorm:"many2many:host_group_acls;"`
Comment string
}
type ACL struct {
gorm.Model
HostGroups []HostGroup `gorm:"many2many:host_group_acls;"`
UserGroups []UserGroup `gorm:"many2many:user_group_acls;"`
HostPattern string
Action string
Weight uint
Comment string
}
func dbInit(db *gorm.DB) error {
db.AutoMigrate(&User{})
db.AutoMigrate(&SSHKey{})
db.AutoMigrate(&Host{})
db.AutoMigrate(&UserKey{})
db.AutoMigrate(&UserGroup{})
db.AutoMigrate(&HostGroup{})
db.AutoMigrate(&ACL{})
// FIXME: check if indexes exist to avoid gorm warns
db.Exec(`CREATE UNIQUE INDEX uix_keys_name ON "ssh_keys"("name") WHERE ("deleted_at" IS NULL)`)
db.Exec(`CREATE UNIQUE INDEX uix_hosts_name ON "hosts"("name") WHERE ("deleted_at" IS NULL)`)
db.Exec(`CREATE UNIQUE INDEX uix_users_name ON "users"("email") WHERE ("deleted_at" IS NULL)`)
db.Exec(`CREATE UNIQUE INDEX uix_usergroups_name ON "user_groups"("name") WHERE ("deleted_at" IS NULL)`)
db.Exec(`CREATE UNIQUE INDEX uix_hostgroups_name ON "host_groups"("name") WHERE ("deleted_at" IS NULL)`)
// create default ssh key
var count uint
if err := db.Table("ssh_keys").Where("name = ?", "default").Count(&count).Error; err != nil {
return err
}
if count == 0 {
key, err := NewSSHKey("rsa", 2048)
if err != nil {
return err
}
key.Name = "default"
key.Comment = "created by sshportal"
if err := db.Create(&key).Error; err != nil {
return err
}
}
// create default host group
if err := db.Table("host_groups").Where("name = ?", "default").Count(&count).Error; err != nil {
return err
}
if count == 0 {
hostGroup := HostGroup{
Name: "default",
Comment: "created by sshportal",
}
if err := db.Create(&hostGroup).Error; err != nil {
return err
}
}
// create default user group
if err := db.Table("user_groups").Where("name = ?", "default").Count(&count).Error; err != nil {
return err
}
if count == 0 {
userGroup := UserGroup{
Name: "default",
Comment: "created by sshportal",
}
if err := db.Create(&userGroup).Error; err != nil {
return err
}
}
// create default acl
if err := db.Table("acls").Count(&count).Error; err != nil {
return err
}
if count == 0 {
var defaultUserGroup UserGroup
db.Where("name = ?", "default").First(&defaultUserGroup)
var defaultHostGroup HostGroup
db.Where("name = ?", "default").First(&defaultHostGroup)
acl := ACL{
UserGroups: []UserGroup{defaultUserGroup},
HostGroups: []HostGroup{defaultHostGroup},
Action: "allow",
//HostPattern: "",
//Weight: 0,
Comment: "created by sshportal",
}
if err := db.Create(&acl).Error; err != nil {
return err
}
}
// create admin user
var defaultUserGroup UserGroup
db.Where("name = ?", "default").First(&defaultUserGroup)
db.Table("users").Count(&count)
if count == 0 {
// if no admin, create an account for the first connection
user := User{
Name: "Administrator",
Email: "admin@sshportal",
Comment: "created by sshportal",
IsAdmin: true,
InviteToken: RandStringBytes(16),
Groups: []UserGroup{defaultUserGroup},
}
db.Create(&user)
log.Printf("Admin user created, use the user 'invite:%s' to associate a public key with this account", user.InviteToken)
}
// create host ssh key
if err := db.Table("ssh_keys").Where("name = ?", "host").Count(&count).Error; err != nil {
return err
}
if count == 0 {
key, err := NewSSHKey("rsa", 2048)
if err != nil {
return err
}
key.Name = "host"
key.Comment = "created by sshportal"
if err := db.Create(&key).Error; err != nil {
return err
}
}
return nil
}
func dbDemo(db *gorm.DB) error {
hostGroup, err := FindHostGroupByIdOrName(db, "default")
if err != nil {
return err
}
key, err := FindKeyByIdOrName(db, "default")
if err != nil {
return err
}
var (
host1 = Host{Name: "sdf", Addr: "sdf.org:22", User: "new", SSHKeyID: key.ID, Groups: []HostGroup{*hostGroup}}
host2 = Host{Name: "whoami", Addr: "whoami.filippo.io:22", User: "test", SSHKeyID: key.ID, Groups: []HostGroup{*hostGroup}}
host3 = Host{Name: "ssh-chat", Addr: "chat.shazow.net:22", User: "test", SSHKeyID: key.ID, Fingerprint: "MD5:e5:d5:d1:75:90:38:42:f6:c7:03:d7:d0:56:7d:6a:db", Groups: []HostGroup{*hostGroup}}
)
// FIXME: check if hosts exist to avoid `UNIQUE constraint` error
db.Create(&host1)
db.Create(&host2)
db.Create(&host3)
return nil
}
func RemoteHostFromSession(s ssh.Session, db *gorm.DB) (*Host, error) {
var host Host
db.Preload("SSHKey").Where("name = ?", s.User()).Find(&host)
if host.Name == "" {
// FIXME: add available hosts
return nil, fmt.Errorf("No such target: %q", s.User())
}
return &host, nil
}
func (host *Host) URL() string {
return fmt.Sprintf("%s@%s", host.User, host.Addr)
}
func NewHostFromURL(rawurl string) (*Host, error) {
if !strings.Contains(rawurl, "://") {
rawurl = "ssh://" + rawurl
}
u, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
host := Host{Addr: u.Host}
if !strings.Contains(host.Addr, ":") {
host.Addr += ":22" // add port if not present
}
host.User = "root" // default username
if u.User != nil {
password, _ := u.User.Password()
host.Password = password
host.User = u.User.Username()
}
return &host, nil
}
func (host *Host) Hostname() string {
return strings.Split(host.Addr, ":")[0]
}
// Host helpers
func FindHostByIdOrName(db *gorm.DB, query string) (*Host, error) {
var host Host
if err := db.Preload("Groups").Preload("SSHKey").Where("id = ?", query).Or("name = ?", query).First(&host).Error; err != nil {
return nil, err
}
return &host, nil
}
func FindHostsByIdOrName(db *gorm.DB, queries []string) ([]*Host, error) {
var hosts []*Host
for _, query := range queries {
host, err := FindHostByIdOrName(db, query)
if err != nil {
return nil, err
}
hosts = append(hosts, host)
}
return hosts, nil
}
// SSHKey helpers
func FindKeyByIdOrName(db *gorm.DB, query string) (*SSHKey, error) {
var key SSHKey
if err := db.Preload("Hosts").Where("id = ?", query).Or("name = ?", query).First(&key).Error; err != nil {
return nil, err
}
return &key, nil
}
func FindKeysByIdOrName(db *gorm.DB, queries []string) ([]*SSHKey, error) {
var keys []*SSHKey
for _, query := range queries {
key, err := FindKeyByIdOrName(db, query)
if err != nil {
return nil, err
}
keys = append(keys, key)
}
return keys, nil
}
// HostGroup helpers
func FindHostGroupByIdOrName(db *gorm.DB, query string) (*HostGroup, error) {
var hostGroup HostGroup
if err := db.Preload("ACLs").Preload("Hosts").Where("id = ?", query).Or("name = ?", query).First(&hostGroup).Error; err != nil {
return nil, err
}
return &hostGroup, nil
}
func FindHostGroupsByIdOrName(db *gorm.DB, queries []string) ([]*HostGroup, error) {
var hostGroups []*HostGroup
for _, query := range queries {
hostGroup, err := FindHostGroupByIdOrName(db, query)
if err != nil {
return nil, err
}
hostGroups = append(hostGroups, hostGroup)
}
return hostGroups, nil
}
// UserGroup heleprs
func FindUserGroupByIdOrName(db *gorm.DB, query string) (*UserGroup, error) {
var userGroup UserGroup
if err := db.Preload("ACLs").Preload("Users").Where("id = ?", query).Or("name = ?", query).First(&userGroup).Error; err != nil {
return nil, err
}
return &userGroup, nil
}
func FindUserGroupsByIdOrName(db *gorm.DB, queries []string) ([]*UserGroup, error) {
var userGroups []*UserGroup
for _, query := range queries {
userGroup, err := FindUserGroupByIdOrName(db, query)
if err != nil {
return nil, err
}
userGroups = append(userGroups, userGroup)
}
return userGroups, nil
}
// User helpers
func FindUserByIdOrEmail(db *gorm.DB, query string) (*User, error) {
var user User
if err := db.Preload("Groups").Preload("Keys").Where("id = ?", query).Or("email = ?", query).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
func FindUsersByIdOrEmail(db *gorm.DB, queries []string) ([]*User, error) {
var users []*User
for _, query := range queries {
user, err := FindUserByIdOrEmail(db, query)
if err != nil {
return nil, err
}
users = append(users, user)
}
return users, nil
}
// ACL helpers
func FindACLById(db *gorm.DB, query string) (*ACL, error) {
var acl ACL
if err := db.Preload("UserGroups").Preload("HostGroups").Where("id = ?", query).First(&acl).Error; err != nil {
return nil, err
}
return &acl, nil
}
func FindACLsById(db *gorm.DB, queries []string) ([]*ACL, error) {
var acls []*ACL
for _, query := range queries {
acl, err := FindACLById(db, query)
if err != nil {
return nil, err
}
acls = append(acls, acl)
}
return acls, nil
}
// UserKey helpers
func FindUserkeyById(db *gorm.DB, query string) (*UserKey, error) {
var userkey UserKey
if err := db.Preload("User").Where("id = ?", query).First(&userkey).Error; err != nil {
return nil, err
}
return &userkey, nil
}
func FindUserkeysById(db *gorm.DB, queries []string) ([]*UserKey, error) {
var userkeys []*UserKey
for _, query := range queries {
userkey, err := FindUserkeyById(db, query)
if err != nil {
return nil, err
}
userkeys = append(userkeys, userkey)
}
return userkeys, nil
}

128
depaware.txt Normal file
View File

@@ -0,0 +1,128 @@
moul.io/sshportal dependencies: (generated by github.com/tailscale/depaware)
github.com/anmitsu/go-shlex from github.com/gliderlabs/ssh+
github.com/asaskevich/govalidator from moul.io/sshportal/pkg/bastion+
github.com/cpuguy83/go-md2man/v2/md2man from github.com/urfave/cli
LD 💣 github.com/creack/pty from github.com/kr/pty
github.com/docker/docker/pkg/namesgenerator from moul.io/sshportal/pkg/bastion
github.com/docker/docker/pkg/random from github.com/docker/docker/pkg/namesgenerator
github.com/dustin/go-humanize from moul.io/sshportal/pkg/bastion
github.com/gliderlabs/ssh from moul.io/sshportal+
github.com/go-sql-driver/mysql from github.com/jinzhu/gorm/dialects/mysql+
github.com/jinzhu/gorm from gopkg.in/gormigrate.v1+
github.com/jinzhu/gorm/dialects/mysql from moul.io/sshportal
github.com/jinzhu/gorm/dialects/postgres from moul.io/sshportal
github.com/jinzhu/gorm/dialects/sqlite from moul.io/sshportal
github.com/jinzhu/inflection from github.com/jinzhu/gorm
LD github.com/kr/pty from moul.io/sshportal
github.com/lib/pq from github.com/jinzhu/gorm/dialects/postgres
github.com/lib/pq/hstore from github.com/jinzhu/gorm/dialects/postgres
github.com/lib/pq/oid from github.com/lib/pq
github.com/lib/pq/scram from github.com/lib/pq
💣 github.com/mattn/go-colorable from github.com/mgutz/ansi
💣 github.com/mattn/go-isatty from github.com/mattn/go-colorable
github.com/mattn/go-runewidth from github.com/olekukonko/tablewriter
💣 github.com/mattn/go-sqlite3 from github.com/jinzhu/gorm/dialects/sqlite
github.com/mgutz/ansi from moul.io/sshportal/pkg/bastion
github.com/olekukonko/tablewriter from moul.io/sshportal/pkg/bastion
github.com/pkg/errors from moul.io/sshportal/pkg/bastion
github.com/reiver/go-oi from github.com/reiver/go-telnet+
github.com/reiver/go-telnet from moul.io/sshportal/pkg/bastion
github.com/russross/blackfriday/v2 from github.com/cpuguy83/go-md2man/v2/md2man
github.com/sabban/bastion/pkg/logchannel from moul.io/sshportal/pkg/bastion
github.com/shurcooL/sanitized_anchor_name from github.com/russross/blackfriday/v2
github.com/urfave/cli from moul.io/sshportal+
gopkg.in/gormigrate.v1 from moul.io/sshportal/pkg/bastion
moul.io/srand from moul.io/sshportal
moul.io/sshportal/pkg/bastion from moul.io/sshportal
moul.io/sshportal/pkg/crypto from moul.io/sshportal/pkg/bastion
moul.io/sshportal/pkg/dbmodels from moul.io/sshportal/pkg/bastion+
golang.org/x/crypto/blowfish from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/chacha20poly1305 from crypto/tls
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from crypto/tls+
golang.org/x/crypto/ed25519 from golang.org/x/crypto/ssh
golang.org/x/crypto/hkdf from crypto/tls
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/ssh from github.com/gliderlabs/ssh+
golang.org/x/crypto/ssh/terminal from moul.io/sshportal/pkg/bastion
golang.org/x/net/dns/dnsmessage from net
D golang.org/x/net/route from net
golang.org/x/sys/cpu from golang.org/x/crypto/chacha20poly1305
LD golang.org/x/sys/unix from github.com/mattn/go-isatty+
W golang.org/x/sys/windows from golang.org/x/crypto/ssh/terminal
bufio from crypto/rand+
bytes from bufio+
container/list from crypto/tls
context from crypto/tls+
crypto from crypto/ecdsa+
crypto/aes from crypto/ecdsa+
crypto/cipher from crypto/aes+
crypto/des from crypto/tls+
crypto/dsa from crypto/x509+
crypto/ecdsa from crypto/tls+
crypto/ed25519 from crypto/tls+
crypto/elliptic from crypto/ecdsa+
crypto/hmac from crypto/tls+
crypto/md5 from crypto/tls+
crypto/rand from crypto/ed25519+
crypto/rc4 from crypto/tls+
crypto/rsa from crypto/tls+
crypto/sha1 from crypto/tls+
crypto/sha256 from crypto/tls+
crypto/sha512 from crypto/ecdsa+
crypto/subtle from crypto/aes+
crypto/tls from github.com/go-sql-driver/mysql+
crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509
database/sql from github.com/go-sql-driver/mysql+
database/sql/driver from database/sql+
encoding from encoding/json
encoding/asn1 from crypto/x509+
encoding/base64 from encoding/json+
encoding/binary from crypto/aes+
encoding/csv from github.com/olekukonko/tablewriter
encoding/hex from crypto/x509+
encoding/json from github.com/asaskevich/govalidator+
encoding/pem from crypto/tls+
errors from bufio+
flag from github.com/urfave/cli
fmt from crypto/tls+
go/ast from github.com/jinzhu/gorm
go/scanner from go/ast
go/token from go/ast+
hash from crypto+
html from github.com/asaskevich/govalidator+
io from bufio+
io/fs from crypto/rand+
io/ioutil from crypto/x509+
log from github.com/gliderlabs/ssh+
math from crypto/rsa+
math/big from crypto/dsa+
math/bits from crypto/md5+
math/rand from github.com/docker/docker/pkg/random+
net from crypto/tls+
net/url from crypto/x509+
os from crypto/rand+
LD os/exec from github.com/creack/pty+
os/user from github.com/lib/pq+
path from github.com/asaskevich/govalidator+
path/filepath from crypto/x509+
reflect from crypto/x509+
regexp from github.com/asaskevich/govalidator+
regexp/syntax from regexp
sort from database/sql+
strconv from crypto+
strings from bufio+
sync from context+
sync/atomic from context+
syscall from crypto/rand+
text/tabwriter from github.com/urfave/cli
text/template from github.com/urfave/cli
text/template/parse from text/template
time from context+
unicode from bytes+
unicode/utf16 from encoding/asn1+
unicode/utf8 from bufio+

View File

@@ -0,0 +1,24 @@
require "language/go"
class Sshportal < Formula
desc "sshportal: simple, fun and transparent SSH bastion"
homepage "https://github.com/moul/sshportal"
url "https://github.com/moul/sshportal/archive/v1.7.1.tar.gz"
sha256 "4611ae2f30cc595b2fb789bd0c92550533db6d4b63c638dd78cf85517b6aeaf0"
head "https://github.com/moul/sshportal.git"
depends_on "go" => :build
def install
ENV["GOPATH"] = buildpath
ENV["GOBIN"] = buildpath
(buildpath/"src/github.com/moul/sshportal").install Dir["*"]
system "go", "build", "-o", "#{bin}/sshportal", "-v", "github.com/moul/sshportal"
end
test do
output = shell_output(bin/"sshportal --version")
assert output.include? "sshportal version "
end
end

View File

@@ -0,0 +1,4 @@
FROM occitech/ssh-client
ENTRYPOINT ["/bin/sh", "-c"]
CMD ["/integration/_client.sh"]
COPY . /integration

View File

@@ -0,0 +1,10 @@
run:
docker-compose down
docker-compose up -d sshportal
docker-compose build client
docker-compose exec sshportal /bin/sshportal healthcheck --wait --quiet
docker-compose run client /integration/_client.sh
docker-compose down
build:
docker-compose build

79
examples/integration/_client.sh Executable file
View File

@@ -0,0 +1,79 @@
#!/bin/sh -e
mkdir -p ~/.ssh
cp /integration/client_test_rsa ~/.ssh/id_rsa
chmod -R 700 ~/.ssh
cat >~/.ssh/config <<EOF
Host sshportal
Port 2222
HostName sshportal
Host testserver
Port 2222
HostName testserver
Host *
StrictHostKeyChecking no
ControlMaster auto
SendEnv TEST_*
EOF
set -x
# login
ssh sshportal -l invite:integration
# hostgroup/usergroup/acl
ssh sshportal -l admin hostgroup create
ssh sshportal -l admin hostgroup create --name=hg1
ssh sshportal -l admin hostgroup create --name=hg2 --comment=test
ssh sshportal -l admin hostgroup inspect hg1 hg2
ssh sshportal -l admin hostgroup ls
ssh sshportal -l admin usergroup create
ssh sshportal -l admin usergroup create --name=ug1
ssh sshportal -l admin usergroup create --name=ug2 --comment=test
ssh sshportal -l admin usergroup inspect ug1 ug2
ssh sshportal -l admin usergroup ls
ssh sshportal -l admin acl create --ug=ug1 --ug=ug2 --hg=hg1 --hg=hg2 --comment=test --action=allow --weight=42
ssh sshportal -l admin acl inspect 2
ssh sshportal -l admin acl ls
# basic host create
ssh sshportal -l admin host create bob@example.org:1234
ssh sshportal -l admin host create test42
ssh sshportal -l admin host create --name=testtest --comment=test --password=test test@test.test
ssh sshportal -l admin host create --group=hg1 --group=hg2 hostwithgroups.org
ssh sshportal -l admin host inspect example test42 testtest hostwithgroups
ssh sshportal -l admin host update --assign-group=hg1 test42
ssh sshportal -l admin host update --unassign-group=hg1 test42
ssh sshportal -l admin host update --assign-group=hg1 test42
ssh sshportal -l admin host update --assign-group=hg2 --unassign-group=hg2 test42
ssh sshportal -l admin host ls
# backup/restore
ssh sshportal -l admin config backup --indent --ignore-events > backup-1
ssh sshportal -l admin config restore --confirm < backup-1
ssh sshportal -l admin config backup --indent --ignore-events > backup-2
(
cat backup-1 | grep -v '"date":' | grep -v 'tedAt":' > backup-1.clean
cat backup-2 | grep -v '"date":' | grep -v 'tedAt":' > backup-2.clean
set -xe
diff backup-1.clean backup-2.clean
)
if [ "$CIRCLECI" = "true" ]; then
echo "Strage behavior with cross-container communication on CircleCI, skipping some tests..."
else
# bastion
ssh sshportal -l admin host create --name=testserver toto@testserver:2222
out="$(ssh sshportal -l testserver echo hello | head -n 1)"
test "$out" = '{"User":"toto","Environ":null,"Command":["echo","hello"]}'
out="$(TEST_A=1 TEST_B=2 TEST_C=3 TEST_D=4 TEST_E=5 TEST_F=6 TEST_G=7 TEST_H=8 TEST_I=9 ssh sshportal -l testserver echo hello | head -n 1)"
test "$out" = '{"User":"toto","Environ":["TEST_A=1","TEST_B=2","TEST_C=3","TEST_D=4","TEST_E=5","TEST_F=6","TEST_G=7","TEST_H=8","TEST_I=9"],"Command":["echo","hello"]}'
fi
# TODO: test more cases (forwards, scp, sftp, interactive, pty, stdin, exit code, ...)

View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAxV0ds/oMuOw9QVLFgxaM0Js2IdJKiYLnmKq96IuZ/wMqMea3
qi1UfNBPUQ2CojwbJGTea8cA9J1Et+a6v1mL66YG8zyxmhdlKHm2KOMnUXSfWPNg
ZArXH7Uj4Nx1k/O1ujfQFAsYTx63kMqwq1lM9JrExLSdp/8D/zQAyF68c82w8UZH
aIpLZJkM/fgh0VJWiw65NYAzuIkJNBgZR8rEBQU7V3lCqFGcSJ88MoqIdVGy0I4b
GGpO9VppDTf+uYGYDthhXlV0nHM45neWL5hzFK6oqbLFLpsaUOY7C3kKv+8+B3lX
p3OfGVoFy7u3evro+yRQEMQ+myS5UBIHaI3qOwIDAQABAoIBABM7/vASV3kSNOoP
2gXrha+y4LStHOyH4HBFe5qVOF3c/hi85ntkTY6YcpJwoaGUAAUs+2w/ib1NMmxF
xT9ux68gkB7WdGyTCR3HttQHR0at+fWeSm+Vit+hNKzub1sK7lQGqnW5mxXi5Xrr
9gnM+y3/g1u0SoUb2lTdyZG9gdo7LnLElzRinraEqTJUowXkqzAhGf1A+Kgp2fkb
/+QP1oiK8QeOFOsITD2UwIVCBRwRl5TjjwfLQ4El6oAWNjcL1ZfSmQLiXZ7U8Smk
Cd+BI+6ZDLA43fBUGDjbg4+2dt2JoKNkS0FfqhCW+Z2A0+ClJ8pwuMqRz8XXaOYr
ONCqOPECgYEA/qyWxSUjEWMvN3tC/mZPEbwHP3m7mbR1KGwhZylWVCmEF7kVC6il
/ICQZUI9ekyGJZ/SKZKwxDe7oeV+vFsus/9FWC5wrp45Xm4kEUwsBr4bWvuNpVOq
jrKecY8NgPZS1X6Uc5BbpiE9/VF2gCdYVVCDXP1NfO2MDhkniXJQUEMCgYEAxmQl
3s/vih9rXllPZcWHafjnFcGU1AIiJD1c+8lAqwCZzm0Bt0Ex4s1t3lp0ew6YBVXN
yGy+BORxOC9FQGTlKZNk/S705+8iAVNc9Sy7XbgN3GY3eat7XYbNpGbQrjiyZ+7I
pdEnoHWQD4NFXHaVsXaVHcBFUovXKoes2PODeqkCgYEAoN/3Ucv2zgoAjqSfmkKY
mhRT48YLOroi9AjyRM95CCs9lRrGb5n2WH4COOTSHwpuByBhSv+uCBVIwqlNGMDk
zLFpZZ3YcoXiqYMb541dlljKwPt8673hVMkCi6uZFSkFBHY0YpgDPPtsxDOMjsHL
7ACzKq+cHlmUimdbcViz4S8CgYEAr2+sVYaHixsRtVNA9PxiLQIgR4rx8zEXw/hH
m5hyiUV0vaiDlewfEzMab0CKNK/JGx6vZQdUWbsxq7+Re8o9JDDlY0b854T+CzIO
x/iQj+XMzBPQBtXvt9sXSsRo0Uft7B6qbIeyhSCxDibFVWjAIzh70N1P8BkdYsyr
uwZMRFECgYEA5QuutlFLI7hMPdBQvsEhjdVwKAj7LvpNemgxDpEoMiQWWm51XzcP
IZjlCwl1UvIE0MxowtvNr5lQuGRN8/88Dajpq+W6eeTSCKi67nn0VZh13cQLKvoX
DRZ6nfC3iLnEYKK+KN/I3NY7JcSjHmW6V8WtrCYAi2D5Ns05XJAG6t8=
-----END RSA PRIVATE KEY-----

View File

@@ -0,0 +1,27 @@
version: '3.0'
services:
sshportal:
image: moul/sshportal
environment:
- SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN=integration
command: server --debug
depends_on:
- testserver
ports:
- 2222
testserver:
image: moul/sshportal
command: _test_server
ports:
- 2222
client:
build: .
depends_on:
- sshportal
- testserver
#volumes:
# - .:/integration
tty: true

View File

@@ -4,12 +4,16 @@ services:
sshportal:
build: ../..
restart: unless-stopped
environment:
SSHPORTAL_DB_DRIVER: mysql
SSHPORTAL_DATABASE_URL: "root:root@tcp(mysql:3306)/db?charset=utf8&parseTime=true&loc=Local"
SSHPORTAL_DEBUG: 1
depends_on:
mysql:
condition: service_healthy
links:
- mysql
command: --db-driver=mysql --debug --db-conn="root:root@tcp(mysql:3306)/db?charset=utf8&parseTime=true&loc=Local"
command: server
ports:
- 2222:2222

36
go.mod generated Normal file
View File

@@ -0,0 +1,36 @@
module moul.io/sshportal
require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/creack/pty v1.1.11 // indirect
github.com/docker/docker v20.10.12+incompatible
github.com/dustin/go-humanize v1.0.0
github.com/gliderlabs/ssh v0.3.3
github.com/go-gormigrate/gormigrate/v2 v2.0.0
github.com/kr/pty v1.1.8
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-runewidth v0.0.12 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1
github.com/reiver/go-oi v1.0.0
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sabban/bastion v0.0.0-20180110125408-b9d3c9b1f4d3
github.com/smartystreets/goconvey v1.7.2
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
github.com/urfave/cli v1.22.5
golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed // indirect
golang.org/x/tools v0.1.10
gorm.io/driver/mysql v1.2.3
gorm.io/driver/postgres v1.2.3
gorm.io/driver/sqlite v1.2.6
gorm.io/gorm v1.22.5
moul.io/srand v1.6.1
)
go 1.14

338
go.sum generated Normal file
View File

@@ -0,0 +1,338 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ=
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc h1:VRRKCwnzqk8QCaRC4os14xoKDdbHqqlJtJA0oc1ZAjg=
github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/docker/docker v20.10.12+incompatible h1:CEeNmFM0QZIsJCZKMkZx0ZcahTiewkrgiwfYD+dfl1U=
github.com/docker/docker v20.10.12+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/gliderlabs/ssh v0.3.3 h1:mBQ8NiOgDkINJrZtoizkC3nDNYgSaWtxyem6S2XHBtA=
github.com/gliderlabs/ssh v0.3.3/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
github.com/go-gormigrate/gormigrate/v2 v2.0.0 h1:e2A3Uznk4viUC4UuemuVgsNnvYZyOA8B3awlYk3UioU=
github.com/go-gormigrate/gormigrate/v2 v2.0.0/go.mod h1:YuVJ+D/dNt4HWrThTBnjgZuRbt7AuwINeg4q52ZE3Jw=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk=
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
github.com/jackc/pgconn v1.6.4/go.mod h1:w2pne1C2tZgP+TvjqLpOigGzNqjBgQW9dUw/4Chex78=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.10.1 h1:DzdIHIjG1AxGwoEEqS+mGsURyjt4enSmqzACXvVzOT8=
github.com/jackc/pgconn v1.10.1/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.0.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns=
github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0=
github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po=
github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
github.com/jackc/pgtype v1.4.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.9.0 h1:/SH1RxEtltvJgsDqp3TbiTFApD3mey3iygpuEGeuBXk=
github.com/jackc/pgtype v1.9.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA=
github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o=
github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
github.com/jackc/pgx/v4 v4.8.1/go.mod h1:4HOLxrl8wToZJReD04/yB20GDwf4KBYETvlHciCnwW0=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.14.0 h1:TgdrmgnM7VY72EuSQzBbBd4JA1RLqJolrw9nQVZABVc=
github.com/jackc/pgx/v4 v4.14.0/go.mod h1:jT3ibf/A0ZVCp89rtCIN0zCJxcE74ypROmHEZYsG/j8=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 h1:+/+DxvQaYifJ+grD4klzrS5y+KJXldn/2YTl5JG+vZ8=
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/reiver/go-oi v1.0.0 h1:nvECWD7LF+vOs8leNGV/ww+F2iZKf3EYjYZ527turzM=
github.com/reiver/go-oi v1.0.0/go.mod h1:RrDBct90BAhoDTxB1fenZwfykqeGvhI6LsNfStJoEkI=
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e h1:quuzZLi72kkJjl+f5AQ93FMcadG19WkS7MO6TXFOSas=
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e/go.mod h1:+5vNVvEWwEIx86DB9Ke/+a5wBI464eDRo3eF0LcfpWg=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sabban/bastion v0.0.0-20180110125408-b9d3c9b1f4d3 h1:yxUGvEatvDMO6gkhwx82Va+Czdyui9LiCw6a5YB/2f8=
github.com/sabban/bastion v0.0.0-20180110125408-b9d3c9b1f4d3/go.mod h1:1Q04m7wmv/IMoZU9t8UkH+n9McWn4i3H9v9LnMgqloo=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502 h1:34icjjmqJ2HPjrSuJYEkdZ+0ItmGQAQ75cRHIiftIyE=
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab h1:lnZ4LoV0UMdibeCUfIB2a4uFwRu491WX/VB2reB8xNc=
golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0=
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.0.1/go.mod h1:KtqSthtg55lFp3S5kUXqlGaelnWpKitn4k1xZTnoiPw=
gorm.io/driver/mysql v1.2.3 h1:cZqzlOfg5Kf1VIdLC1D9hT6Cy9BgxhExLj/2tIgUe7Y=
gorm.io/driver/mysql v1.2.3/go.mod h1:qsiz+XcAyMrS6QY+X3M9R6b/lKM1imKmcuK9kac5LTo=
gorm.io/driver/postgres v1.0.0/go.mod h1:wtMFcOzmuA5QigNsgEIb7O5lhvH1tHAF1RbWmLWV4to=
gorm.io/driver/postgres v1.2.3 h1:f4t0TmNMy9gh3TU2PX+EppoA6YsgFnyq8Ojtddb42To=
gorm.io/driver/postgres v1.2.3/go.mod h1:pJV6RgYQPG47aM1f0QeOzFH9HxQc8JcmAgjRCgS0wjs=
gorm.io/driver/sqlite v1.1.1/go.mod h1:hm2olEcl8Tmsc6eZyxYSeznnsDaMqamBvEXLNtBg4cI=
gorm.io/driver/sqlite v1.2.6 h1:SStaH/b+280M7C8vXeZLz/zo9cLQmIGwwj3cSj7p6l4=
gorm.io/driver/sqlite v1.2.6/go.mod h1:gyoX0vHiiwi0g49tv+x2E7l8ksauLK0U/gShcdUsjWY=
gorm.io/driver/sqlserver v1.0.2 h1:FzxAlw0/7hntMzSiNfotpYCo9Lz8dqWQGdmCGqIiFGo=
gorm.io/driver/sqlserver v1.0.2/go.mod h1:gb0Y9QePGgqjzrVyTQUZeh9zkd5v0iz71cM1B4ZycEY=
gorm.io/gorm v1.9.19/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.20.0/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.22.3/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
gorm.io/gorm v1.22.4/go.mod h1:1aeVC+pe9ZmvKZban/gW4QPra7PRoTEssyc922qCAkk=
gorm.io/gorm v1.22.5 h1:lYREBgc02Be/5lSCTuysZZDb6ffL2qrat6fg9CFbvXU=
gorm.io/gorm v1.22.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
moul.io/srand v1.6.1 h1:SJ335F+54ivLdlH7wH52Rtyv0Ffos6DpsF5wu3ZVMXU=
moul.io/srand v1.6.1/go.mod h1:P2uaZB+GFstFNo8sEj6/U8FRV1n25kD0LLckFpJ+qvc=

73
healthcheck.go Normal file
View File

@@ -0,0 +1,73 @@
package main
import (
"bytes"
"fmt"
"log"
"net"
"strings"
"time"
"github.com/urfave/cli"
gossh "golang.org/x/crypto/ssh"
)
// perform a healthcheck test without requiring an ssh client or an ssh key (used for Docker's HEALTHCHECK)
func healthcheck(addr string, wait, quiet bool) error {
cfg := gossh.ClientConfig{
User: "healthcheck",
HostKeyCallback: func(hostname string, remote net.Addr, key gossh.PublicKey) error { return nil },
Auth: []gossh.AuthMethod{gossh.Password("healthcheck")},
}
if wait {
for {
if err := healthcheckOnce(addr, cfg, quiet); err != nil {
if !quiet {
log.Printf("error: %v", err)
}
time.Sleep(time.Second)
continue
}
return nil
}
}
if err := healthcheckOnce(addr, cfg, quiet); err != nil {
if quiet {
return cli.NewExitError("", 1)
}
return err
}
return nil
}
func healthcheckOnce(addr string, config gossh.ClientConfig, quiet bool) error {
client, err := gossh.Dial("tcp", addr, &config)
if err != nil {
return err
}
session, err := client.NewSession()
if err != nil {
return err
}
defer func() {
if err := session.Close(); err != nil {
if !quiet {
log.Printf("failed to close session: %v", err)
}
}
}()
var b bytes.Buffer
session.Stdout = &b
if err := session.Run(""); err != nil {
return err
}
stdout := strings.TrimSpace(b.String())
if stdout != "OK" {
return fmt.Errorf("invalid stdout: %q expected 'OK'", stdout)
}
return nil
}

View File

@@ -0,0 +1,22 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

21
helm/sshportal/Chart.yaml Normal file
View File

@@ -0,0 +1,21 @@
apiVersion: v2
name: sshportal
description: A Helm chart for SSHPortal on Kubernetes
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application.
appVersion: 1.10.0

View File

@@ -0,0 +1,33 @@
1. Get the admin invitation token (only on first install):
export INVITE=$(kubectl --namespace sshportal logs -l "app.kubernetes.io/name={{ include "sshportal.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" | grep -Eo "invite:[a-zA-Z0-9]+")
2. Get the service IP and Port:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "sshportal.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "sshportal.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "sshportal.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "sshportal.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 2222:{{ .Values.service.port }}
{{- end }}
3. Enroll your SSH public key:
{{- if contains "NodePort" .Values.service.type }}
ssh $NODE_IP -p $NODE_PORT -l $INVITE
{{- else if contains "LoadBalancer" .Values.service.type }}
ssh $SERVICE_IP -p {{ .Values.service.port }} -l $INVITE
{{- else if contains "ClusterIP" .Values.service.type }}
ssh localhost -p 2222 -l $INVITE
{{- end }}
4. Configure your {{ include "sshportal.name" . }} install:
{{- if contains "NodePort" .Values.service.type }}
ssh admin@$NODE_IP -p $NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
ssh admin@$SERVICE_IP -p {{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
ssh admin@localhost -p 2222
{{- end }}

View File

@@ -0,0 +1,63 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "sshportal.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "sshportal.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "sshportal.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Common labels
*/}}
{{- define "sshportal.labels" -}}
helm.sh/chart: {{ include "sshportal.chart" . }}
{{ include "sshportal.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{/*
Selector labels
*/}}
{{- define "sshportal.selectorLabels" -}}
app.kubernetes.io/name: {{ include "sshportal.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{/*
Create the name of the service account to use
*/}}
{{- define "sshportal.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{ default (include "sshportal.fullname" .) .Values.serviceAccount.name }}
{{- else -}}
{{ default "default" .Values.serviceAccount.name }}
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,69 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "sshportal.fullname" . }}
labels:
{{- include "sshportal.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "sshportal.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "sshportal.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:v{{ .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: ssh
containerPort: 2222
protocol: TCP
livenessProbe:
exec:
command:
- sshportal
- healthcheck
- --quiet
readinessProbe:
exec:
command:
- sshportal
- healthcheck
- --quiet
resources:
{{- toYaml .Values.resources | nindent 12 }}
env:
{{- if .Values.mysql.enabled }}
- name: SSHPORTAL_DATABASE_URL
value: {{ .Values.mysql.user }}:{{ .Values.mysql.password }}@tcp({{ .Values.mysql.server }}:{{ .Values.mysql.port }})/{{ .Values.mysql.database }}?charset=utf8&parseTime=true&loc=Local
- name: SSHPORTAL_DB_DRIVER
value: mysql
{{- end }}
{{- if .Values.debug}}
- name: SSHPORTAL_DEBUG
value: "1"
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,21 @@
{{- if .Values.mysql.enabled }}
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "sshportal.fullname" . }}
labels:
{{- include "sshportal.labels" . | nindent 4 }}
spec:
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "sshportal.fullname" . }}
metrics:
- type: Resource
resource:
name: cpu
targetAverageUtilization: {{ .Values.autoscaling.cpuTarget }}
{{- end }}

View File

@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "sshportal.fullname" . }}
annotations:
{{- toYaml .Values.service.annotations | nindent 4 }}
labels:
{{- include "sshportal.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: 2222
protocol: TCP
name: ssh
selector:
{{- include "sshportal.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "sshportal.fullname" . }}-test-connection"
labels:
{{ include "sshportal.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test-success
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "sshportal.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

119
helm/sshportal/values.yaml Normal file
View File

@@ -0,0 +1,119 @@
# Default values for sshportal.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
## Enable SSHPortal debug mode
##
debug: false
## SSH Portal Docker image
##
image:
repository: moul/sshportal
pullPolicy: IfNotPresent
## Reference to one or more secrets to be used when pulling images
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
##
imagePullSecrets: []
## Provide a name in place of sshportal for `app:` labels
##
nameOverride: ""
## Provide a name to substitute for the full names of resources
##
fullnameOverride: ""
## PodSecurityContext holds pod-level security attributes.
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
##
podSecurityContext: {}
# fsGroup: 2000
## SecurityContext holds container-level security attributes.
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
##
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
## Service
##
service:
## Configure additional annotations for SSHPortal service
##
annotations: {}
# service.beta.kubernetes.io/openstack-internal-load-balancer: "true"
## Service type, one of
## NodePort, ClusterIP, LoadBalancer
##
type: LoadBalancer
## Port to expose on the service
##
port: 22
## Define resources requests and limits
## ref: https://kubernetes.io/docs/user-guide/compute-resources/
##
resources: {}
# requests:
# cpu: 100m
# memory: 128Mi
# limits:
# cpu: 2
# memory: 2Gi
## Mysql/MariaDB configuration for HA
##
mysql:
enabled: false
## Database user
##
user: sshportal
## Database password
##
password: change_me
## Database name
##
database: sshportal
## Database server FQDN or IP
##
server: mariadb-mariadb-galera
## Database port
##
port: 3306
## Define which Nodes the Pods are scheduled on.
## ref: https://kubernetes.io/docs/user-guide/node-selection/
##
nodeSelector: {}
## The pod's tolerations.
## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
##
tolerations: []
## Assign custom affinity rules
## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/
##
affinity: {}
## HPA support, require `mysql.enable: true`
## This section enables sshportal to autoscale based on metrics.
##
autoscaling:
maxReplicas: 4
minReplicas: 2
cpuTarget: 60

11
internal/tools/tools.go Normal file
View File

@@ -0,0 +1,11 @@
// +build tools
package tools
import (
// required by depaware
_ "github.com/tailscale/depaware/depaware"
// required by goimports
_ "golang.org/x/tools/cover"
)

297
main.go
View File

@@ -1,226 +1,115 @@
package main
package main // import "moul.io/sshportal"
import (
"errors"
"fmt"
"log"
"math/rand"
"os"
"path"
"strings"
"github.com/gliderlabs/ssh"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/urfave/cli"
gossh "golang.org/x/crypto/ssh"
"moul.io/srand"
)
var version = "0.0.1"
type sshportalContextKey string
var (
userContextKey = sshportalContextKey("user")
messageContextKey = sshportalContextKey("message")
errorContextKey = sshportalContextKey("error")
// GitTag will be overwritten automatically by the build system
GitTag = "n/a"
// GitSha will be overwritten automatically by the build system
GitSha = "n/a"
)
func main() {
rand.Seed(srand.MustSecure())
app := cli.NewApp()
app.Name = path.Base(os.Args[0])
app.Author = "Manfred Touron"
app.Version = version
app.Email = "https://github.com/moul/sshportal"
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "bind-address, b",
EnvVar: "SSHPORTAL_BIND",
Value: ":2222",
Usage: "SSH server bind address",
},
cli.BoolFlag{
Name: "demo",
Usage: "*unsafe* - demo mode: accept all connections",
},
cli.StringFlag{
Name: "db-driver",
Value: "sqlite3",
Usage: "GORM driver (sqlite3, mysql)",
},
cli.StringFlag{
Name: "db-conn",
Value: "./sshportal.db",
Usage: "GORM connection string",
},
cli.BoolFlag{
Name: "debug, D",
Usage: "Display debug information",
},
cli.StringFlag{
Name: "config-user",
Usage: "SSH user that spawns a configuration shell",
Value: "admin",
app.Version = GitTag + " (" + GitSha + ")"
app.Email = "https://moul.io/sshportal"
app.Commands = []cli.Command{
{
Name: "server",
Usage: "Start sshportal server",
Action: func(c *cli.Context) error {
if err := ensureLogDirectory(c.String("logs-location")); err != nil {
return err
}
cfg, err := parseServerConfig(c)
if err != nil {
return err
}
return server(cfg)
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "bind-address, b",
EnvVar: "SSHPORTAL_BIND",
Value: ":2222",
Usage: "SSH server bind address",
},
cli.StringFlag{
Name: "db-driver",
EnvVar: "SSHPORTAL_DB_DRIVER",
Value: "sqlite3",
Usage: "GORM driver (sqlite3)",
},
cli.StringFlag{
Name: "db-conn",
EnvVar: "SSHPORTAL_DATABASE_URL",
Value: "./sshportal.db",
Usage: "GORM connection string",
},
cli.BoolFlag{
Name: "debug, D",
EnvVar: "SSHPORTAL_DEBUG",
Usage: "Display debug information",
},
cli.StringFlag{
Name: "aes-key",
EnvVar: "SSHPORTAL_AES_KEY",
Usage: "Encrypt sensitive data in database (length: 16, 24 or 32)",
},
cli.StringFlag{
Name: "logs-location",
EnvVar: "SSHPORTAL_LOGS_LOCATION",
Value: "./log",
Usage: "Store user session files",
},
cli.DurationFlag{
Name: "idle-timeout",
Value: 0,
Usage: "Duration before an inactive connection is timed out (0 to disable)",
},
cli.StringFlag{
Name: "acl-check-cmd",
EnvVar: "SSHPORTAL_ACL_CHECK_CMD",
Usage: "Execute external command to check ACL",
},
},
}, {
Name: "healthcheck",
Action: func(c *cli.Context) error { return healthcheck(c.String("addr"), c.Bool("wait"), c.Bool("quiet")) },
Flags: []cli.Flag{
cli.StringFlag{
Name: "addr, a",
Value: "localhost:2222",
Usage: "sshportal server address",
},
cli.BoolFlag{
Name: "wait, w",
Usage: "Loop indefinitely until sshportal is ready",
},
cli.BoolFlag{
Name: "quiet, q",
Usage: "Do not print errors, if any",
},
},
}, {
Name: "_test_server",
Hidden: true,
Action: testServer,
},
}
app.Action = server
if err := app.Run(os.Args); err != nil {
log.Fatalf("error: %v", err)
}
}
func server(c *cli.Context) error {
db, err := gorm.Open(c.String("db-driver"), c.String("db-conn"))
if err != nil {
return err
}
defer db.Close()
if c.Bool("debug") {
db.LogMode(true)
}
if err := dbInit(db); err != nil {
return err
}
ssh.Handle(func(s ssh.Session) {
currentUser := s.Context().Value(userContextKey).(User)
log.Printf("New connection: sshUser=%q remote=%q local=%q command=%q dbUser=id:%q,email:%s", s.User(), s.RemoteAddr(), s.LocalAddr(), s.Command(), currentUser.ID, currentUser.Email)
if err := s.Context().Value(errorContextKey); err != nil {
fmt.Fprintf(s, "error: %v\n", err)
return
}
if msg := s.Context().Value(messageContextKey); msg != nil {
fmt.Fprint(s, msg.(string))
}
switch username := s.User(); {
case username == c.String("config-user"):
if !currentUser.IsAdmin {
fmt.Fprintf(s, "You are not an administrator, permission denied.\n")
return
}
if err := shell(c, s, s.Command(), db); err != nil {
fmt.Fprintf(s, "error: %v\n", err)
}
case strings.HasPrefix(username, "invite:"):
return
default:
host, err := RemoteHostFromSession(s, db)
if err != nil {
fmt.Fprintf(s, "error: %v\n", err)
// FIXME: print available hosts
return
}
// load up-to-date objects
// FIXME: cache them or try not to load them
var tmpUser User
if err := db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", currentUser.ID).First(&tmpUser).Error; err != nil {
fmt.Fprintf(s, "error: %v\n", err)
return
}
var tmpHost Host
if err := db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", host.ID).First(&tmpHost).Error; err != nil {
fmt.Fprintf(s, "error: %v\n", err)
return
}
action, err := CheckACLs(tmpUser, tmpHost)
if err != nil {
fmt.Fprintf(s, "error: %v\n", err)
return
}
switch action {
case "allow":
if err := proxy(s, host); err != nil {
fmt.Fprintf(s, "error: %v\n", err)
}
case "deny":
fmt.Fprintf(s, "You don't have permission to that host.\n")
default:
fmt.Fprintf(s, "error: %v\n", err)
}
}
})
opts := []ssh.Option{}
if c.Bool("demo") {
if c.Bool("demo") {
if err := dbDemo(db); err != nil {
return err
}
}
}
opts = append(opts, ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
var (
userKey UserKey
user User
username = ctx.User()
)
// lookup user by key
db.Where("key = ?", key.Marshal()).First(&userKey)
if userKey.UserID > 0 {
db.Where("id = ?", userKey.UserID).First(&user)
if strings.HasPrefix(username, "invite:") {
ctx.SetValue(errorContextKey, fmt.Errorf("invites are only supported for ney SSH keys; your ssh key is already associated with the user %q.", user.Email))
}
ctx.SetValue(userContextKey, user)
return true
}
// handle invite "links"
if strings.HasPrefix(username, "invite:") {
inputToken := strings.Split(username, ":")[1]
if len(inputToken) == 16 {
db.Where("invite_token = ?", inputToken).First(&user)
}
if user.ID > 0 {
userKey = UserKey{
UserID: user.ID,
Key: key.Marshal(),
Comment: "created by sshportal",
}
db.Create(&userKey)
// token is only usable once
user.InviteToken = ""
db.Update(&user)
ctx.SetValue(messageContextKey, fmt.Sprintf("Welcome %s!\n\nYour key is now associated with the user %q.\n", user.Name, user.Email))
ctx.SetValue(userContextKey, user)
} else {
ctx.SetValue(userContextKey, User{Name: "Anonymous"})
ctx.SetValue(errorContextKey, errors.New("your token is invalid or expired"))
}
return true
}
// fallback
ctx.SetValue(errorContextKey, errors.New("unknown ssh key"))
ctx.SetValue(userContextKey, User{Name: "Anonymous"})
return true
}))
opts = append(opts, func(srv *ssh.Server) error {
key, err := FindKeyByIdOrName(db, "host")
if err != nil {
return err
}
signer, err := gossh.ParsePrivateKey([]byte(key.PrivKey))
if err != nil {
return err
}
srv.AddHostKey(signer)
return nil
})
log.Printf("SSH Server accepting connections on %s", c.String("bind-address"))
return ssh.ListenAndServe(c.String("bind-address"), nil, opts...)
}

120
pkg/bastion/acl.go Normal file
View File

@@ -0,0 +1,120 @@
package bastion
import (
"context"
"encoding/json"
"fmt"
"log"
"os/exec"
"sort"
"strings"
"time"
"moul.io/sshportal/pkg/dbmodels"
)
// ACLHookTimeout is timeout for external ACL hook execution
const ACLHookTimeout = 2 * time.Second
type byWeight []*dbmodels.ACL
func (a byWeight) Len() int { return len(a) }
func (a byWeight) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byWeight) Less(i, j int) bool { return a[i].Weight < a[j].Weight }
func checkACLs(user dbmodels.User, host dbmodels.Host, aclCheckCmd string) string {
currentTime := time.Now()
// shared ACLs between user and host
aclMap := map[uint]*dbmodels.ACL{}
for _, userGroup := range user.Groups {
for _, userGroupACL := range userGroup.ACLs {
for _, hostGroup := range host.Groups {
for _, hostGroupACL := range hostGroup.ACLs {
if userGroupACL.ID == hostGroupACL.ID {
if (userGroupACL.Inception == nil || currentTime.After(*userGroupACL.Inception)) &&
(userGroupACL.Expiration == nil || currentTime.Before(*userGroupACL.Expiration)) {
aclMap[userGroupACL.ID] = userGroupACL
}
}
}
}
}
}
// FIXME: add ACLs that match host pattern
// if no shared ACL then execute ACLs hook if it exists and return its result
if len(aclMap) == 0 {
action, err := checkACLsHook(aclCheckCmd, string(dbmodels.ACLActionDeny), user, host)
if err != nil {
log.Println(err)
}
return action
}
// transform map to slice and sort it
acls := make([]*dbmodels.ACL, 0, len(aclMap))
for _, acl := range aclMap {
acls = append(acls, acl)
}
sort.Sort(byWeight(acls))
action, err := checkACLsHook(aclCheckCmd, acls[0].Action, user, host)
if err != nil {
log.Println(err)
}
return action
}
// checkACLsHook executes external command to check ACL and passes following parameters:
// $1 - SSH Portal `action` (`allow` or `deny`)
// $2 - User as JSON string
// $3 - Host as JSON string
// External program has to return `allow` or `deny` in stdout.
// In case of any error function returns `action`.
func checkACLsHook(aclCheckCmd string, action string, user dbmodels.User, host dbmodels.Host) (string, error) {
if aclCheckCmd == "" {
return action, nil
}
ctx, cancel := context.WithTimeout(context.Background(), ACLHookTimeout)
defer cancel()
jsonUser, err := json.Marshal(user)
if err != nil {
return action, err
}
jsonHost, err := json.Marshal(host)
if err != nil {
return action, err
}
args := []string{
action,
string(jsonUser),
string(jsonHost),
}
cmd := exec.CommandContext(ctx, aclCheckCmd, args...)
out, err := cmd.Output()
if err != nil {
return action, err
}
if ctx.Err() == context.DeadlineExceeded {
return action, fmt.Errorf("external ACL hook command timed out")
}
outStr := strings.TrimSuffix(string(out), "\n")
switch outStr {
case string(dbmodels.ACLActionAllow):
return string(dbmodels.ACLActionAllow), nil
case string(dbmodels.ACLActionDeny):
return string(dbmodels.ACLActionDeny), nil
default:
return action, fmt.Errorf("acl-check-cmd wrong output '%s'", outStr)
}
}

47
pkg/bastion/acl_test.go Normal file
View File

@@ -0,0 +1,47 @@
package bastion // import "moul.io/sshportal/pkg/bastion"
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
. "github.com/smartystreets/goconvey/convey"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"moul.io/sshportal/pkg/dbmodels"
)
func TestCheckACLs(t *testing.T) {
Convey("Testing CheckACLs", t, func(c C) {
// create tmp dir
tempDir, err := ioutil.TempDir("", "sshportal")
c.So(err, ShouldBeNil)
defer func() {
c.So(os.RemoveAll(tempDir), ShouldBeNil)
}()
// create sqlite db
db, err := gorm.Open(sqlite.Open(filepath.Join(tempDir, "sshportal.db")), &gorm.Config{})
c.So(err, ShouldBeNil)
c.So(DBInit(db), ShouldBeNil)
// create dummy objects
var hostGroup dbmodels.HostGroup
err = dbmodels.HostGroupsByIdentifiers(db, []string{"default"}).First(&hostGroup).Error
c.So(err, ShouldBeNil)
db.Create(&dbmodels.Host{Groups: []*dbmodels.HostGroup{&hostGroup}})
//. load db
var (
hosts []dbmodels.Host
users []dbmodels.User
)
db.Preload("Groups").Preload("Groups.ACLs").Find(&hosts)
db.Preload("Groups").Preload("Groups.ACLs").Find(&users)
// test
action := checkACLs(users[0], hosts[0], "")
c.So(action, ShouldEqual, dbmodels.ACLActionAllow)
})
}

684
pkg/bastion/dbinit.go Normal file
View File

@@ -0,0 +1,684 @@
package bastion // import "moul.io/sshportal/pkg/bastion"
import (
"fmt"
"io/ioutil"
"log"
"math/rand"
"os"
"os/user"
"strings"
"time"
gormigrate "github.com/go-gormigrate/gormigrate/v2"
gossh "golang.org/x/crypto/ssh"
"gorm.io/gorm"
"moul.io/sshportal/pkg/crypto"
"moul.io/sshportal/pkg/dbmodels"
)
func DBInit(db *gorm.DB) error {
log.SetOutput(ioutil.Discard)
log.SetOutput(os.Stderr)
m := gormigrate.New(db, gormigrate.DefaultOptions, []*gormigrate.Migration{
{
ID: "1",
Migrate: func(tx *gorm.DB) error {
type Setting struct {
gorm.Model
Name string `gorm:"index:uix_settings_name,unique"`
Value string
}
return tx.AutoMigrate(&Setting{})
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable("settings")
},
}, {
ID: "2",
Migrate: func(tx *gorm.DB) error {
type SSHKey struct {
gorm.Model
Name string
Type string
Length uint
Fingerprint string
PrivKey string `sql:"size:5000"`
PubKey string `sql:"size:1000"`
Hosts []*dbmodels.Host `gorm:"ForeignKey:SSHKeyID"`
Comment string
}
return tx.AutoMigrate(&SSHKey{})
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable("ssh_keys")
},
}, {
ID: "3",
Migrate: func(tx *gorm.DB) error {
type Host struct {
gorm.Model
Name string `gorm:"size:32"`
Addr string
User string
Password string
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
SSHKeyID uint `gorm:"index"`
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
Fingerprint string
Comment string
}
return tx.AutoMigrate(&Host{})
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable("hosts")
},
}, {
ID: "4",
Migrate: func(tx *gorm.DB) error {
type UserKey struct {
gorm.Model
Key []byte `sql:"size:1000"`
UserID uint ``
User *dbmodels.User `gorm:"ForeignKey:UserID"`
Comment string
}
return tx.AutoMigrate(&UserKey{})
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable("user_keys")
},
}, {
ID: "5",
Migrate: func(tx *gorm.DB) error {
type User struct {
gorm.Model
IsAdmin bool
Email string
Name string
Keys []*dbmodels.UserKey `gorm:"ForeignKey:UserID"`
Groups []*dbmodels.UserGroup `gorm:"many2many:user_user_groups;"`
Comment string
InviteToken string
}
return tx.AutoMigrate(&User{})
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable("users")
},
}, {
ID: "6",
Migrate: func(tx *gorm.DB) error {
type UserGroup struct {
gorm.Model
Name string
Users []*dbmodels.User `gorm:"many2many:user_user_groups;"`
ACLs []*dbmodels.ACL `gorm:"many2many:user_group_acls;"`
Comment string
}
return tx.AutoMigrate(&UserGroup{})
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable("user_groups")
},
}, {
ID: "7",
Migrate: func(tx *gorm.DB) error {
type HostGroup struct {
gorm.Model
Name string
Hosts []*dbmodels.Host `gorm:"many2many:host_host_groups;"`
ACLs []*dbmodels.ACL `gorm:"many2many:host_group_acls;"`
Comment string
}
return tx.AutoMigrate(&HostGroup{})
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable("host_groups")
},
}, {
ID: "8",
Migrate: func(tx *gorm.DB) error {
type ACL struct {
gorm.Model
HostGroups []*dbmodels.HostGroup `gorm:"many2many:host_group_acls;"`
UserGroups []*dbmodels.UserGroup `gorm:"many2many:user_group_acls;"`
HostPattern string
Action string
Weight uint
Comment string
}
return tx.AutoMigrate(&ACL{})
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable("acls")
},
}, {
ID: "9",
Migrate: func(tx *gorm.DB) error {
if err := tx.Migrator().DropIndex(&dbmodels.Setting{}, "uix_settings_name"); err != nil {
return err
}
return tx.Migrator().CreateIndex(&dbmodels.Setting{}, "uix_settings_name")
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropIndex(&dbmodels.Setting{}, "uix_settings_name")
},
}, {
ID: "10",
Migrate: func(tx *gorm.DB) error {
if err := tx.Migrator().DropIndex(&dbmodels.SSHKey{}, "uix_keys_name"); err != nil {
return err
}
return tx.Migrator().CreateIndex(&dbmodels.SSHKey{}, "uix_keys_name")
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropIndex(&dbmodels.SSHKey{}, "uix_keys_name")
},
}, {
ID: "11",
Migrate: func(tx *gorm.DB) error {
if err := tx.Migrator().DropIndex(&dbmodels.Host{}, "uix_hosts_name"); err != nil {
return err
}
return tx.Migrator().CreateIndex(&dbmodels.Host{}, "uix_hosts_name")
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropIndex(&dbmodels.Host{}, "uix_hosts_name")
},
}, {
ID: "12",
Migrate: func(tx *gorm.DB) error {
if err := tx.Migrator().DropIndex(&dbmodels.User{}, "uix_users_name"); err != nil {
return err
}
return tx.Migrator().CreateIndex(&dbmodels.User{}, "uix_users_name")
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropIndex(&dbmodels.User{}, "uix_users_name")
},
}, {
ID: "13",
Migrate: func(tx *gorm.DB) error {
if err := tx.Migrator().DropIndex(&dbmodels.UserGroup{}, "uix_usergroups_name"); err != nil {
return err
}
return tx.Migrator().CreateIndex(&dbmodels.UserGroup{}, "uix_usergroups_name")
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropIndex(&dbmodels.UserGroup{}, "uix_usergroups_name")
},
}, {
ID: "14",
Migrate: func(tx *gorm.DB) error {
if err := tx.Migrator().DropIndex(&dbmodels.HostGroup{}, "uix_hostgroups_name"); err != nil {
return err
}
return tx.Migrator().CreateIndex(&dbmodels.HostGroup{}, "uix_hostgroups_name")
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropIndex(&dbmodels.HostGroup{}, "uix_hostgroups_name")
},
}, {
ID: "15",
Migrate: func(tx *gorm.DB) error {
type UserRole struct {
gorm.Model
Name string `valid:"required,length(1|32),unix_user"`
Users []*dbmodels.User `gorm:"many2many:user_user_roles"`
}
return tx.AutoMigrate(&UserRole{})
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable("user_roles")
},
}, {
ID: "16",
Migrate: func(tx *gorm.DB) error {
type User struct {
gorm.Model
IsAdmin bool
Roles []*dbmodels.UserRole `gorm:"many2many:user_user_roles"`
Email string `valid:"required,email"`
Name string `valid:"required,length(1|32),unix_user"`
Keys []*dbmodels.UserKey `gorm:"ForeignKey:UserID"`
Groups []*dbmodels.UserGroup `gorm:"many2many:user_user_groups;"`
Comment string `valid:"optional"`
InviteToken string `valid:"optional,length(10|60)"`
}
return tx.AutoMigrate(&User{})
},
Rollback: func(tx *gorm.DB) error {
return fmt.Errorf("not implemented")
},
}, {
ID: "17",
Migrate: func(tx *gorm.DB) error {
return tx.Create(&dbmodels.UserRole{Name: "admin"}).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.Where("name = ?", "admin").Unscoped().Delete(&dbmodels.UserRole{}).Error
},
}, {
ID: "18",
Migrate: func(tx *gorm.DB) error {
var adminRole dbmodels.UserRole
if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
return err
}
var users []*dbmodels.User
if err := db.Preload("Roles").Where("is_admin = ?", true).Find(&users).Error; err != nil {
return err
}
for _, user := range users {
user.Roles = append(user.Roles, &adminRole)
if err := tx.Save(user).Error; err != nil {
return err
}
}
return nil
},
Rollback: func(tx *gorm.DB) error {
return fmt.Errorf("not implemented")
},
}, {
ID: "19",
Migrate: func(tx *gorm.DB) error {
type User struct {
gorm.Model
Roles []*dbmodels.UserRole `gorm:"many2many:user_user_roles"`
Email string `valid:"required,email"`
Name string `valid:"required,length(1|32),unix_user"`
Keys []*dbmodels.UserKey `gorm:"ForeignKey:UserID"`
Groups []*dbmodels.UserGroup `gorm:"many2many:user_user_groups;"`
Comment string `valid:"optional"`
InviteToken string `valid:"optional,length(10|60)"`
}
return tx.AutoMigrate(&User{})
},
Rollback: func(tx *gorm.DB) error {
return fmt.Errorf("not implemented")
},
}, {
ID: "20",
Migrate: func(tx *gorm.DB) error {
return tx.Create(&dbmodels.UserRole{Name: "listhosts"}).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.Where("name = ?", "listhosts").Unscoped().Delete(&dbmodels.UserRole{}).Error
},
}, {
ID: "21",
Migrate: func(tx *gorm.DB) error {
type Session struct {
gorm.Model
StoppedAt time.Time `valid:"optional"`
Status string `valid:"required"`
User *dbmodels.User `gorm:"ForeignKey:UserID"`
Host *dbmodels.Host `gorm:"ForeignKey:HostID"`
UserID uint `valid:"optional"`
HostID uint `valid:"optional"`
ErrMsg string `valid:"optional"`
Comment string `valid:"optional"`
}
return tx.AutoMigrate(&Session{})
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable("sessions")
},
}, {
ID: "22",
Migrate: func(tx *gorm.DB) error {
type Event struct {
gorm.Model
Author *dbmodels.User `gorm:"ForeignKey:AuthorID"`
AuthorID uint `valid:"optional"`
Domain string `valid:"required"`
Action string `valid:"required"`
Entity string `valid:"optional"`
Args []byte `sql:"size:10000" valid:"optional,length(1|10000)"`
}
return tx.AutoMigrate(&Event{})
},
Rollback: func(tx *gorm.DB) error {
return tx.Migrator().DropTable("events")
},
}, {
ID: "23",
Migrate: func(tx *gorm.DB) error {
type UserKey struct {
gorm.Model
Key []byte `sql:"size:1000" valid:"required,length(1|1000)"`
AuthorizedKey string `sql:"size:1000" valid:"required,length(1|1000)"`
UserID uint ``
User *dbmodels.User `gorm:"ForeignKey:UserID"`
Comment string `valid:"optional"`
}
return tx.AutoMigrate(&UserKey{})
},
Rollback: func(tx *gorm.DB) error {
return fmt.Errorf("not implemented")
},
}, {
ID: "24",
Migrate: func(tx *gorm.DB) error {
var userKeys []*dbmodels.UserKey
if err := db.Find(&userKeys).Error; err != nil {
return err
}
for _, userKey := range userKeys {
key, err := gossh.ParsePublicKey(userKey.Key)
if err != nil {
return err
}
userKey.AuthorizedKey = string(gossh.MarshalAuthorizedKey(key))
if err := db.Model(userKey).Updates(userKey).Error; err != nil {
return err
}
}
return nil
},
Rollback: func(tx *gorm.DB) error {
return fmt.Errorf("not implemented")
},
}, {
ID: "25",
Migrate: func(tx *gorm.DB) error {
type Host struct {
gorm.Model
Name string `gorm:"size:32" valid:"required,length(1|32),unix_user"`
Addr string `valid:"required"`
User string `valid:"optional"`
Password string `valid:"optional"`
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
SSHKeyID uint `gorm:"index"`
HostKey []byte `sql:"size:1000" valid:"optional"`
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
Fingerprint string `valid:"optional"`
Comment string `valid:"optional"`
}
return tx.AutoMigrate(&Host{})
},
Rollback: func(tx *gorm.DB) error {
return fmt.Errorf("not implemented")
},
}, {
ID: "26",
Migrate: func(tx *gorm.DB) error {
type Session struct {
gorm.Model
StoppedAt *time.Time `sql:"index" valid:"optional"`
Status string `valid:"required"`
User *dbmodels.User `gorm:"ForeignKey:UserID"`
Host *dbmodels.Host `gorm:"ForeignKey:HostID"`
UserID uint `valid:"optional"`
HostID uint `valid:"optional"`
ErrMsg string `valid:"optional"`
Comment string `valid:"optional"`
}
return tx.AutoMigrate(&Session{})
},
Rollback: func(tx *gorm.DB) error {
return fmt.Errorf("not implemented")
},
}, {
ID: "27",
Migrate: func(tx *gorm.DB) error {
var sessions []*dbmodels.Session
if err := db.Find(&sessions).Error; err != nil {
return err
}
for _, session := range sessions {
if session.StoppedAt != nil && session.StoppedAt.IsZero() {
if err := db.Model(session).Updates(map[string]interface{}{"stopped_at": nil}).Error; err != nil {
return err
}
}
}
return nil
},
Rollback: func(tx *gorm.DB) error {
return fmt.Errorf("not implemented")
},
}, {
ID: "28",
Migrate: func(tx *gorm.DB) error {
type Host struct {
gorm.Model
Name string `gorm:"size:32"`
Addr string
User string
Password string
URL string
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
SSHKeyID uint `gorm:"index"`
HostKey []byte `sql:"size:1000"`
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
Comment string
}
return tx.AutoMigrate(&Host{})
},
Rollback: func(tx *gorm.DB) error {
return fmt.Errorf("not implemented")
},
}, {
ID: "29",
Migrate: func(tx *gorm.DB) error {
type Host struct {
gorm.Model
Name string `gorm:"size:32"`
Addr string
User string
Password string
URL string
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
SSHKeyID uint `gorm:"index"`
HostKey []byte `sql:"size:1000"`
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
Comment string
Hop *dbmodels.Host
HopID uint
}
return tx.AutoMigrate(&Host{})
},
Rollback: func(tx *gorm.DB) error {
return fmt.Errorf("not implemented")
},
}, {
ID: "30",
Migrate: func(tx *gorm.DB) error {
type Host struct {
gorm.Model
Name string `gorm:"size:32"`
Addr string
User string
Password string
URL string
SSHKey *dbmodels.SSHKey `gorm:"ForeignKey:SSHKeyID"`
SSHKeyID uint `gorm:"index"`
HostKey []byte `sql:"size:10000"`
Groups []*dbmodels.HostGroup `gorm:"many2many:host_host_groups;"`
Comment string
Hop *dbmodels.Host
Logging string
HopID uint
}
return tx.AutoMigrate(&Host{})
},
Rollback: func(tx *gorm.DB) error { return fmt.Errorf("not implemented") },
}, {
ID: "31",
Migrate: func(tx *gorm.DB) error {
return tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Model(&dbmodels.Host{}).Updates(&dbmodels.Host{Logging: "everything"}).Error
},
Rollback: func(tx *gorm.DB) error { return fmt.Errorf("not implemented") },
}, {
ID: "32",
Migrate: func(tx *gorm.DB) error {
type ACL struct {
gorm.Model
HostGroups []*dbmodels.HostGroup `gorm:"many2many:host_group_acls;"`
UserGroups []*dbmodels.UserGroup `gorm:"many2many:user_group_acls;"`
HostPattern string `valid:"optional"`
Action string `valid:"required"`
Weight uint ``
Comment string `valid:"optional"`
Inception *time.Time
Expiration *time.Time
}
return tx.AutoMigrate(&ACL{})
},
Rollback: func(tx *gorm.DB) error { return fmt.Errorf("not implemented") },
},
})
if err := m.Migrate(); err != nil {
return err
}
dbmodels.NewEvent("system", "migrated").Log(db)
// create default ssh key
var count int64
if err := db.Table("ssh_keys").Where("name = ?", "default").Count(&count).Error; err != nil {
return err
}
if count == 0 {
key, err := crypto.NewSSHKey("ed25519", 1)
if err != nil {
return err
}
key.Name = "default"
key.Comment = "created by sshportal"
if err := db.Create(&key).Error; err != nil {
return err
}
}
// create default host group
if err := db.Table("host_groups").Where("name = ?", "default").Count(&count).Error; err != nil {
return err
}
if count == 0 {
hostGroup := dbmodels.HostGroup{
Name: "default",
Comment: "created by sshportal",
}
if err := db.Create(&hostGroup).Error; err != nil {
return err
}
}
// create default user group
if err := db.Table("user_groups").Where("name = ?", "default").Count(&count).Error; err != nil {
return err
}
if count == 0 {
userGroup := dbmodels.UserGroup{
Name: "default",
Comment: "created by sshportal",
}
if err := db.Create(&userGroup).Error; err != nil {
return err
}
}
// create default acl
if err := db.Table("acls").Count(&count).Error; err != nil {
return err
}
if count == 0 {
var defaultUserGroup dbmodels.UserGroup
db.Where("name = ?", "default").First(&defaultUserGroup)
var defaultHostGroup dbmodels.HostGroup
db.Where("name = ?", "default").First(&defaultHostGroup)
acl := dbmodels.ACL{
UserGroups: []*dbmodels.UserGroup{&defaultUserGroup},
HostGroups: []*dbmodels.HostGroup{&defaultHostGroup},
Action: "allow",
//HostPattern: "",
//Weight: 0,
Comment: "created by sshportal",
}
if err := db.Create(&acl).Error; err != nil {
return err
}
}
// create admin user
var defaultUserGroup dbmodels.UserGroup
db.Where("name = ?", "default").First(&defaultUserGroup)
if err := db.Table("users").Count(&count).Error; err != nil {
return err
}
if count == 0 {
// if no admin, create an account for the first connection
inviteToken := randStringBytes(16)
if os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN") != "" {
inviteToken = os.Getenv("SSHPORTAL_DEFAULT_ADMIN_INVITE_TOKEN")
}
var adminRole dbmodels.UserRole
if err := db.Where("name = ?", "admin").First(&adminRole).Error; err != nil {
return err
}
var username string
if currentUser, err := user.Current(); err == nil {
username = currentUser.Username
}
if username == "" {
username = os.Getenv("USER")
}
username = strings.ToLower(username)
if username == "" {
username = "admin" // fallback username
}
user := dbmodels.User{
Name: username,
Email: fmt.Sprintf("%s@localhost", username),
Comment: "created by sshportal",
Roles: []*dbmodels.UserRole{&adminRole},
InviteToken: inviteToken,
Groups: []*dbmodels.UserGroup{&defaultUserGroup},
}
if err := db.Create(&user).Error; err != nil {
return err
}
log.Printf("info 'admin' user created, use the user 'invite:%s' to associate a public key with this account", user.InviteToken)
}
// create host ssh key
if err := db.Table("ssh_keys").Where("name = ?", "host").Count(&count).Error; err != nil {
return err
}
if count == 0 {
key, err := crypto.NewSSHKey("ed25519", 1)
if err != nil {
return err
}
key.Name = "host"
key.Comment = "created by sshportal"
if err := db.Create(&key).Error; err != nil {
return err
}
}
// close unclosed connections
return db.Table("sessions").Where("status = ?", "active").Updates(&dbmodels.Session{
Status: string(dbmodels.SessionStatusClosed),
ErrMsg: "sshportal was halted while the connection was still active",
}).Error
}
func randStringBytes(n int) string {
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}

71
pkg/bastion/logtunnel.go Normal file
View File

@@ -0,0 +1,71 @@
package bastion // import "moul.io/sshportal/pkg/bastion"
import (
"encoding/binary"
"errors"
"io"
"log"
"syscall"
"time"
"golang.org/x/crypto/ssh"
)
type logTunnel struct {
host string
channel ssh.Channel
writer io.WriteCloser
}
type logTunnelForwardData struct {
DestinationHost string
DestinationPort uint32
SourceHost string
SourcePort uint32
}
func writeHeader(fd io.Writer, length int) {
t := time.Now()
tv := syscall.NsecToTimeval(t.UnixNano())
if err := binary.Write(fd, binary.LittleEndian, int32(tv.Sec)); err != nil {
log.Printf("failed to write log header: %v", err)
}
if err := binary.Write(fd, binary.LittleEndian, tv.Usec); err != nil {
log.Printf("failed to write log header: %v", err)
}
if err := binary.Write(fd, binary.LittleEndian, int32(length)); err != nil {
log.Printf("failed to write log header: %v", err)
}
}
func newLogTunnel(channel ssh.Channel, writer io.WriteCloser, host string) io.ReadWriteCloser {
return &logTunnel{
host: host,
channel: channel,
writer: writer,
}
}
func (l *logTunnel) Read(data []byte) (int, error) {
return 0, errors.New("logTunnel.Read is not implemented")
}
func (l *logTunnel) Write(data []byte) (int, error) {
writeHeader(l.writer, len(data)+len(l.host+": "))
if _, err := l.writer.Write([]byte(l.host + ": ")); err != nil {
log.Printf("failed to write log: %v", err)
}
if _, err := l.writer.Write(data); err != nil {
log.Printf("failed to write log: %v", err)
}
return l.channel.Write(data)
}
func (l *logTunnel) Close() error {
l.writer.Close()
return l.channel.Close()
}

264
pkg/bastion/session.go Normal file
View File

@@ -0,0 +1,264 @@
package bastion // import "moul.io/sshportal/pkg/bastion"
import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"time"
"github.com/gliderlabs/ssh"
"github.com/pkg/errors"
"github.com/sabban/bastion/pkg/logchannel"
gossh "golang.org/x/crypto/ssh"
)
type sessionConfig struct {
Addr string
LogsLocation string
ClientConfig *gossh.ClientConfig
LoggingMode string
}
func multiChannelHandler(conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context, configs []sessionConfig, sessionID uint) error {
var lastClient *gossh.Client
switch newChan.ChannelType() {
case "session":
lch, lreqs, err := newChan.Accept()
// TODO: defer clean closer
if err != nil {
// TODO: trigger event callback
return nil
}
// go through all the hops
for _, config := range configs {
var client *gossh.Client
if lastClient == nil {
client, err = gossh.Dial("tcp", config.Addr, config.ClientConfig)
} else {
rconn, err := lastClient.Dial("tcp", config.Addr)
if err != nil {
return err
}
ncc, chans, reqs, err := gossh.NewClientConn(rconn, config.Addr, config.ClientConfig)
if err != nil {
return err
}
client = gossh.NewClient(ncc, chans, reqs)
}
if err != nil {
lch.Close() // fix #56
return err
}
defer func() { _ = client.Close() }()
lastClient = client
}
rch, rreqs, err := lastClient.OpenChannel("session", []byte{})
if err != nil {
return err
}
user := conn.User()
actx := ctx.Value(authContextKey).(*authContext)
username := actx.user.Name
// pipe everything
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1], user, username, sessionID, newChan)
case "direct-tcpip":
lch, lreqs, err := newChan.Accept()
// TODO: defer clean closer
if err != nil {
// TODO: trigger event callback
return nil
}
// go through all the hops
for _, config := range configs {
var client *gossh.Client
if lastClient == nil {
client, err = gossh.Dial("tcp", config.Addr, config.ClientConfig)
} else {
rconn, err := lastClient.Dial("tcp", config.Addr)
if err != nil {
return err
}
ncc, chans, reqs, err := gossh.NewClientConn(rconn, config.Addr, config.ClientConfig)
if err != nil {
return err
}
client = gossh.NewClient(ncc, chans, reqs)
}
if err != nil {
lch.Close()
return err
}
defer func() { _ = client.Close() }()
lastClient = client
}
d := logTunnelForwardData{}
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
return err
}
rch, rreqs, err := lastClient.OpenChannel("direct-tcpip", newChan.ExtraData())
if err != nil {
return err
}
user := conn.User()
actx := ctx.Value(authContextKey).(*authContext)
username := actx.user.Name
// pipe everything
return pipe(lreqs, rreqs, lch, rch, configs[len(configs)-1], user, username, sessionID, newChan)
default:
if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil {
log.Printf("failed to reject chan: %v", err)
}
return nil
}
}
func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel, sessConfig sessionConfig, user string, username string, sessionID uint, newChan gossh.NewChannel) error {
defer func() {
_ = lch.Close()
_ = rch.Close()
}()
errch := make(chan error, 1)
quit := make(chan string, 1)
channeltype := newChan.ChannelType()
var logWriter io.WriteCloser = newDiscardWriteCloser()
if sessConfig.LoggingMode != "disabled" {
filename := filepath.Join(sessConfig.LogsLocation, fmt.Sprintf("%s-%s-%s-%d-%s", user, username, channeltype, sessionID, time.Now().Format(time.RFC3339)))
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0440)
if err != nil {
return errors.Wrap(err, "open log file")
}
defer func() {
_ = f.Close()
}()
log.Printf("Session %v is recorded in %v", channeltype, filename)
logWriter = f
}
if channeltype == "session" {
switch sessConfig.LoggingMode {
case "input":
wrappedrch := logchannel.New(rch, logWriter)
go func(quit chan string) {
_, _ = io.Copy(lch, rch)
quit <- "rch"
}(quit)
go func(quit chan string) {
_, _ = io.Copy(wrappedrch, lch)
quit <- "lch"
}(quit)
default: // everything, disabled
wrappedlch := logchannel.New(lch, logWriter)
go func(quit chan string) {
_, _ = io.Copy(wrappedlch, rch)
quit <- "rch"
}(quit)
go func(quit chan string) {
_, _ = io.Copy(rch, lch)
quit <- "lch"
}(quit)
}
}
if channeltype == "direct-tcpip" {
d := logTunnelForwardData{}
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
return err
}
wrappedlch := newLogTunnel(lch, logWriter, d.SourceHost)
wrappedrch := newLogTunnel(rch, logWriter, d.DestinationHost)
go func(quit chan string) {
_, _ = io.Copy(wrappedlch, rch)
quit <- "rch"
}(quit)
go func(quit chan string) {
_, _ = io.Copy(wrappedrch, lch)
quit <- "lch"
}(quit)
}
go func(quit chan string) {
for req := range lreqs {
b, err := rch.SendRequest(req.Type, req.WantReply, req.Payload)
if req.Type == "exec" {
wrappedlch := logchannel.New(lch, logWriter)
req.Payload = append(req.Payload, []byte("\n")...)
if _, err := wrappedlch.LogWrite(req.Payload); err != nil {
log.Printf("failed to write log: %v", err)
}
}
if err != nil {
errch <- err
}
if err2 := req.Reply(b, nil); err2 != nil {
errch <- err2
}
}
quit <- "lreqs"
}(quit)
go func(quit chan string) {
for req := range rreqs {
b, err := lch.SendRequest(req.Type, req.WantReply, req.Payload)
if err != nil {
errch <- err
}
if err2 := req.Reply(b, nil); err2 != nil {
errch <- err2
}
}
quit <- "rreqs"
}(quit)
lchEOF, rchEOF, lchClosed, rchClosed := false, false, false, false
for {
select {
case err := <-errch:
return err
case q := <-quit:
switch q {
case "lch":
lchEOF = true
_ = rch.CloseWrite()
case "rch":
rchEOF = true
_ = lch.CloseWrite()
case "lreqs":
lchClosed = true
case "rreqs":
rchClosed = true
}
if lchEOF && lchClosed && !rchClosed {
rch.Close()
}
if rchEOF && rchClosed && !lchClosed {
lch.Close()
}
if lchEOF && rchEOF && lchClosed && rchClosed {
return nil
}
}
}
}
func newDiscardWriteCloser() io.WriteCloser { return &discardWriteCloser{ioutil.Discard} }
type discardWriteCloser struct {
io.Writer
}
func (discardWriteCloser) Close() error {
return nil
}

2398
pkg/bastion/shell.go Normal file

File diff suppressed because it is too large Load Diff

370
pkg/bastion/ssh.go Normal file
View File

@@ -0,0 +1,370 @@
package bastion // import "moul.io/sshportal/pkg/bastion"
import (
"bytes"
"errors"
"fmt"
"log"
"net"
"strings"
"time"
"github.com/gliderlabs/ssh"
gossh "golang.org/x/crypto/ssh"
"gorm.io/gorm"
"moul.io/sshportal/pkg/crypto"
"moul.io/sshportal/pkg/dbmodels"
)
type sshportalContextKey string
var authContextKey = sshportalContextKey("auth")
type authContext struct {
message string
err error
user dbmodels.User
inputUsername string
db *gorm.DB
userKey dbmodels.UserKey
logsLocation string
aclCheckCmd string
aesKey string
dbDriver, dbURL string
bindAddr string
demo, debug bool
authMethod string
authSuccess bool
}
type userType string
const (
userTypeHealthcheck userType = "healthcheck"
userTypeBastion userType = "bastion"
userTypeInvite userType = "invite"
userTypeShell userType = "shell"
)
func (c authContext) userType() userType {
switch {
case c.inputUsername == "healthcheck":
return userTypeHealthcheck
case c.inputUsername == c.user.Name || c.inputUsername == c.user.Email || c.inputUsername == "admin":
return userTypeShell
case strings.HasPrefix(c.inputUsername, "invite:"):
return userTypeInvite
default:
return userTypeBastion
}
}
func dynamicHostKey(db *gorm.DB, host *dbmodels.Host) gossh.HostKeyCallback {
return func(hostname string, remote net.Addr, key gossh.PublicKey) error {
if len(host.HostKey) == 0 {
log.Println("Discovering host fingerprint...")
return db.Model(host).Update("HostKey", key.Marshal()).Error
}
if !bytes.Equal(host.HostKey, key.Marshal()) {
return fmt.Errorf("ssh: host key mismatch")
}
return nil
}
}
var DefaultChannelHandler ssh.ChannelHandler = func(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {}
func ChannelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
switch newChan.ChannelType() {
case "session":
case "direct-tcpip":
default:
// TODO: handle direct-tcp (only for ssh scheme)
if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil {
log.Printf("error: failed to reject channel: %v", err)
}
return
}
actx := ctx.Value(authContextKey).(*authContext)
if actx.user.ID == 0 && actx.userType() != userTypeHealthcheck {
ip, err := net.ResolveTCPAddr(conn.RemoteAddr().Network(), conn.RemoteAddr().String())
if err == nil {
log.Printf("Auth failed: sshUser=%q remote=%q", conn.User(), ip.IP.String())
actx.err = errors.New("access denied")
ch, _, err2 := newChan.Accept()
if err2 != nil {
return
}
fmt.Fprintf(ch, "error: %v\n", actx.err)
_ = ch.Close()
return
}
}
switch actx.userType() {
case userTypeBastion:
log.Printf("New connection(bastion): sshUser=%q remote=%q local=%q dbUser=id:%d,email:%s", conn.User(), conn.RemoteAddr(), conn.LocalAddr(), actx.user.ID, actx.user.Email)
host, err := dbmodels.HostByName(actx.db, actx.inputUsername)
if err != nil {
ch, _, err2 := newChan.Accept()
if err2 != nil {
return
}
fmt.Fprintf(ch, "error: %v\n", err)
// FIXME: force close all channels
_ = ch.Close()
return
}
switch host.Scheme() {
case dbmodels.BastionSchemeSSH:
sessionConfigs := make([]sessionConfig, 0)
currentHost := host
for currentHost != nil {
clientConfig, err2 := bastionClientConfig(ctx, currentHost)
if err2 != nil {
ch, _, err3 := newChan.Accept()
if err3 != nil {
return
}
fmt.Fprintf(ch, "error: %v\n", err2)
// FIXME: force close all channels
_ = ch.Close()
return
}
sessionConfigs = append([]sessionConfig{{
Addr: currentHost.DialAddr(),
ClientConfig: clientConfig,
LogsLocation: actx.logsLocation,
LoggingMode: currentHost.Logging,
}}, sessionConfigs...)
if currentHost.HopID != 0 {
var newHost dbmodels.Host
if err := actx.db.Model(currentHost).Association("HopID").Find(&newHost); err != nil {
log.Printf("Error: %v", err)
return
}
hostname := newHost.Name
currentHost, _ = dbmodels.HostByName(actx.db, hostname)
} else {
currentHost = nil
}
}
sess := dbmodels.Session{
UserID: actx.user.ID,
HostID: host.ID,
Status: string(dbmodels.SessionStatusActive),
}
if err = actx.db.Create(&sess).Error; err != nil {
ch, _, err2 := newChan.Accept()
if err2 != nil {
return
}
fmt.Fprintf(ch, "error: %v\n", err)
_ = ch.Close()
return
}
go func() {
err = multiChannelHandler(conn, newChan, ctx, sessionConfigs, sess.ID)
if err != nil {
log.Printf("Error: %v", err)
}
now := time.Now()
sessUpdate := dbmodels.Session{
Status: string(dbmodels.SessionStatusClosed),
ErrMsg: fmt.Sprintf("%v", err),
StoppedAt: &now,
}
if err == nil {
sessUpdate.ErrMsg = ""
}
actx.db.Model(&sess).Updates(&sessUpdate)
}()
case dbmodels.BastionSchemeTelnet:
tmpSrv := ssh.Server{
// PtyCallback: srv.PtyCallback,
Handler: telnetHandler(host),
}
DefaultChannelHandler(&tmpSrv, conn, newChan, ctx)
default:
ch, _, err2 := newChan.Accept()
if err2 != nil {
return
}
fmt.Fprintf(ch, "error: unknown bastion scheme: %q\n", host.Scheme())
// FIXME: force close all channels
_ = ch.Close()
}
default: // shell
DefaultChannelHandler(srv, conn, newChan, ctx)
}
}
func bastionClientConfig(ctx ssh.Context, host *dbmodels.Host) (*gossh.ClientConfig, error) {
actx := ctx.Value(authContextKey).(*authContext)
crypto.HostDecrypt(actx.aesKey, host)
crypto.SSHKeyDecrypt(actx.aesKey, host.SSHKey)
clientConfig, err := host.ClientConfig(dynamicHostKey(actx.db, host))
if err != nil {
return nil, err
}
var tmpUser dbmodels.User
if err = actx.db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", actx.user.ID).First(&tmpUser).Error; err != nil {
return nil, err
}
var tmpHost dbmodels.Host
if err = actx.db.Preload("Groups").Preload("Groups.ACLs").Where("id = ?", host.ID).First(&tmpHost).Error; err != nil {
return nil, err
}
action := checkACLs(tmpUser, tmpHost, actx.aclCheckCmd)
switch action {
case string(dbmodels.ACLActionAllow):
// do nothing
case string(dbmodels.ACLActionDeny):
return nil, fmt.Errorf("you don't have permission to that host")
default:
return nil, fmt.Errorf("invalid ACL action: %q", action)
}
return clientConfig, nil
}
func ShellHandler(s ssh.Session, version, gitSha, gitTag string) {
actx := s.Context().Value(authContextKey).(*authContext)
if actx.userType() != userTypeHealthcheck {
log.Printf("New connection(shell): sshUser=%q remote=%q local=%q command=%q dbUser=id:%d,email:%s", s.User(), s.RemoteAddr(), s.LocalAddr(), s.Command(), actx.user.ID, actx.user.Email)
}
if actx.err != nil {
fmt.Fprintf(s, "error: %v\n", actx.err)
_ = s.Exit(1)
return
}
if actx.message != "" {
fmt.Fprint(s, actx.message)
}
switch actx.userType() {
case userTypeHealthcheck:
fmt.Fprintln(s, "OK")
return
case userTypeShell:
if err := shell(s, version, gitSha, gitTag); err != nil {
fmt.Fprintf(s, "error: %v\n", err)
_ = s.Exit(1)
}
return
case userTypeInvite:
// do nothing (message was printed at the beginning of the function)
return
}
panic("should not happen")
}
func PasswordAuthHandler(db *gorm.DB, logsLocation, aclCheckCmd, aesKey, dbDriver, dbURL, bindAddr string, demo bool) ssh.PasswordHandler {
return func(ctx ssh.Context, pass string) bool {
actx := &authContext{
db: db,
inputUsername: ctx.User(),
logsLocation: logsLocation,
aclCheckCmd: aclCheckCmd,
aesKey: aesKey,
dbDriver: dbDriver,
dbURL: dbURL,
bindAddr: bindAddr,
demo: demo,
authMethod: "password",
}
actx.authSuccess = actx.userType() == userTypeHealthcheck
ctx.SetValue(authContextKey, actx)
return actx.authSuccess
}
}
func PrivateKeyFromDB(db *gorm.DB, aesKey string) func(*ssh.Server) error {
return func(srv *ssh.Server) error {
var key dbmodels.SSHKey
if err := dbmodels.SSHKeysByIdentifiers(db, []string{"host"}).First(&key).Error; err != nil {
return err
}
crypto.SSHKeyDecrypt(aesKey, &key)
signer, err := gossh.ParsePrivateKey([]byte(key.PrivKey))
if err != nil {
return err
}
srv.AddHostKey(signer)
return nil
}
}
func PublicKeyAuthHandler(db *gorm.DB, logsLocation, aclCheckCmd, aesKey, dbDriver, dbURL, bindAddr string, demo bool) ssh.PublicKeyHandler {
return func(ctx ssh.Context, key ssh.PublicKey) bool {
actx := &authContext{
db: db,
inputUsername: ctx.User(),
logsLocation: logsLocation,
aclCheckCmd: aclCheckCmd,
aesKey: aesKey,
dbDriver: dbDriver,
dbURL: dbURL,
bindAddr: bindAddr,
demo: demo,
authMethod: "pubkey",
authSuccess: true,
}
ctx.SetValue(authContextKey, actx)
// lookup user by key
db.Where("authorized_key = ?", string(gossh.MarshalAuthorizedKey(key))).First(&actx.userKey)
if actx.userKey.UserID > 0 {
db.Preload("Roles").Where("id = ?", actx.userKey.UserID).First(&actx.user)
if actx.userType() == userTypeInvite {
actx.err = fmt.Errorf("invites are only supported for new SSH keys; your ssh key is already associated with the user %q", actx.user.Email)
}
return true
}
// handle invite "links"
if actx.userType() == userTypeInvite {
inputToken := strings.Split(actx.inputUsername, ":")[1]
if len(inputToken) > 0 {
db.Where("invite_token = ?", inputToken).First(&actx.user)
}
if actx.user.ID > 0 {
actx.userKey = dbmodels.UserKey{
UserID: actx.user.ID,
Key: key.Marshal(),
Comment: "created by sshportal",
AuthorizedKey: string(gossh.MarshalAuthorizedKey(key)),
}
db.Create(&actx.userKey)
// token is only usable once
actx.user.InviteToken = ""
db.Model(&actx.user).Updates(&actx.user)
actx.message = fmt.Sprintf("Welcome %s!\n\nYour key is now associated with the user %q.\n", actx.user.Name, actx.user.Email)
} else {
actx.user = dbmodels.User{Name: "Anonymous"}
actx.err = errors.New("your token is invalid or expired")
}
return true
}
// fallback
actx.err = errors.New("unknown ssh key")
actx.user = dbmodels.User{Name: "Anonymous"}
return true
}
}

88
pkg/bastion/telnet.go Normal file
View File

@@ -0,0 +1,88 @@
package bastion // import "moul.io/sshportal/pkg/bastion"
import (
"bufio"
"bytes"
"fmt"
"io"
"log"
"time"
"github.com/gliderlabs/ssh"
oi "github.com/reiver/go-oi"
telnet "github.com/reiver/go-telnet"
"moul.io/sshportal/pkg/dbmodels"
)
type bastionTelnetCaller struct {
ssh ssh.Session
}
func (caller bastionTelnetCaller) CallTELNET(ctx telnet.Context, w telnet.Writer, r telnet.Reader) {
go func(writer io.Writer, reader io.Reader) {
var buffer [1]byte // Seems like the length of the buffer needs to be small, otherwise will have to wait for buffer to fill up.
p := buffer[:]
for {
// Read 1 byte.
n, err := reader.Read(p)
if n <= 0 && err == nil {
continue
} else if n <= 0 && err != nil {
break
}
if _, err = oi.LongWrite(writer, p); err != nil {
log.Printf("telnet longwrite failed: %v", err)
}
}
}(caller.ssh, r)
var buffer bytes.Buffer
var p []byte
var crlfBuffer = [2]byte{'\r', '\n'}
crlf := crlfBuffer[:]
scanner := bufio.NewScanner(caller.ssh)
scanner.Split(scannerSplitFunc)
for scanner.Scan() {
buffer.Write(scanner.Bytes())
buffer.Write(crlf)
p = buffer.Bytes()
n, err := oi.LongWrite(w, p)
if nil != err {
break
}
if expected, actual := int64(len(p)), n; expected != actual {
err := fmt.Errorf("transmission problem: tried sending %d bytes, but actually only sent %d bytes", expected, actual)
fmt.Fprint(caller.ssh, err.Error())
return
}
buffer.Reset()
}
// Wait a bit to receive data from the server (that we would send to io.Stdout).
time.Sleep(3 * time.Millisecond)
}
func scannerSplitFunc(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF {
return 0, nil, nil
}
return bufio.ScanLines(data, atEOF)
}
func telnetHandler(host *dbmodels.Host) ssh.Handler {
return func(s ssh.Session) {
// FIXME: log session in db
// actx := s.Context().Value(authContextKey).(*authContext)
caller := bastionTelnetCaller{ssh: s}
if err := telnet.DialToAndCall(host.DialAddr(), caller); err != nil {
fmt.Fprintf(s, "error: %v", err)
}
}
}

242
pkg/crypto/crypto.go Normal file
View File

@@ -0,0 +1,242 @@
package crypto // import "moul.io/sshportal/pkg/crypto"
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"io"
"strings"
gossh "golang.org/x/crypto/ssh"
"moul.io/sshportal/pkg/dbmodels"
)
func NewSSHKey(keyType string, length uint) (*dbmodels.SSHKey, error) {
key := dbmodels.SSHKey{
Type: keyType,
Length: length,
}
// generate the private key
var err error
var pemKey *pem.Block
var publicKey gossh.PublicKey
switch keyType {
case "rsa":
pemKey, publicKey, err = NewRSAKey(length)
case "ecdsa":
pemKey, publicKey, err = NewECDSAKey(length)
case "ed25519":
pemKey, publicKey, err = NewEd25519Key()
default:
return nil, fmt.Errorf("key type not supported: %q, supported types are: rsa, ecdsa, ed25519", key.Type)
}
if err != nil {
return nil, err
}
buf := bytes.NewBufferString("")
if err = pem.Encode(buf, pemKey); err != nil {
return nil, err
}
key.PrivKey = buf.String()
// generate authorized-key formatted pubkey output
key.PubKey = strings.TrimSpace(string(gossh.MarshalAuthorizedKey(publicKey)))
return &key, nil
}
func NewRSAKey(length uint) (*pem.Block, gossh.PublicKey, error) {
if length < 1024 || length > 16384 {
return nil, nil, fmt.Errorf("key length not supported: %d, supported values are between 1024 and 16384", length)
}
privateKey, err := rsa.GenerateKey(rand.Reader, int(length))
if err != nil {
return nil, nil, err
}
// convert priv key to x509 format
pemKey := &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
}
publicKey, err := gossh.NewPublicKey(&privateKey.PublicKey)
if err != nil {
return nil, nil, err
}
return pemKey, publicKey, err
}
func NewECDSAKey(length uint) (*pem.Block, gossh.PublicKey, error) {
var curve elliptic.Curve
switch length {
case 256:
curve = elliptic.P256()
case 384:
curve = elliptic.P384()
case 521:
curve = elliptic.P521()
default:
return nil, nil, fmt.Errorf("key length not supported: %d, supported values are 256, 384, 521", length)
}
privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return nil, nil, err
}
// convert priv key to x509 format
marshaledKey, err := x509.MarshalPKCS8PrivateKey(privateKey)
pemKey := &pem.Block{
Type: "PRIVATE KEY",
Bytes: marshaledKey,
}
if err != nil {
return nil, nil, err
}
publicKey, err := gossh.NewPublicKey(&privateKey.PublicKey)
if err != nil {
return nil, nil, err
}
return pemKey, publicKey, err
}
func NewEd25519Key() (*pem.Block, gossh.PublicKey, error) {
publicKeyEd25519, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, nil, err
}
// convert priv key to x509 format
marshaledKey, err := x509.MarshalPKCS8PrivateKey(privateKey)
pemKey := &pem.Block{
Type: "PRIVATE KEY",
Bytes: marshaledKey,
}
if err != nil {
return nil, nil, err
}
publicKey, err := gossh.NewPublicKey(publicKeyEd25519)
if err != nil {
return nil, nil, err
}
return pemKey, publicKey, err
}
func ImportSSHKey(keyValue string) (*dbmodels.SSHKey, error) {
key := dbmodels.SSHKey{
Type: "rsa",
}
parsedKey, err := gossh.ParseRawPrivateKey([]byte(keyValue))
if err != nil {
return nil, err
}
var privateKey *rsa.PrivateKey
var ok bool
if privateKey, ok = parsedKey.(*rsa.PrivateKey); !ok {
return nil, errors.New("key type not supported")
}
key.Length = uint(privateKey.PublicKey.N.BitLen())
// convert priv key to x509 format
var pemKey = &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
}
buf := bytes.NewBufferString("")
if err = pem.Encode(buf, pemKey); err != nil {
return nil, err
}
key.PrivKey = buf.String()
// generte authorized-key formatted pubkey output
pub, err := gossh.NewPublicKey(&privateKey.PublicKey)
if err != nil {
return nil, err
}
key.PubKey = strings.TrimSpace(string(gossh.MarshalAuthorizedKey(pub)))
return &key, nil
}
func encrypt(key []byte, text string) (string, error) {
plaintext := []byte(text)
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return "", err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
return base64.URLEncoding.EncodeToString(ciphertext), nil
}
func decrypt(key []byte, cryptoText string) (string, error) {
ciphertext, _ := base64.URLEncoding.DecodeString(cryptoText)
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
if len(ciphertext) < aes.BlockSize {
return "", fmt.Errorf("ciphertext too short")
}
iv := ciphertext[:aes.BlockSize]
ciphertext = ciphertext[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(ciphertext, ciphertext)
return string(ciphertext), nil
}
func safeDecrypt(key []byte, cryptoText string) string {
if len(key) == 0 {
return cryptoText
}
out, err := decrypt(key, cryptoText)
if err != nil {
return cryptoText
}
return out
}
func HostEncrypt(aesKey string, host *dbmodels.Host) (err error) {
if aesKey == "" {
return nil
}
if host.Password != "" {
host.Password, err = encrypt([]byte(aesKey), host.Password)
}
return
}
func HostDecrypt(aesKey string, host *dbmodels.Host) {
if aesKey == "" {
return
}
if host.Password != "" {
host.Password = safeDecrypt([]byte(aesKey), host.Password)
}
}
func SSHKeyEncrypt(aesKey string, key *dbmodels.SSHKey) (err error) {
if aesKey == "" {
return nil
}
key.PrivKey, err = encrypt([]byte(aesKey), key.PrivKey)
return
}
func SSHKeyDecrypt(aesKey string, key *dbmodels.SSHKey) {
if aesKey == "" {
return
}
key.PrivKey = safeDecrypt([]byte(aesKey), key.PrivKey)
}

468
pkg/dbmodels/dbmodels.go Normal file
View File

@@ -0,0 +1,468 @@
package dbmodels // import "moul.io/sshportal/pkg/dbmodels"
import (
"encoding/json"
"fmt"
"log"
"net/url"
"strconv"
"strings"
"time"
gossh "golang.org/x/crypto/ssh"
"gorm.io/gorm"
)
type Config struct {
SSHKeys []*SSHKey `json:"keys"`
Hosts []*Host `json:"hosts"`
UserKeys []*UserKey `json:"user_keys"`
Users []*User `json:"users"`
UserGroups []*UserGroup `json:"user_groups"`
HostGroups []*HostGroup `json:"host_groups"`
ACLs []*ACL `json:"acls"`
Settings []*Setting `json:"settings"`
Events []*Event `json:"events"`
Sessions []*Session `json:"sessions"`
// FIXME: add latest migration
Date time.Time `json:"date"`
}
type Setting struct {
gorm.Model
Name string `valid:"required" gorm:"index:uix_settings_name,unique"`
Value string `valid:"required"`
}
// SSHKey defines a ssh client key (used by sshportal to connect to remote hosts)
type SSHKey struct {
// FIXME: use uuid for ID
gorm.Model
Name string `valid:"required,length(1|255),unix_user" gorm:"index:uix_keys_name,unique"`
Type string `valid:"required"`
Length uint `valid:"required"`
Fingerprint string `valid:"optional"`
PrivKey string `sql:"size:5000" valid:"required"`
PubKey string `sql:"size:1000" valid:"optional"`
Hosts []*Host `gorm:"ForeignKey:SSHKeyID"`
Comment string `valid:"optional"`
}
type Host struct {
// FIXME: use uuid for ID
gorm.Model
Name string `gorm:"index:uix_hosts_name,unique;type:varchar(255)" valid:"required,length(1|255)"`
Addr string `valid:"optional"` // FIXME: to be removed in a future version in favor of URL
User string `valid:"optional"` // FIXME: to be removed in a future version in favor of URL
Password string `valid:"optional"` // FIXME: to be removed in a future version in favor of URL
URL string `valid:"optional"`
SSHKey *SSHKey `gorm:"ForeignKey:SSHKeyID"` // SSHKey used to connect by the client
SSHKeyID uint `gorm:"index"`
HostKey []byte `sql:"size:1000" valid:"optional"`
Groups []*HostGroup `gorm:"many2many:host_host_groups;"`
Comment string `valid:"optional"`
Logging string `valid:"optional,host_logging_mode"`
Hop *Host
HopID uint
}
// UserKey defines a user public key used by sshportal to identify the user
type UserKey struct {
gorm.Model
Key []byte `sql:"size:1000" valid:"length(1|1000)"`
AuthorizedKey string `sql:"size:1000" valid:"required,length(1|1000)"`
UserID uint ``
User *User `gorm:"ForeignKey:UserID"`
Comment string `valid:"optional"`
}
type UserRole struct {
gorm.Model
Name string `valid:"required,length(1|255),unix_user"`
Users []*User `gorm:"many2many:user_user_roles"`
}
type User struct {
// FIXME: use uuid for ID
gorm.Model
Roles []*UserRole `gorm:"many2many:user_user_roles"`
Email string `valid:"required,email"`
Name string `valid:"required,length(1|255),unix_user" gorm:"index:uix_users_name,unique"`
Keys []*UserKey `gorm:"ForeignKey:UserID"`
Groups []*UserGroup `gorm:"many2many:user_user_groups;"`
Comment string `valid:"optional"`
InviteToken string `valid:"optional,length(10|60)"`
}
type UserGroup struct {
gorm.Model
Name string `valid:"required,length(1|255),unix_user" gorm:"index:uix_usergroups_name,unique"`
Users []*User `gorm:"many2many:user_user_groups;"`
ACLs []*ACL `gorm:"many2many:user_group_acls;"`
Comment string `valid:"optional"`
}
type HostGroup struct {
gorm.Model
Name string `valid:"required,length(1|255),unix_user" gorm:"index:uix_hostgroups_name,unique"`
Hosts []*Host `gorm:"many2many:host_host_groups;"`
ACLs []*ACL `gorm:"many2many:host_group_acls;"`
Comment string `valid:"optional"`
}
type ACL struct {
gorm.Model
HostGroups []*HostGroup `gorm:"many2many:host_group_acls;"`
UserGroups []*UserGroup `gorm:"many2many:user_group_acls;"`
HostPattern string `valid:"optional"`
Action string `valid:"required"`
Weight uint ``
Comment string `valid:"optional"`
Inception *time.Time
Expiration *time.Time
}
type Session struct {
gorm.Model
StoppedAt *time.Time `sql:"index" valid:"optional"`
Status string `valid:"required"`
User *User `gorm:"ForeignKey:UserID"`
Host *Host `gorm:"ForeignKey:HostID"`
UserID uint `valid:"optional"`
HostID uint `valid:"optional"`
ErrMsg string `valid:"optional"`
Comment string `valid:"optional"`
}
type Event struct {
gorm.Model
Author *User `gorm:"ForeignKey:AuthorID"`
AuthorID uint `valid:"optional"`
Domain string `valid:"required"`
Action string `valid:"required"`
Entity string `valid:"optional"`
Args []byte `sql:"size:10000" valid:"optional,length(1|10000)" json:"-"`
ArgsMap map[string]interface{} `gorm:"-" json:"Args"`
}
type SessionStatus string
const (
SessionStatusUnknown SessionStatus = "unknown"
SessionStatusActive SessionStatus = "active"
SessionStatusClosed SessionStatus = "closed"
)
type ACLAction string
const (
ACLActionAllow ACLAction = "allow"
ACLActionDeny ACLAction = "deny"
)
type BastionScheme string
const (
BastionSchemeSSH BastionScheme = "ssh"
BastionSchemeTelnet BastionScheme = "telnet"
)
// Generic Helper
func GenericNameOrID(db *gorm.DB, identifiers []string) *gorm.DB {
var ids []string
var names []string
for _, s := range identifiers {
if _, err := strconv.Atoi(s); err == nil {
ids = append(ids, s)
} else {
names = append(names, s)
}
}
if len(ids) > 0 && len(names) > 0 {
return db.Where("id IN (?)", ids).Or("name IN (?)", names)
} else if len(ids) > 0 {
return db.Where("id IN (?)", ids)
}
return db.Where("name IN (?)", names)
}
// Host helpers
func (host *Host) DialAddr() string {
return fmt.Sprintf("%s:%d", host.Hostname(), host.Port())
}
func (host *Host) String() string {
if host.URL != "" {
return host.URL
} else if host.Addr != "" { // to be removed in a future version in favor of URL
if host.Password != "" {
return fmt.Sprintf("ssh://%s:%s@%s", host.User, strings.Repeat("*", 4), host.Addr)
}
return fmt.Sprintf("ssh://%s@%s", host.User, host.Addr)
}
return ""
}
func (host *Host) Scheme() BastionScheme {
if host.URL != "" {
u, err := url.Parse(host.URL)
if err != nil {
return BastionSchemeSSH
}
return BastionScheme(u.Scheme)
} else if host.Addr != "" {
return BastionSchemeSSH
}
return ""
}
func (host *Host) Hostname() string {
if host.URL != "" {
u, err := url.Parse(host.URL)
if err != nil {
return ""
}
return u.Hostname()
} else if host.Addr != "" { // to be removed in a future version in favor of URL
return strings.Split(host.Addr, ":")[0]
}
return ""
}
func (host *Host) Username() string {
if host.URL != "" {
u, err := url.Parse(host.URL)
if err != nil {
return "root"
}
if u.User != nil {
return u.User.Username()
}
} else if host.User != "" { // to be removed in a future version in favor of URL
return host.User
}
return "root"
}
func (host *Host) Passwd() string {
if host.URL != "" {
u, err := url.Parse(host.URL)
if err != nil {
return ""
}
if u.User != nil {
password, _ := u.User.Password()
return password
}
} else if host.Password != "" { // to be removed in a future version in favor of URL
return host.Password
}
return ""
}
func (host *Host) Port() uint64 {
var portString string
if host.URL != "" {
u, err := url.Parse(host.URL)
if err != nil {
goto defaultPort
}
portString = u.Port()
} else if host.Addr != "" { // to be removed in a future version in favor of URL
portString = strings.Split(host.Addr, ":")[1]
}
if portString != "" {
port, err := strconv.ParseUint(portString, 10, 64)
if err != nil {
goto defaultPort
}
return port
}
defaultPort:
switch host.Scheme() {
case BastionSchemeSSH:
return 22
case BastionSchemeTelnet:
return 23
default:
return 0
}
}
func HostsPreload(db *gorm.DB) *gorm.DB {
return db.Preload("Groups").Preload("SSHKey")
}
func HostsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
return GenericNameOrID(db, identifiers)
}
func HostByName(db *gorm.DB, name string) (*Host, error) {
var host Host
db.Preload("SSHKey").Where("name = ?", name).Find(&host)
if host.Name == "" {
// FIXME: add available hosts
return nil, fmt.Errorf("no such target: %q", name)
}
return &host, nil
}
func (host *Host) ClientConfig(hk gossh.HostKeyCallback) (*gossh.ClientConfig, error) {
config := gossh.ClientConfig{
User: host.Username(),
HostKeyCallback: hk,
Auth: []gossh.AuthMethod{},
}
if host.SSHKey != nil {
signer, err := gossh.ParsePrivateKey([]byte(host.SSHKey.PrivKey))
if err != nil {
return nil, err
}
config.Auth = append(config.Auth, gossh.PublicKeys(signer))
}
if host.Passwd() != "" {
config.Auth = append(config.Auth, gossh.Password(host.Passwd()))
}
if len(config.Auth) == 0 {
return nil, fmt.Errorf("no valid authentication method for host %q", host.Name)
}
return &config, nil
}
// SSHKey helpers
func SSHKeysPreload(db *gorm.DB) *gorm.DB {
return db.Preload("Hosts")
}
func SSHKeysByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
return GenericNameOrID(db, identifiers)
}
// HostGroup helpers
func HostGroupsPreload(db *gorm.DB) *gorm.DB {
return db.Preload("ACLs").Preload("Hosts")
}
func HostGroupsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
return GenericNameOrID(db, identifiers)
}
// UserGroup helpers
func UserGroupsPreload(db *gorm.DB) *gorm.DB {
return db.Preload("ACLs").Preload("Users")
}
func UserGroupsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
return GenericNameOrID(db, identifiers)
}
// User helpers
func UsersPreload(db *gorm.DB) *gorm.DB {
return db.Preload("Groups").Preload("Keys").Preload("Roles")
}
func UsersByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
var ids []string
var names []string
for _, s := range identifiers {
if _, err := strconv.Atoi(s); err == nil {
ids = append(ids, s)
} else {
names = append(names, s)
}
}
if len(ids) > 0 && len(names) > 0 {
db.Where("id IN (?)", identifiers).Or("email IN (?)", identifiers).Or("name IN (?)", identifiers)
} else if len(ids) > 0 {
return db.Where("id IN (?)", ids)
}
return db.Where("email IN (?)", identifiers).Or("name IN (?)", identifiers)
}
func (u *User) HasRole(name string) bool {
for _, role := range u.Roles {
if role.Name == name {
return true
}
}
return false
}
func (u *User) CheckRoles(names []string) error {
for _, name := range names {
if u.HasRole(name) {
return nil
}
}
return fmt.Errorf("you don't have permission to access this feature (requires any of these roles: '%s')", strings.Join(names, "', '"))
}
// ACL helpers
func ACLsPreload(db *gorm.DB) *gorm.DB {
return db.Preload("UserGroups").Preload("HostGroups")
}
func ACLsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
return db.Where("id IN (?)", identifiers)
}
// UserKey helpers
func UserKeysPreload(db *gorm.DB) *gorm.DB {
return db.Preload("User")
}
func UserKeysByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
return db.Where("id IN (?)", identifiers)
}
func UserKeysByUserID(db *gorm.DB, identifiers []string) *gorm.DB {
return db.Where("user_id IN (?)", identifiers)
}
// UserRole helpers
func UserRolesByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
return GenericNameOrID(db, identifiers)
}
// Session helpers
func SessionsPreload(db *gorm.DB) *gorm.DB {
return db.Preload("User").Preload("Host")
}
func SessionsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
return db.Where("id IN (?)", identifiers)
}
// Events helpers
func EventsPreload(db *gorm.DB) *gorm.DB {
return db.Preload("Author")
}
func EventsByIdentifiers(db *gorm.DB, identifiers []string) *gorm.DB {
return db.Where("id IN (?)", identifiers)
}
func NewEvent(domain, action string) *Event {
return &Event{
Domain: domain,
Action: action,
ArgsMap: map[string]interface{}{},
}
}
func (e *Event) String() string {
return fmt.Sprintf("%s %s %s %s", e.Domain, e.Action, e.Entity, string(e.Args))
}
func (e *Event) Log(db *gorm.DB) {
if len(e.ArgsMap) > 0 {
var err error
if e.Args, err = json.Marshal(e.ArgsMap); err != nil {
log.Printf("error: %v", err)
}
}
log.Printf("info: %s", e)
if err := db.Create(e).Error; err != nil {
log.Printf("warning: %v", err)
}
}
func (e *Event) SetAuthor(user *User) *Event {
e.AuthorID = user.ID
return e
}
func (e *Event) SetArg(name string, value interface{}) *Event {
e.ArgsMap[name] = value
return e
}

33
pkg/dbmodels/validator.go Normal file
View File

@@ -0,0 +1,33 @@
package dbmodels
import (
"regexp"
"github.com/asaskevich/govalidator"
)
func InitValidator() {
unixUserRegexp := regexp.MustCompile("[a-z_][a-z0-9_-]*")
govalidator.CustomTypeTagMap.Set("unix_user", govalidator.CustomTypeValidator(func(i interface{}, context interface{}) bool {
name, ok := i.(string)
if !ok {
return false
}
return unixUserRegexp.MatchString(name)
}))
govalidator.CustomTypeTagMap.Set("host_logging_mode", govalidator.CustomTypeValidator(func(i interface{}, context interface{}) bool {
name, ok := i.(string)
if !ok {
return false
}
if name == "" {
return true
}
return IsValidHostLoggingMode(name)
}))
}
func IsValidHostLoggingMode(name string) bool {
return name == "disabled" || name == "input" || name == "everything"
}

View File

@@ -0,0 +1,13 @@
package utils
import "regexp"
var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
// ValidateEmail validates email.
func ValidateEmail(e string) bool {
if len(e) < 3 || len(e) > 254 {
return false
}
return emailRegex.MatchString(e)
}

View File

@@ -0,0 +1,30 @@
package utils_test
import (
"testing"
"moul.io/sshportal/pkg/utils"
)
func TestValidateEmail(t *testing.T) {
tests := []struct {
input string
expected bool
}{
{"goodemail@email.com", true},
{"b@2323.22", true},
{"b@2322.", false},
{"", false},
{"blah", false},
{"blah.com", false},
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
got := utils.ValidateEmail(test.input)
if got != test.expected {
t.Errorf("expected %v, got %v", test.expected, got)
}
})
}
}

View File

@@ -1,99 +0,0 @@
package main
import (
"errors"
"fmt"
"io"
"log"
"github.com/gliderlabs/ssh"
gossh "golang.org/x/crypto/ssh"
)
func proxy(s ssh.Session, host *Host) error {
config, err := host.ClientConfig(s)
if err != nil {
return err
}
rconn, err := gossh.Dial("tcp", host.Addr, config)
if err != nil {
return err
}
defer rconn.Close()
rch, rreqs, err := rconn.OpenChannel("session", []byte{})
if err != nil {
return err
}
log.Println("SSH Connectin established")
return pipe(s.MaskedReqs(), rreqs, s, rch)
}
func pipe(lreqs, rreqs <-chan *gossh.Request, lch, rch gossh.Channel) error {
defer func() {
lch.Close()
rch.Close()
}()
errch := make(chan error, 1)
go func() {
_, _ = io.Copy(lch, rch)
errch <- errors.New("lch closed the connection")
}()
go func() {
_, _ = io.Copy(rch, lch)
errch <- errors.New("rch closed the connection")
}()
for {
select {
case req := <-lreqs: // forward ssh requests from local to remote
if req == nil {
return nil
}
b, err := rch.SendRequest(req.Type, req.WantReply, req.Payload)
if err != nil {
return err
}
req.Reply(b, nil)
case req := <-rreqs: // forward ssh requests from remote to local
if req == nil {
return nil
}
b, err := lch.SendRequest(req.Type, req.WantReply, req.Payload)
if err != nil {
return err
}
req.Reply(b, nil)
case err := <-errch:
return err
}
}
return nil
}
func (host *Host) ClientConfig(_ ssh.Session) (*gossh.ClientConfig, error) {
config := gossh.ClientConfig{
User: host.User,
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
Auth: []gossh.AuthMethod{},
}
if host.SSHKey != nil {
signer, err := gossh.ParsePrivateKey([]byte(host.SSHKey.PrivKey))
if err != nil {
return nil, err
}
config.Auth = append(config.Auth, gossh.PublicKeys(signer))
}
if host.Password != "" {
config.Auth = append(config.Auth, gossh.Password(host.Password))
}
if len(config.Auth) == 0 {
return nil, fmt.Errorf("no valid authentication method for host %q", host.Name)
}
return &config, nil
}

361
rules.mk vendored Normal file
View File

@@ -0,0 +1,361 @@
# +--------------------------------------------------------------+
# | * * * moul.io/rules.mk |
# +--------------------------------------------------------------+
# | |
# | ++ ______________________________________ |
# | ++++ / \ |
# | ++++ | | |
# | ++++++++++ | https://moul.io/rules.mk is a set | |
# | +++ | | of common Makefile rules that can | |
# | ++ | | be configured from the Makefile | |
# | + -== ==| | or with environment variables. | |
# | ( <*> <*> | | |
# | | | /| Manfred Touron | |
# | | _) / | manfred.life | |
# | | +++ / \______________________________________/ |
# | \ =+ / |
# | \ + |
# | |\++++++ |
# | | ++++ ||// |
# | ___| |___ _||/__ __|
# | / --- \ \| ||| __ _ ___ __ __/ /|
# |/ | | \ \ / / ' \/ _ \/ // / / |
# || | | | | | /_/_/_/\___/\_,_/_/ |
# +--------------------------------------------------------------+
.PHONY: _default_entrypoint
_default_entrypoint: help
##
## Common helpers
##
rwildcard = $(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst *,%,$2),$d))
check-program = $(foreach exec,$(1),$(if $(shell PATH="$(PATH)" which $(exec)),,$(error "No $(exec) in PATH")))
my-filter-out = $(foreach v,$(2),$(if $(findstring $(1),$(v)),,$(v)))
novendor = $(call my-filter-out,vendor/,$(1))
##
## rules.mk
##
ifneq ($(wildcard rules.mk),)
.PHONY: rulesmk.bumpdeps
rulesmk.bumpdeps:
wget -O rules.mk https://raw.githubusercontent.com/moul/rules.mk/master/rules.mk
BUMPDEPS_STEPS += rulesmk.bumpdeps
endif
##
## Maintainer
##
ifneq ($(wildcard .git/HEAD),)
.PHONY: generate.authors
generate.authors: AUTHORS
AUTHORS: .git/
echo "# This file lists all individuals having contributed content to the repository." > AUTHORS
echo "# For how it is generated, see 'https://github.com/moul/rules.mk'" >> AUTHORS
echo >> AUTHORS
git log --format='%aN <%aE>' | LC_ALL=C.UTF-8 sort -uf >> AUTHORS
GENERATE_STEPS += generate.authors
endif
##
## Golang
##
ifndef GOPKG
ifneq ($(wildcard go.mod),)
GOPKG = $(shell sed '/module/!d;s/^omdule\ //' go.mod)
endif
endif
ifdef GOPKG
GO ?= go
GOPATH ?= $(HOME)/go
GO_INSTALL_OPTS ?=
GO_TEST_OPTS ?= -test.timeout=30s
GOMOD_DIRS ?= $(sort $(call novendor,$(dir $(call rwildcard,*,*/go.mod go.mod))))
GOCOVERAGE_FILE ?= ./coverage.txt
GOTESTJSON_FILE ?= ./go-test.json
GOBUILDLOG_FILE ?= ./go-build.log
GOINSTALLLOG_FILE ?= ./go-install.log
ifdef GOBINS
.PHONY: go.install
go.install:
ifeq ($(CI),true)
@rm -f /tmp/goinstall.log
@set -e; for dir in $(GOBINS); do ( set -xe; \
cd $$dir; \
$(GO) install -v $(GO_INSTALL_OPTS) .; \
); done 2>&1 | tee $(GOINSTALLLOG_FILE)
else
@set -e; for dir in $(GOBINS); do ( set -xe; \
cd $$dir; \
$(GO) install $(GO_INSTALL_OPTS) .; \
); done
endif
INSTALL_STEPS += go.install
.PHONY: go.release
go.release:
$(call check-program, goreleaser)
goreleaser --snapshot --skip-publish --rm-dist
@echo -n "Do you want to release? [y/N] " && read ans && \
if [ $${ans:-N} = y ]; then set -xe; goreleaser --rm-dist; fi
RELEASE_STEPS += go.release
endif
.PHONY: go.unittest
go.unittest:
ifeq ($(CI),true)
@echo "mode: atomic" > /tmp/gocoverage
@rm -f $(GOTESTJSON_FILE)
@set -e; for dir in $(GOMOD_DIRS); do (set -e; (set -euf pipefail; \
cd $$dir; \
(($(GO) test ./... $(GO_TEST_OPTS) -cover -coverprofile=/tmp/profile.out -covermode=atomic -race -json && touch $@.ok) | tee -a $(GOTESTJSON_FILE) 3>&1 1>&2 2>&3 | tee -a $(GOBUILDLOG_FILE); \
); \
rm $@.ok 2>/dev/null || exit 1; \
if [ -f /tmp/profile.out ]; then \
cat /tmp/profile.out | sed "/mode: atomic/d" >> /tmp/gocoverage; \
rm -f /tmp/profile.out; \
fi)); done
@mv /tmp/gocoverage $(GOCOVERAGE_FILE)
else
@echo "mode: atomic" > /tmp/gocoverage
@set -e; for dir in $(GOMOD_DIRS); do (set -e; (set -xe; \
cd $$dir; \
$(GO) test ./... $(GO_TEST_OPTS) -cover -coverprofile=/tmp/profile.out -covermode=atomic -race); \
if [ -f /tmp/profile.out ]; then \
cat /tmp/profile.out | sed "/mode: atomic/d" >> /tmp/gocoverage; \
rm -f /tmp/profile.out; \
fi); done
@mv /tmp/gocoverage $(GOCOVERAGE_FILE)
endif
.PHONY: go.checkdoc
go.checkdoc:
go doc $(first $(GOMOD_DIRS))
.PHONY: go.coverfunc
go.coverfunc: go.unittest
go tool cover -func=$(GOCOVERAGE_FILE) | grep -v .pb.go: | grep -v .pb.gw.go:
.PHONY: go.lint
go.lint:
@set -e; for dir in $(GOMOD_DIRS); do ( set -xe; \
cd $$dir; \
golangci-lint run --verbose ./...; \
); done
.PHONY: go.tidy
go.tidy:
@# tidy dirs with go.mod files
@set -e; for dir in $(GOMOD_DIRS); do ( set -xe; \
cd $$dir; \
$(GO) mod tidy; \
); done
.PHONY: go.depaware-update
go.depaware-update: go.tidy
@# gen depaware for bins
@set -e; for dir in $(GOBINS); do ( set -xe; \
cd $$dir; \
$(GO) run github.com/tailscale/depaware --update .; \
); done
@# tidy unused depaware deps if not in a tools_test.go file
@set -e; for dir in $(GOMOD_DIRS); do ( set -xe; \
cd $$dir; \
$(GO) mod tidy; \
); done
.PHONY: go.depaware-check
go.depaware-check: go.tidy
@# gen depaware for bins
@set -e; for dir in $(GOBINS); do ( set -xe; \
cd $$dir; \
$(GO) run github.com/tailscale/depaware --check .; \
); done
.PHONY: go.build
go.build:
@set -e; for dir in $(GOMOD_DIRS); do ( set -xe; \
cd $$dir; \
$(GO) build ./...; \
); done
.PHONY: go.bump-deps
go.bumpdeps:
@set -e; for dir in $(GOMOD_DIRS); do ( set -xe; \
cd $$dir; \
$(GO) get -u ./...; \
); done
.PHONY: go.fmt
go.fmt:
@set -e; for dir in $(GOMOD_DIRS); do ( set -xe; \
cd $$dir; \
$(GO) run golang.org/x/tools/cmd/goimports -w `go list -f '{{.Dir}}' ./...` \
); done
VERIFY_STEPS += go.depaware-check
BUILD_STEPS += go.build
BUMPDEPS_STEPS += go.bumpdeps go.depaware-update
TIDY_STEPS += go.tidy
LINT_STEPS += go.lint
UNITTEST_STEPS += go.unittest
FMT_STEPS += go.fmt
# FIXME: disabled, because currently slow
# new rule that is manually run sometimes, i.e. `make pre-release` or `make maintenance`.
# alternative: run it each time the go.mod is changed
#GENERATE_STEPS += go.depaware-update
endif
##
## Gitattributes
##
ifneq ($(wildcard .gitattributes),)
.PHONY: _linguist-ignored
_linguist-kept:
@git check-attr linguist-vendored $(shell git check-attr linguist-generated $(shell find . -type f | grep -v .git/) | grep unspecified | cut -d: -f1) | grep unspecified | cut -d: -f1 | sort
.PHONY: _linguist-kept
_linguist-ignored:
@git check-attr linguist-vendored linguist-ignored `find . -not -path './.git/*' -type f` | grep '\ set$$' | cut -d: -f1 | sort -u
endif
##
## Node
##
ifndef NPM_PACKAGES
ifneq ($(wildcard package.json),)
NPM_PACKAGES = .
endif
endif
ifdef NPM_PACKAGES
.PHONY: npm.publish
npm.publish:
@echo -n "Do you want to npm publish? [y/N] " && read ans && \
@if [ $${ans:-N} = y ]; then \
set -e; for dir in $(NPM_PACKAGES); do ( set -xe; \
cd $$dir; \
npm publish --access=public; \
); done; \
fi
RELEASE_STEPS += npm.publish
endif
##
## Docker
##
docker_build = docker build \
--build-arg VCS_REF=`git rev-parse --short HEAD` \
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
--build-arg VERSION=`git describe --tags --always` \
-t "$2" -f "$1" "$(dir $1)"
ifndef DOCKERFILE_PATH
DOCKERFILE_PATH = ./Dockerfile
endif
ifndef DOCKER_IMAGE
ifneq ($(wildcard Dockerfile),)
DOCKER_IMAGE = $(notdir $(PWD))
endif
endif
ifdef DOCKER_IMAGE
ifneq ($(DOCKER_IMAGE),none)
.PHONY: docker.build
docker.build:
$(call check-program, docker)
$(call docker_build,$(DOCKERFILE_PATH),$(DOCKER_IMAGE))
BUILD_STEPS += docker.build
endif
endif
##
## Common
##
TEST_STEPS += $(UNITTEST_STEPS)
TEST_STEPS += $(LINT_STEPS)
TEST_STEPS += $(TIDY_STEPS)
ifneq ($(strip $(TEST_STEPS)),)
.PHONY: test
test: $(PRE_TEST_STEPS) $(TEST_STEPS)
endif
ifdef INSTALL_STEPS
.PHONY: install
install: $(PRE_INSTALL_STEPS) $(INSTALL_STEPS)
endif
ifdef UNITTEST_STEPS
.PHONY: unittest
unittest: $(PRE_UNITTEST_STEPS) $(UNITTEST_STEPS)
endif
ifdef LINT_STEPS
.PHONY: lint
lint: $(PRE_LINT_STEPS) $(FMT_STEPS) $(LINT_STEPS)
endif
ifdef TIDY_STEPS
.PHONY: tidy
tidy: $(PRE_TIDY_STEPS) $(TIDY_STEPS)
endif
ifdef BUILD_STEPS
.PHONY: build
build: $(PRE_BUILD_STEPS) $(BUILD_STEPS)
endif
ifdef VERIFY_STEPS
.PHONY: verify
verify: $(PRE_VERIFY_STEPS) $(VERIFY_STEPS)
endif
ifdef RELEASE_STEPS
.PHONY: release
release: $(PRE_RELEASE_STEPS) $(RELEASE_STEPS)
endif
ifdef BUMPDEPS_STEPS
.PHONY: bumpdeps
bumpdeps: $(PRE_BUMDEPS_STEPS) $(BUMPDEPS_STEPS)
endif
ifdef FMT_STEPS
.PHONY: fmt
fmt: $(PRE_FMT_STEPS) $(FMT_STEPS)
endif
ifdef GENERATE_STEPS
.PHONY: generate
generate: $(PRE_GENERATE_STEPS) $(GENERATE_STEPS)
endif
.PHONY: help
help::
@echo "General commands:"
@[ "$(BUILD_STEPS)" != "" ] && echo " build" || true
@[ "$(BUMPDEPS_STEPS)" != "" ] && echo " bumpdeps" || true
@[ "$(FMT_STEPS)" != "" ] && echo " fmt" || true
@[ "$(GENERATE_STEPS)" != "" ] && echo " generate" || true
@[ "$(INSTALL_STEPS)" != "" ] && echo " install" || true
@[ "$(LINT_STEPS)" != "" ] && echo " lint" || true
@[ "$(RELEASE_STEPS)" != "" ] && echo " release" || true
@[ "$(TEST_STEPS)" != "" ] && echo " test" || true
@[ "$(TIDY_STEPS)" != "" ] && echo " tidy" || true
@[ "$(UNITTEST_STEPS)" != "" ] && echo " unittest" || true
@[ "$(VERIFY_STEPS)" != "" ] && echo " verify" || true
@# FIXME: list other commands
print-% : ; $(info $* is a $(flavor $*) variable set to [$($*)]) @true

158
server.go Normal file
View File

@@ -0,0 +1,158 @@
package main
import (
"fmt"
"log"
"math"
"net"
"os"
"time"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/gliderlabs/ssh"
"github.com/urfave/cli"
gossh "golang.org/x/crypto/ssh"
"moul.io/sshportal/pkg/bastion"
)
type serverConfig struct {
aesKey string
dbDriver, dbURL string
logsLocation string
bindAddr string
debug, demo bool
idleTimeout time.Duration
aclCheckCmd string
}
func parseServerConfig(c *cli.Context) (*serverConfig, error) {
ret := &serverConfig{
aesKey: c.String("aes-key"),
dbDriver: c.String("db-driver"),
dbURL: c.String("db-conn"),
bindAddr: c.String("bind-address"),
debug: c.Bool("debug"),
demo: c.Bool("demo"),
logsLocation: c.String("logs-location"),
idleTimeout: c.Duration("idle-timeout"),
aclCheckCmd: c.String("acl-check-cmd"),
}
switch len(ret.aesKey) {
case 0, 16, 24, 32:
default:
return nil, fmt.Errorf("invalid aes key size, should be 16 or 24, 32")
}
return ret, nil
}
func ensureLogDirectory(location string) error {
// check for the logdir existence
logsLocation, err := os.Stat(location)
if err != nil {
if os.IsNotExist(err) {
return os.MkdirAll(location, os.ModeDir|os.FileMode(0750))
}
return err
}
if !logsLocation.IsDir() {
return fmt.Errorf("log directory cannot be created")
}
return nil
}
func dbConnect(c *serverConfig, config gorm.Option) (*gorm.DB, error) {
var dbOpen func(string) gorm.Dialector
if c.dbDriver == "sqlite3" {
dbOpen = sqlite.Open
}
if c.dbDriver == "postgres" {
dbOpen = postgres.Open
}
if c.dbDriver == "mysql" {
dbOpen = mysql.Open
}
return gorm.Open(dbOpen(c.dbURL), config)
}
func server(c *serverConfig) (err error) {
// configure db logging
db, err := dbConnect(c, &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
sqlDB, err := db.DB()
defer func() {
origErr := err
err = sqlDB.Close()
if origErr != nil {
err = origErr
}
}()
if err = sqlDB.Ping(); err != nil {
return
}
if err = bastion.DBInit(db); err != nil {
return
}
// create TCP listening socket
ln, err := net.Listen("tcp", c.bindAddr)
if err != nil {
return err
}
// configure server
srv := &ssh.Server{
Addr: c.bindAddr,
Handler: func(s ssh.Session) { bastion.ShellHandler(s, GitTag, GitSha, GitTag) }, // ssh.Server.Handler is the handler for the DefaultSessionHandler
Version: fmt.Sprintf("sshportal-%s", GitTag),
ChannelHandlers: map[string]ssh.ChannelHandler{
"default": bastion.ChannelHandler,
},
}
// configure channel handler
bastion.DefaultChannelHandler = func(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
switch newChan.ChannelType() {
case "session":
go ssh.DefaultSessionHandler(srv, conn, newChan, ctx)
case "direct-tcpip":
go ssh.DirectTCPIPHandler(srv, conn, newChan, ctx)
default:
if err := newChan.Reject(gossh.UnknownChannelType, "unsupported channel type"); err != nil {
log.Printf("failed to reject chan: %v", err)
}
}
}
if c.idleTimeout != 0 {
srv.IdleTimeout = c.idleTimeout
// gliderlabs/ssh requires MaxTimeout to be non-zero if we want to use IdleTimeout.
// So, set it to the max value, because we don't want a max timeout.
srv.MaxTimeout = math.MaxInt64
}
for _, opt := range []ssh.Option{
// custom PublicKeyAuth handler
ssh.PublicKeyAuth(bastion.PublicKeyAuthHandler(db, c.logsLocation, c.aclCheckCmd, c.aesKey, c.dbDriver, c.dbURL, c.bindAddr, c.demo)),
ssh.PasswordAuth(bastion.PasswordAuthHandler(db, c.logsLocation, c.aclCheckCmd, c.aesKey, c.dbDriver, c.dbURL, c.bindAddr, c.demo)),
// retrieve sshportal SSH private key from database
bastion.PrivateKeyFromDB(db, c.aesKey),
} {
if err := srv.SetOption(opt); err != nil {
return err
}
}
log.Printf("info: SSH Server accepting connections on %s, idle-timout=%v", c.bindAddr, c.idleTimeout)
return srv.Serve(ln)
}

923
shell.go
View File

@@ -1,923 +0,0 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"io"
"os"
"regexp"
"runtime"
"strings"
"time"
shlex "github.com/anmitsu/go-shlex"
"github.com/gliderlabs/ssh"
"github.com/jinzhu/gorm"
"github.com/moby/moby/pkg/namesgenerator"
"github.com/olekukonko/tablewriter"
"github.com/urfave/cli"
"golang.org/x/crypto/ssh/terminal"
)
var banner = `
__________ _____ __ __
/ __/ __/ // / _ \___ ____/ /____ _/ /
_\ \_\ \/ _ / ___/ _ \/ __/ __/ _ '/ /
/___/___/_//_/_/ \___/_/ \__/\_,_/_/
`
var isNameValid = regexp.MustCompile(`^[A-Za-z0-9_-]+$`).MatchString
var startTime = time.Now()
func shell(globalContext *cli.Context, s ssh.Session, sshCommand []string, db *gorm.DB) error {
if len(sshCommand) == 0 {
io.WriteString(s, banner)
}
cli.AppHelpTemplate = `COMMANDS:
{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{if .VisibleFlags}}
GLOBAL OPTIONS:
{{range .VisibleFlags}}{{.}}
{{end}}{{end}}
`
cli.OsExiter = func(c int) {
// FIXME: forward valid exit code
io.WriteString(s, fmt.Sprintf("exit: %d\n", c))
}
cli.HelpFlag = cli.BoolFlag{
Name: "help, h",
Hidden: true,
}
app := cli.NewApp()
app.Writer = s
app.HideVersion = true
app.Commands = []cli.Command{
{
Name: "acl",
Usage: "Manages acls",
Subcommands: []cli.Command{
{
Name: "create",
Usage: "Creates a new ACL",
Description: "$> acl create -",
Flags: []cli.Flag{
cli.StringSliceFlag{Name: "hostgroup, hg", Usage: "Assigns host groups to the acl"},
cli.StringSliceFlag{Name: "usergroup, ug", Usage: "Assigns host groups to the acl"},
cli.StringFlag{Name: "pattern", Usage: "Assigns a host pattern to the acl"},
cli.StringFlag{Name: "comment"},
cli.StringFlag{Name: "action", Usage: "Assigns the ACL action (allow,deny)", Value: "allow"},
cli.UintFlag{Name: "weight, w", Usage: "Assigns the ACL weight (priority)"},
},
Action: func(c *cli.Context) error {
acl := ACL{
Comment: c.String("comment"),
HostPattern: c.String("pattern"),
UserGroups: []UserGroup{},
HostGroups: []HostGroup{},
Weight: c.Uint("weight"),
Action: c.String("action"),
}
if acl.Action != "allow" && acl.Action != "deny" {
return fmt.Errorf("invalid action %q, allowed values: allow, deny", acl.Action)
}
for _, name := range c.StringSlice("usergroup") {
userGroup, err := FindUserGroupByIdOrName(db, name)
if err != nil {
return fmt.Errorf("unknown user group %q: %v", name, err)
}
acl.UserGroups = append(acl.UserGroups, *userGroup)
}
for _, name := range c.StringSlice("hostgroup") {
hostGroup, err := FindHostGroupByIdOrName(db, name)
if err != nil {
return fmt.Errorf("unknown host group %q: %v", name, err)
}
acl.HostGroups = append(acl.HostGroups, *hostGroup)
}
if len(acl.UserGroups) == 0 {
return fmt.Errorf("an ACL must have at least one user group")
}
if len(acl.HostGroups) == 0 && acl.HostPattern == "" {
return fmt.Errorf("an ACL must have at least one host group or host pattern")
}
if err := db.Create(&acl).Error; err != nil {
return err
}
fmt.Fprintf(s, "%d\n", acl.ID)
return nil
},
}, {
Name: "inspect",
Usage: "Shows detailed information on one or more acls",
ArgsUsage: "<id> [<id> [<id>...]]",
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
return cli.ShowSubcommandHelp(c)
}
acls, err := FindACLsById(db, c.Args())
if err != nil {
return nil
}
enc := json.NewEncoder(s)
enc.SetIndent("", " ")
return enc.Encode(acls)
},
}, {
Name: "ls",
Usage: "Lists acls",
Action: func(c *cli.Context) error {
var acls []ACL
if err := db.Preload("UserGroups").Preload("HostGroups").Find(&acls).Error; err != nil {
return err
}
table := tablewriter.NewWriter(s)
table.SetHeader([]string{"ID", "User groups", "Host groups", "Host pattern", "Action", "Comment"})
table.SetBorder(false)
table.SetCaption(true, fmt.Sprintf("Total: %d acls.", len(acls)))
for _, acl := range acls {
userGroups := []string{}
hostGroups := []string{}
for _, entity := range acl.UserGroups {
userGroups = append(userGroups, entity.Name)
}
for _, entity := range acl.HostGroups {
hostGroups = append(hostGroups, entity.Name)
}
table.Append([]string{
fmt.Sprintf("%d", acl.ID),
strings.Join(userGroups, ", "),
strings.Join(hostGroups, ", "),
acl.HostPattern,
acl.Action,
acl.Comment,
})
}
table.Render()
return nil
},
}, {
Name: "rm",
Usage: "Removes one or more acls",
ArgsUsage: "<id or name> [<id or name> [<id or name>...]]",
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
return cli.ShowSubcommandHelp(c)
}
acls, err := FindACLsById(db, c.Args())
if err != nil {
return nil
}
for _, acl := range acls {
db.Where("id = ?", acl.ID).Delete(&ACL{})
fmt.Fprintf(s, "%d\n", acl.ID)
}
return nil
},
},
},
}, {
Name: "host",
Usage: "Manages hosts",
Subcommands: []cli.Command{
{
Name: "create",
Usage: "Creates a new host",
ArgsUsage: "<user>[:<password>]@<host>[:<port>]",
Description: "$> host create bart@foo.org\n $> host create bob:marley@example.com:2222",
Flags: []cli.Flag{
cli.StringFlag{Name: "name", Usage: "Assigns a name to the host"},
cli.StringFlag{Name: "password", Usage: "If present, sshportal will use password-based authentication"},
cli.StringFlag{Name: "fingerprint", Usage: "SSH host key fingerprint"},
cli.StringFlag{Name: "comment"},
cli.StringFlag{Name: "key", Usage: "ID or name of the key to use for authentication"},
cli.StringFlag{Name: "group", Usage: "Name or ID of the host group", Value: "default"},
},
Action: func(c *cli.Context) error {
if c.NArg() != 1 {
return cli.ShowSubcommandHelp(c)
}
host, err := NewHostFromURL(c.Args().First())
if err != nil {
return err
}
if c.String("password") != "" {
host.Password = c.String("password")
}
host.Fingerprint = c.String("fingerprint")
host.Name = strings.Split(host.Hostname(), ".")[0]
if c.String("name") != "" {
host.Name = c.String("name")
}
if !isNameValid(host.Name) {
return fmt.Errorf("invalid name %q", host.Name)
}
// FIXME: check if name already exists
host.Comment = c.String("comment")
inputKey := c.String("key")
if inputKey == "" && host.Password == "" {
inputKey = "default"
}
if inputKey != "" {
key, err := FindKeyByIdOrName(db, inputKey)
if err != nil {
return err
}
host.SSHKeyID = key.ID
}
// host group
hostGroup, err := FindHostGroupByIdOrName(db, c.String("group"))
if err != nil {
return err
}
host.Groups = []HostGroup{*hostGroup}
if err := db.Create(&host).Error; err != nil {
return err
}
fmt.Fprintf(s, "%d\n", host.ID)
return nil
},
}, {
Name: "inspect",
Usage: "Shows detailed information on one or more hosts",
ArgsUsage: "<id or name> [<id or name> [<id or name>...]]",
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
return cli.ShowSubcommandHelp(c)
}
hosts, err := FindHostsByIdOrName(db, c.Args())
if err != nil {
return nil
}
enc := json.NewEncoder(s)
enc.SetIndent("", " ")
return enc.Encode(hosts)
},
}, {
Name: "ls",
Usage: "Lists hosts",
Action: func(c *cli.Context) error {
var hosts []Host
if err := db.Preload("Groups").Find(&hosts).Error; err != nil {
return err
}
table := tablewriter.NewWriter(s)
table.SetHeader([]string{"ID", "Name", "URL", "Key", "Pass", "Groups", "Comment"})
table.SetBorder(false)
table.SetCaption(true, fmt.Sprintf("Total: %d hosts.", len(hosts)))
for _, host := range hosts {
authKey, authPass := "", ""
if host.Password != "" {
authPass = "X"
}
if host.SSHKeyID > 0 {
var key SSHKey
db.Model(&host).Related(&key)
authKey = key.Name
}
table.Append([]string{
fmt.Sprintf("%d", host.ID),
host.Name,
host.URL(),
authKey,
authPass,
fmt.Sprintf("%d", len(host.Groups)),
host.Comment,
//FIXME: add some stats about last access time etc
//FIXME: add creation date
})
}
table.Render()
return nil
},
}, {
Name: "rm",
Usage: "Removes one or more hosts",
ArgsUsage: "<id or name> [<id or name> [<id or name>...]]",
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
return cli.ShowSubcommandHelp(c)
}
hosts, err := FindHostsByIdOrName(db, c.Args())
if err != nil {
return nil
}
for _, host := range hosts {
db.Where("id = ?", host.ID).Delete(&Host{})
fmt.Fprintf(s, "%d\n", host.ID)
}
return nil
},
},
},
}, {
Name: "hostgroup",
Usage: "Manages host groups",
Subcommands: []cli.Command{
{
Name: "create",
Usage: "Creates a new host group",
Description: "$> hostgroup create --name=prod",
Flags: []cli.Flag{
cli.StringFlag{Name: "name", Usage: "Assigns a name to the host group"},
cli.StringFlag{Name: "comment"},
},
Action: func(c *cli.Context) error {
hostGroup := HostGroup{
Name: c.String("name"),
}
if hostGroup.Name == "" {
hostGroup.Name = namesgenerator.GetRandomName(0)
}
if !isNameValid(hostGroup.Name) {
return fmt.Errorf("invalid name %q", hostGroup.Name)
}
// FIXME: check if name already exists
hostGroup.Comment = c.String("comment")
if err := db.Create(&hostGroup).Error; err != nil {
return err
}
fmt.Fprintf(s, "%d\n", hostGroup.ID)
return nil
},
}, {
Name: "inspect",
Usage: "Shows detailed information on one or more host groups",
ArgsUsage: "<id or name> [<id or name> [<id or name>...]]",
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
return cli.ShowSubcommandHelp(c)
}
hostGroups, err := FindHostGroupsByIdOrName(db, c.Args())
if err != nil {
return nil
}
enc := json.NewEncoder(s)
enc.SetIndent("", " ")
return enc.Encode(hostGroups)
},
}, {
Name: "ls",
Usage: "Lists host groups",
Action: func(c *cli.Context) error {
var hostGroups []HostGroup
if err := db.Preload("ACLs").Preload("Hosts").Find(&hostGroups).Error; err != nil {
return err
}
table := tablewriter.NewWriter(s)
table.SetHeader([]string{"ID", "Name", "Hosts", "ACLs", "Comment"})
table.SetBorder(false)
table.SetCaption(true, fmt.Sprintf("Total: %d host groups.", len(hostGroups)))
for _, hostGroup := range hostGroups {
// FIXME: add more stats (amount of hosts, linked usergroups, ...)
table.Append([]string{
fmt.Sprintf("%d", hostGroup.ID),
hostGroup.Name,
fmt.Sprintf("%d", len(hostGroup.Hosts)),
fmt.Sprintf("%d", len(hostGroup.ACLs)),
hostGroup.Comment,
})
}
table.Render()
return nil
},
}, {
Name: "rm",
Usage: "Removes one or more host groups",
ArgsUsage: "<id or name> [<id or name> [<id or name>...]]",
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
return cli.ShowSubcommandHelp(c)
}
hostGroups, err := FindHostGroupsByIdOrName(db, c.Args())
if err != nil {
return nil
}
for _, hostGroup := range hostGroups {
db.Where("id = ?", hostGroup.ID).Delete(&HostGroup{})
fmt.Fprintf(s, "%d\n", hostGroup.ID)
}
return nil
},
},
},
}, {
Name: "info",
Usage: "Shows system-wide information",
Action: func(c *cli.Context) error {
fmt.Fprintf(s, "Debug mode (server): %v\n", globalContext.Bool("debug"))
hostname, _ := os.Hostname()
fmt.Fprintf(s, "Hostname: %s\n", hostname)
fmt.Fprintf(s, "CPUs: %d\n", runtime.NumCPU())
fmt.Fprintf(s, "Demo mode: %v\n", globalContext.Bool("demo"))
fmt.Fprintf(s, "DB Driver: %s\n", globalContext.String("db-driver"))
fmt.Fprintf(s, "DB Conn: %s\n", globalContext.String("db-conn"))
fmt.Fprintf(s, "Bind Address: %s\n", globalContext.String("bind-address"))
fmt.Fprintf(s, "System Time: %v\n", time.Now().Format(time.RFC3339Nano))
fmt.Fprintf(s, "OS Type: %s\n", runtime.GOOS)
fmt.Fprintf(s, "OS Architecture: %s\n", runtime.GOARCH)
fmt.Fprintf(s, "Go routines: %d\n", runtime.NumGoroutine())
fmt.Fprintf(s, "Go version (build): %v\n", runtime.Version())
fmt.Fprintf(s, "Uptime: %v\n", time.Since(startTime))
myself := s.Context().Value(userContextKey).(User)
fmt.Fprintf(s, "User email: %v\n", myself.ID)
fmt.Fprintf(s, "User email: %s\n", myself.Email)
// FIXME: add version
// FIXME: add info about current server (network, cpu, ram, OS)
// FIXME: add info about current user
// FIXME: add active connections
// FIXME: stats
return nil
},
}, {
Name: "key",
Usage: "Manages keys",
Subcommands: []cli.Command{
{
Name: "create",
Usage: "Creates a new key",
Description: "$> key create\n $> key create --name=mykey",
Flags: []cli.Flag{
cli.StringFlag{Name: "name", Usage: "Assigns a name to the key"},
cli.StringFlag{Name: "type", Value: "rsa"},
cli.UintFlag{Name: "length", Value: 2048},
cli.StringFlag{Name: "comment"},
},
Action: func(c *cli.Context) error {
name := namesgenerator.GetRandomName(0)
if c.String("name") != "" {
name = c.String("name")
}
if name == "" || !isNameValid(name) {
return fmt.Errorf("invalid name %q", name)
}
// FIXME: check if name already exists
key, err := NewSSHKey(c.String("type"), c.Uint("length"))
if err != nil {
return err
}
key.Name = name
key.Comment = c.String("comment")
// save the key in database
if err := db.Create(&key).Error; err != nil {
return err
}
fmt.Fprintf(s, "%d\n", key.ID)
return nil
},
}, {
Name: "inspect",
Usage: "Shows detailed information on one or more keys",
ArgsUsage: "<id or name> [<id or name> [<id or name>...]]",
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
return cli.ShowSubcommandHelp(c)
}
keys, err := FindKeysByIdOrName(db, c.Args())
if err != nil {
return nil
}
enc := json.NewEncoder(s)
enc.SetIndent("", " ")
return enc.Encode(keys)
},
}, {
Name: "ls",
Usage: "Lists keys",
Action: func(c *cli.Context) error {
var keys []SSHKey
if err := db.Preload("Hosts").Find(&keys).Error; err != nil {
return err
}
table := tablewriter.NewWriter(s)
table.SetHeader([]string{"ID", "Name", "Type", "Length", "Hosts", "Comment"})
table.SetBorder(false)
table.SetCaption(true, fmt.Sprintf("Total: %d keys.", len(keys)))
for _, key := range keys {
table.Append([]string{
fmt.Sprintf("%d", key.ID),
key.Name,
key.Type,
fmt.Sprintf("%d", key.Length),
//key.Fingerprint,
fmt.Sprintf("%d", len(key.Hosts)),
key.Comment,
//FIXME: add some stats
//FIXME: add creation date
})
}
table.Render()
return nil
},
}, {
Name: "rm",
Usage: "Removes one or more keys",
ArgsUsage: "<id or name> [<id or name> [<id or name>...]]",
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
return cli.ShowSubcommandHelp(c)
}
keys, err := FindKeysByIdOrName(db, c.Args())
if err != nil {
return nil
}
for _, key := range keys {
db.Where("id = ?", key.ID).Delete(&SSHKey{})
fmt.Fprintf(s, "%d\n", key.ID)
}
return nil
},
},
},
}, {
Name: "user",
Usage: "Manages users",
Subcommands: []cli.Command{
{
Name: "inspect",
Usage: "Shows detailed information on one or more users",
ArgsUsage: "<id or email> [<id or email> [<id or email>...]]",
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
return cli.ShowSubcommandHelp(c)
}
users, err := FindUsersByIdOrEmail(db, c.Args())
if err != nil {
return nil
}
enc := json.NewEncoder(s)
enc.SetIndent("", " ")
return enc.Encode(users)
},
}, {
Name: "invite",
ArgsUsage: "<email>",
Usage: "Invites a new user",
Description: "$> user invite bob@example.com\n $> user invite --name=Robert bob@example.com",
Flags: []cli.Flag{
cli.StringFlag{Name: "name", Usage: "Assigns a name to the user"},
cli.StringFlag{Name: "comment"},
cli.StringFlag{Name: "group", Usage: "Name or ID of the user group", Value: "default"},
},
Action: func(c *cli.Context) error {
if c.NArg() != 1 {
return cli.ShowSubcommandHelp(c)
}
// FIXME: validate email
email := c.Args().First()
name := strings.Split(email, "@")[0]
if c.String("name") != "" {
name = c.String("name")
}
user := User{
Name: name,
Email: email,
Comment: c.String("comment"),
InviteToken: RandStringBytes(16),
}
// user group
userGroup, err := FindUserGroupByIdOrName(db, c.String("group"))
if err != nil {
return err
}
user.Groups = []UserGroup{*userGroup}
// save the user in database
if err := db.Create(&user).Error; err != nil {
return err
}
fmt.Fprintf(s, "User %d created.\nTo associate this account with a key, use the following SSH user: 'invite-%s'.\n", user.ID, user.InviteToken)
return nil
},
}, {
Name: "ls",
Usage: "Lists users",
Action: func(c *cli.Context) error {
var users []User
if err := db.Preload("Groups").Preload("Keys").Find(&users).Error; err != nil {
return err
}
table := tablewriter.NewWriter(s)
table.SetHeader([]string{"ID", "Name", "Email", "Keys", "Groups", "Comment"})
table.SetBorder(false)
table.SetCaption(true, fmt.Sprintf("Total: %d users.", len(users)))
for _, user := range users {
table.Append([]string{
fmt.Sprintf("%d", user.ID),
user.Name,
user.Email,
fmt.Sprintf("%d", len(user.Keys)),
fmt.Sprintf("%d", len(user.Groups)),
user.Comment,
//FIXME: add some stats about last access time etc
//FIXME: add creation date
})
}
table.Render()
return nil
},
}, {
Name: "rm",
Usage: "Removes one or more users",
ArgsUsage: "<id or email> [<id or email> [<id or email>...]]",
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
return cli.ShowSubcommandHelp(c)
}
users, err := FindUsersByIdOrEmail(db, c.Args())
if err != nil {
return nil
}
for _, user := range users {
db.Where("id = ?", user.ID).Delete(&User{})
fmt.Fprintf(s, "%d\n", user.ID)
}
return nil
},
},
},
}, {
Name: "usergroup",
Usage: "Manages user groups",
Subcommands: []cli.Command{
{
Name: "create",
Usage: "Creates a new user group",
Description: "$> usergroup create --name=prod",
Flags: []cli.Flag{
cli.StringFlag{Name: "name", Usage: "Assigns a name to the user group"},
cli.StringFlag{Name: "comment"},
},
Action: func(c *cli.Context) error {
userGroup := UserGroup{
Name: c.String("name"),
}
if userGroup.Name == "" {
userGroup.Name = namesgenerator.GetRandomName(0)
}
if !isNameValid(userGroup.Name) {
return fmt.Errorf("invalid name %q", userGroup.Name)
}
// FIXME: check if name already exists
userGroup.Comment = c.String("comment")
// add myself to the new group
myself := s.Context().Value(userContextKey).(User)
// FIXME: use foreign key with ID to avoid updating the user with the context
userGroup.Users = []User{myself}
if err := db.Create(&userGroup).Error; err != nil {
return err
}
fmt.Fprintf(s, "%d\n", userGroup.ID)
return nil
},
}, {
Name: "inspect",
Usage: "Shows detailed information on one or more user groups",
ArgsUsage: "<id or name> [<id or name> [<id or name>...]]",
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
return cli.ShowSubcommandHelp(c)
}
userGroups, err := FindUserGroupsByIdOrName(db, c.Args())
if err != nil {
return nil
}
enc := json.NewEncoder(s)
enc.SetIndent("", " ")
return enc.Encode(userGroups)
},
}, {
Name: "ls",
Usage: "Lists user groups",
Action: func(c *cli.Context) error {
var userGroups []UserGroup
if err := db.Preload("ACLs").Preload("Users").Find(&userGroups).Error; err != nil {
return err
}
table := tablewriter.NewWriter(s)
table.SetHeader([]string{"ID", "Name", "Users", "ACLs", "Comment"})
table.SetBorder(false)
table.SetCaption(true, fmt.Sprintf("Total: %d user groups.", len(userGroups)))
for _, userGroup := range userGroups {
// FIXME: add more stats (amount of users, linked usergroups, ...)
table.Append([]string{
fmt.Sprintf("%d", userGroup.ID),
userGroup.Name,
fmt.Sprintf("%d", len(userGroup.Users)),
fmt.Sprintf("%d", len(userGroup.ACLs)),
userGroup.Comment,
})
}
table.Render()
return nil
},
}, {
Name: "rm",
Usage: "Removes one or more user groups",
ArgsUsage: "<id or name> [<id or name> [<id or name>...]]",
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
return cli.ShowSubcommandHelp(c)
}
userGroups, err := FindUserGroupsByIdOrName(db, c.Args())
if err != nil {
return nil
}
for _, userGroup := range userGroups {
db.Where("id = ?", userGroup.ID).Delete(&UserGroup{})
fmt.Fprintf(s, "%d\n", userGroup.ID)
}
return nil
},
},
},
}, {
Name: "userkey",
Usage: "Manages userkeys",
Subcommands: []cli.Command{
{
Name: "create",
ArgsUsage: "<user ID or email>",
Usage: "Creates a new userkey",
Description: "$> userkey create bob\n $> user create --name=mykey bob",
Flags: []cli.Flag{
cli.StringFlag{Name: "comment"},
},
Action: func(c *cli.Context) error {
if c.NArg() != 1 {
return cli.ShowSubcommandHelp(c)
}
user, err := FindUserByIdOrEmail(db, c.Args().First())
if err != nil {
return err
}
fmt.Fprintf(s, "Enter key:\n")
reader := bufio.NewReader(s)
text, _ := reader.ReadString('\n')
fmt.Println(text)
key, comment, _, _, err := ssh.ParseAuthorizedKey([]byte(text))
if err != nil {
return err
}
userkey := UserKey{
UserID: user.ID,
Key: key.Marshal(),
Comment: comment,
}
if c.String("comment") != "" {
userkey.Comment = c.String("comment")
}
// save the userkey in database
if err := db.Create(&userkey).Error; err != nil {
return err
}
fmt.Fprintf(s, "%d\n", userkey.ID)
return nil
},
}, {
Name: "inspect",
Usage: "Shows detailed information on one or more userkeys",
ArgsUsage: "<id> [<id> [<id>...]]",
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
return cli.ShowSubcommandHelp(c)
}
userkeys, err := FindUserkeysById(db, c.Args())
if err != nil {
return nil
}
enc := json.NewEncoder(s)
enc.SetIndent("", " ")
return enc.Encode(userkeys)
},
}, {
Name: "ls",
Usage: "Lists userkeys",
Action: func(c *cli.Context) error {
var userkeys []UserKey
if err := db.Preload("User").Find(&userkeys).Error; err != nil {
return err
}
table := tablewriter.NewWriter(s)
table.SetHeader([]string{"ID", "User", "Comment"})
table.SetBorder(false)
table.SetCaption(true, fmt.Sprintf("Total: %d userkeys.", len(userkeys)))
for _, userkey := range userkeys {
table.Append([]string{
fmt.Sprintf("%d", userkey.ID),
userkey.User.Email,
// FIXME: add fingerprint
userkey.Comment,
})
}
table.Render()
return nil
},
}, {
Name: "rm",
Usage: "Removes one or more userkeys",
ArgsUsage: "<id> [<id> [<id>...]]",
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
return cli.ShowSubcommandHelp(c)
}
userkeys, err := FindUserkeysById(db, c.Args())
if err != nil {
return nil
}
for _, userkey := range userkeys {
db.Where("id = ?", userkey.ID).Delete(&UserKey{})
fmt.Fprintf(s, "%d\n", userkey.ID)
}
return nil
},
},
},
}, {
Name: "version",
Usage: "Shows the SSHPortal version information",
Action: func(c *cli.Context) error {
fmt.Fprintf(s, "%s\n", version)
return nil
},
},
}
if len(sshCommand) == 0 { // interactive mode
term := terminal.NewTerminal(s, "config> ")
for {
line, err := term.ReadLine()
if err != nil {
return err
}
words, err := shlex.Split(line, true)
if err != nil {
io.WriteString(s, "syntax error.\n")
continue
}
if err := app.Run(append([]string{"config"}, words...)); err != nil {
io.WriteString(s, fmt.Sprintf("error: %v\n", err))
}
}
} else { // oneshot mode
if err := app.Run(append([]string{"config"}, sshCommand...)); err != nil {
io.WriteString(s, fmt.Sprintf("error: %v\n", err))
}
}
return nil
}

81
testserver.go Normal file
View File

@@ -0,0 +1,81 @@
//go:build !windows
// +build !windows
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"os/exec"
"syscall"
"unsafe"
"github.com/gliderlabs/ssh"
"github.com/kr/pty"
"github.com/urfave/cli"
)
// testServer is an hidden handler used for integration tests
func testServer(c *cli.Context) error {
ssh.Handle(func(s ssh.Session) {
helloMsg := struct {
User string
Environ []string
Command []string
}{
User: s.User(),
Environ: s.Environ(),
Command: s.Command(),
}
if err := json.NewEncoder(s).Encode(&helloMsg); err != nil {
log.Fatalf("failed to write helloMsg: %v", err)
}
cmd := exec.Command(s.Command()[0], s.Command()[1:]...) // #nosec
if s.Command() == nil {
cmd = exec.Command("/bin/sh") // #nosec
}
ptyReq, winCh, isPty := s.Pty()
var cmdErr error
if isPty {
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term))
f, err := pty.Start(cmd)
if err != nil {
fmt.Fprintf(s, "failed to run command: %v\n", err) // #nosec
_ = s.Exit(1) // #nosec
return
}
go func() {
for win := range winCh {
_, _, _ = syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ),
uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(win.Height), uint16(win.Width), 0, 0}))) // #nosec
}
}()
go func() {
// stdin
_, _ = io.Copy(f, s) // #nosec
}()
// stdout
_, _ = io.Copy(s, f) // #nosec
cmdErr = cmd.Wait()
} else {
// cmd.Stdin = s
cmd.Stdout = s
cmd.Stderr = s
cmdErr = cmd.Run()
}
if cmdErr != nil {
if exitError, ok := cmdErr.(*exec.ExitError); ok {
_ = s.Exit(exitError.Sys().(syscall.WaitStatus).ExitStatus()) // #nosec
return
}
}
_ = s.Exit(cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()) // #nosec
})
log.Println("starting ssh server on port 2222...")
return ssh.ListenAndServe(":2222", nil)
}

14
testserver_unsupported.go Normal file
View File

@@ -0,0 +1,14 @@
// +build windows
package main
import (
"fmt"
"github.com/urfave/cli"
)
// testServer is an hidden handler used for integration tests
func testServer(c *cli.Context) error {
return fmt.Errorf("not available on windows")
}

13
util.go
View File

@@ -1,13 +0,0 @@
package main
import "math/rand"
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func RandStringBytes(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}

View File

@@ -1,20 +0,0 @@
Copyright (c) anmitsu <anmitsu.s@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,38 +0,0 @@
# go-shlex
go-shlex is a library to make a lexical analyzer like Unix shell for
Go.
## Install
go get -u "github.com/anmitsu/go-shlex"
## Usage
```go
package main
import (
"fmt"
"log"
"github.com/anmitsu/go-shlex"
)
func main() {
cmd := `cp -Rdp "file name" 'file name2' dir\ name`
words, err := shlex.Split(cmd, true)
if err != nil {
log.Fatal(err)
}
for _, w := range words {
fmt.Println(w)
}
}
```
## Documentation
http://godoc.org/github.com/anmitsu/go-shlex

View File

@@ -1,193 +0,0 @@
// Package shlex provides a simple lexical analysis like Unix shell.
package shlex
import (
"bufio"
"errors"
"io"
"strings"
"unicode"
)
var (
ErrNoClosing = errors.New("No closing quotation")
ErrNoEscaped = errors.New("No escaped character")
)
// Tokenizer is the interface that classifies a token according to
// words, whitespaces, quotations, escapes and escaped quotations.
type Tokenizer interface {
IsWord(rune) bool
IsWhitespace(rune) bool
IsQuote(rune) bool
IsEscape(rune) bool
IsEscapedQuote(rune) bool
}
// DefaultTokenizer implements a simple tokenizer like Unix shell.
type DefaultTokenizer struct{}
func (t *DefaultTokenizer) IsWord(r rune) bool {
return r == '_' || unicode.IsLetter(r) || unicode.IsNumber(r)
}
func (t *DefaultTokenizer) IsQuote(r rune) bool {
switch r {
case '\'', '"':
return true
default:
return false
}
}
func (t *DefaultTokenizer) IsWhitespace(r rune) bool {
return unicode.IsSpace(r)
}
func (t *DefaultTokenizer) IsEscape(r rune) bool {
return r == '\\'
}
func (t *DefaultTokenizer) IsEscapedQuote(r rune) bool {
return r == '"'
}
// Lexer represents a lexical analyzer.
type Lexer struct {
reader *bufio.Reader
tokenizer Tokenizer
posix bool
whitespacesplit bool
}
// NewLexer creates a new Lexer reading from io.Reader. This Lexer
// has a DefaultTokenizer according to posix and whitespacesplit
// rules.
func NewLexer(r io.Reader, posix, whitespacesplit bool) *Lexer {
return &Lexer{
reader: bufio.NewReader(r),
tokenizer: &DefaultTokenizer{},
posix: posix,
whitespacesplit: whitespacesplit,
}
}
// NewLexerString creates a new Lexer reading from a string. This
// Lexer has a DefaultTokenizer according to posix and whitespacesplit
// rules.
func NewLexerString(s string, posix, whitespacesplit bool) *Lexer {
return NewLexer(strings.NewReader(s), posix, whitespacesplit)
}
// Split splits a string according to posix or non-posix rules.
func Split(s string, posix bool) ([]string, error) {
return NewLexerString(s, posix, true).Split()
}
// SetTokenizer sets a Tokenizer.
func (l *Lexer) SetTokenizer(t Tokenizer) {
l.tokenizer = t
}
func (l *Lexer) Split() ([]string, error) {
result := make([]string, 0)
for {
token, err := l.readToken()
if token != "" {
result = append(result, token)
}
if err == io.EOF {
break
} else if err != nil {
return result, err
}
}
return result, nil
}
func (l *Lexer) readToken() (string, error) {
t := l.tokenizer
token := ""
quoted := false
state := ' '
escapedstate := ' '
scanning:
for {
next, _, err := l.reader.ReadRune()
if err != nil {
if t.IsQuote(state) {
return token, ErrNoClosing
} else if t.IsEscape(state) {
return token, ErrNoEscaped
}
return token, err
}
switch {
case t.IsWhitespace(state):
switch {
case t.IsWhitespace(next):
break scanning
case l.posix && t.IsEscape(next):
escapedstate = 'a'
state = next
case t.IsWord(next):
token += string(next)
state = 'a'
case t.IsQuote(next):
if !l.posix {
token += string(next)
}
state = next
default:
token = string(next)
if l.whitespacesplit {
state = 'a'
} else if token != "" || (l.posix && quoted) {
break scanning
}
}
case t.IsQuote(state):
quoted = true
switch {
case next == state:
if !l.posix {
token += string(next)
break scanning
} else {
state = 'a'
}
case l.posix && t.IsEscape(next) && t.IsEscapedQuote(state):
escapedstate = state
state = next
default:
token += string(next)
}
case t.IsEscape(state):
if t.IsQuote(escapedstate) && next != state && next != escapedstate {
token += string(state)
}
token += string(next)
state = escapedstate
case t.IsWord(state):
switch {
case t.IsWhitespace(next):
if token != "" || (l.posix && quoted) {
break scanning
}
case l.posix && t.IsQuote(next):
state = next
case l.posix && t.IsEscape(next):
escapedstate = 'a'
state = next
case t.IsWord(next) || t.IsQuote(next):
token += string(next)
default:
if l.whitespacesplit {
token += string(next)
} else if token != "" {
l.reader.UnreadRune()
break scanning
}
}
}
}
return token, nil
}

View File

@@ -1,27 +0,0 @@
Copyright (c) 2016 Glider Labs. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Glider Labs nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,96 +0,0 @@
# gliderlabs/ssh
[![GoDoc](https://godoc.org/github.com/gliderlabs/ssh?status.svg)](https://godoc.org/github.com/gliderlabs/ssh)
[![CircleCI](https://img.shields.io/circleci/project/github/gliderlabs/ssh.svg)](https://circleci.com/gh/gliderlabs/ssh)
[![Go Report Card](https://goreportcard.com/badge/github.com/gliderlabs/ssh)](https://goreportcard.com/report/github.com/gliderlabs/ssh)
[![OpenCollective](https://opencollective.com/ssh/sponsors/badge.svg)](#sponsors)
[![Slack](http://slack.gliderlabs.com/badge.svg)](http://slack.gliderlabs.com)
[![Email Updates](https://img.shields.io/badge/updates-subscribe-yellow.svg)](https://app.convertkit.com/landing_pages/243312)
> The Glider Labs SSH server package is dope. &mdash;[@bradfitz](https://twitter.com/bradfitz), Go team member
This Go package wraps the [crypto/ssh
package](https://godoc.org/golang.org/x/crypto/ssh) with a higher-level API for
building SSH servers. The goal of the API was to make it as simple as using
[net/http](https://golang.org/pkg/net/http/), so the API is very similar:
```
package main
import (
"github.com/gliderlabs/ssh"
"io"
"log"
)
func main() {
ssh.Handle(func(s ssh.Session) {
io.WriteString(s, "Hello world\n")
})
log.Fatal(ssh.ListenAndServe(":2222", nil))
}
```
This package was built by [@progrium](https://twitter.com/progrium) after working on nearly a dozen projects at Glider Labs using SSH and collaborating with [@shazow](https://twitter.com/shazow) (known for [ssh-chat](https://github.com/shazow/ssh-chat)).
## Examples
A bunch of great examples are in the `_examples` directory.
## Usage
[See GoDoc reference.](https://godoc.org/github.com/gliderlabs/ssh)
## Contributing
Pull requests are welcome! However, since this project is very much about API
design, please submit API changes as issues to discuss before submitting PRs.
Also, you can [join our Slack](http://slack.gliderlabs.com) to discuss as well.
## Roadmap
* Non-session channel handlers
* Cleanup callback API
* 1.0 release
* High-level client?
## Sponsors
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/ssh#sponsor)]
<a href="https://opencollective.com/ssh/sponsor/0/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/1/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/2/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/3/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/4/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/5/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/6/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/7/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/8/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/9/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/9/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/10/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/10/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/11/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/11/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/12/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/12/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/13/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/13/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/14/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/14/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/15/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/15/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/16/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/16/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/17/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/17/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/18/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/18/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/19/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/19/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/20/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/20/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/21/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/21/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/22/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/22/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/23/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/23/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/24/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/24/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/25/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/25/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/26/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/26/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/27/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/27/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/28/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/28/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/29/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/29/avatar.svg"></a>
## License
BSD

View File

@@ -1,81 +0,0 @@
package ssh
import (
"io"
"io/ioutil"
"net"
"path"
"sync"
gossh "golang.org/x/crypto/ssh"
)
const (
agentRequestType = "auth-agent-req@openssh.com"
agentChannelType = "auth-agent@openssh.com"
agentTempDir = "auth-agent"
agentListenFile = "listener.sock"
)
// contextKeyAgentRequest is an internal context key for storing if the
// client requested agent forwarding
var contextKeyAgentRequest = &contextKey{"auth-agent-req"}
func setAgentRequested(sess *session) {
sess.ctx.SetValue(contextKeyAgentRequest, true)
}
// AgentRequested returns true if the client requested agent forwarding.
func AgentRequested(sess Session) bool {
return sess.Context().Value(contextKeyAgentRequest) == true
}
// NewAgentListener sets up a temporary Unix socket that can be communicated
// to the session environment and used for forwarding connections.
func NewAgentListener() (net.Listener, error) {
dir, err := ioutil.TempDir("", agentTempDir)
if err != nil {
return nil, err
}
l, err := net.Listen("unix", path.Join(dir, agentListenFile))
if err != nil {
return nil, err
}
return l, nil
}
// ForwardAgentConnections takes connections from a listener to proxy into the
// session on the OpenSSH channel for agent connections. It blocks and services
// connections until the listener stop accepting.
func ForwardAgentConnections(l net.Listener, s Session) {
sshConn := s.Context().Value(ContextKeyConn).(gossh.Conn)
for {
conn, err := l.Accept()
if err != nil {
return
}
go func(conn net.Conn) {
defer conn.Close()
channel, reqs, err := sshConn.OpenChannel(agentChannelType, nil)
if err != nil {
return
}
defer channel.Close()
go gossh.DiscardRequests(reqs)
var wg sync.WaitGroup
wg.Add(2)
go func() {
io.Copy(conn, channel)
conn.(*net.UnixConn).CloseWrite()
wg.Done()
}()
go func() {
io.Copy(channel, conn)
channel.CloseWrite()
wg.Done()
}()
wg.Wait()
}(conn)
}
}

View File

@@ -1,10 +0,0 @@
version: 2.0
jobs:
build:
docker:
- image: golang:1.8
working_directory: /go/src/github.com/gliderlabs/ssh
steps:
- checkout
- run: go get
- run: go test -v -race

View File

@@ -1,55 +0,0 @@
package ssh
import (
"context"
"net"
"time"
)
type serverConn struct {
net.Conn
idleTimeout time.Duration
maxDeadline time.Time
closeCanceler context.CancelFunc
}
func (c *serverConn) Write(p []byte) (n int, err error) {
c.updateDeadline()
n, err = c.Conn.Write(p)
if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
c.closeCanceler()
}
return
}
func (c *serverConn) Read(b []byte) (n int, err error) {
c.updateDeadline()
n, err = c.Conn.Read(b)
if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
c.closeCanceler()
}
return
}
func (c *serverConn) Close() (err error) {
err = c.Conn.Close()
if c.closeCanceler != nil {
c.closeCanceler()
}
return
}
func (c *serverConn) updateDeadline() {
switch {
case c.idleTimeout > 0:
idleDeadline := time.Now().Add(c.idleTimeout)
if idleDeadline.Unix() < c.maxDeadline.Unix() {
c.Conn.SetDeadline(idleDeadline)
return
}
fallthrough
default:
c.Conn.SetDeadline(c.maxDeadline)
}
}

View File

@@ -1,148 +0,0 @@
package ssh
import (
"context"
"encoding/hex"
"net"
gossh "golang.org/x/crypto/ssh"
)
// contextKey is a value for use with context.WithValue. It's used as
// a pointer so it fits in an interface{} without allocation.
type contextKey struct {
name string
}
var (
// ContextKeyUser is a context key for use with Contexts in this package.
// The associated value will be of type string.
ContextKeyUser = &contextKey{"user"}
// ContextKeySessionID is a context key for use with Contexts in this package.
// The associated value will be of type string.
ContextKeySessionID = &contextKey{"session-id"}
// ContextKeyPermissions is a context key for use with Contexts in this package.
// The associated value will be of type *Permissions.
ContextKeyPermissions = &contextKey{"permissions"}
// ContextKeyClientVersion is a context key for use with Contexts in this package.
// The associated value will be of type string.
ContextKeyClientVersion = &contextKey{"client-version"}
// ContextKeyServerVersion is a context key for use with Contexts in this package.
// The associated value will be of type string.
ContextKeyServerVersion = &contextKey{"server-version"}
// ContextKeyLocalAddr is a context key for use with Contexts in this package.
// The associated value will be of type net.Addr.
ContextKeyLocalAddr = &contextKey{"local-addr"}
// ContextKeyRemoteAddr is a context key for use with Contexts in this package.
// The associated value will be of type net.Addr.
ContextKeyRemoteAddr = &contextKey{"remote-addr"}
// ContextKeyServer is a context key for use with Contexts in this package.
// The associated value will be of type *Server.
ContextKeyServer = &contextKey{"ssh-server"}
// ContextKeyConn is a context key for use with Contexts in this package.
// The associated value will be of type gossh.Conn.
ContextKeyConn = &contextKey{"ssh-conn"}
// ContextKeyPublicKey is a context key for use with Contexts in this package.
// The associated value will be of type PublicKey.
ContextKeyPublicKey = &contextKey{"public-key"}
)
// Context is a package specific context interface. It exposes connection
// metadata and allows new values to be easily written to it. It's used in
// authentication handlers and callbacks, and its underlying context.Context is
// exposed on Session in the session Handler.
type Context interface {
context.Context
// User returns the username used when establishing the SSH connection.
User() string
// SessionID returns the session hash.
SessionID() string
// ClientVersion returns the version reported by the client.
ClientVersion() string
// ServerVersion returns the version reported by the server.
ServerVersion() string
// RemoteAddr returns the remote address for this connection.
RemoteAddr() net.Addr
// LocalAddr returns the local address for this connection.
LocalAddr() net.Addr
// Permissions returns the Permissions object used for this connection.
Permissions() *Permissions
// SetValue allows you to easily write new values into the underlying context.
SetValue(key, value interface{})
}
type sshContext struct {
context.Context
}
func newContext(srv *Server) (*sshContext, context.CancelFunc) {
innerCtx, cancel := context.WithCancel(context.Background())
ctx := &sshContext{innerCtx}
ctx.SetValue(ContextKeyServer, srv)
perms := &Permissions{&gossh.Permissions{}}
ctx.SetValue(ContextKeyPermissions, perms)
return ctx, cancel
}
// this is separate from newContext because we will get ConnMetadata
// at different points so it needs to be applied separately
func (ctx *sshContext) applyConnMetadata(conn gossh.ConnMetadata) {
if ctx.Value(ContextKeySessionID) != nil {
return
}
ctx.SetValue(ContextKeySessionID, hex.EncodeToString(conn.SessionID()))
ctx.SetValue(ContextKeyClientVersion, string(conn.ClientVersion()))
ctx.SetValue(ContextKeyServerVersion, string(conn.ServerVersion()))
ctx.SetValue(ContextKeyUser, conn.User())
ctx.SetValue(ContextKeyLocalAddr, conn.LocalAddr())
ctx.SetValue(ContextKeyRemoteAddr, conn.RemoteAddr())
}
func (ctx *sshContext) SetValue(key, value interface{}) {
ctx.Context = context.WithValue(ctx.Context, key, value)
}
func (ctx *sshContext) User() string {
return ctx.Value(ContextKeyUser).(string)
}
func (ctx *sshContext) SessionID() string {
return ctx.Value(ContextKeySessionID).(string)
}
func (ctx *sshContext) ClientVersion() string {
return ctx.Value(ContextKeyClientVersion).(string)
}
func (ctx *sshContext) ServerVersion() string {
return ctx.Value(ContextKeyServerVersion).(string)
}
func (ctx *sshContext) RemoteAddr() net.Addr {
return ctx.Value(ContextKeyRemoteAddr).(net.Addr)
}
func (ctx *sshContext) LocalAddr() net.Addr {
return ctx.Value(ContextKeyLocalAddr).(net.Addr)
}
func (ctx *sshContext) Permissions() *Permissions {
return ctx.Value(ContextKeyPermissions).(*Permissions)
}

View File

@@ -1,47 +0,0 @@
/*
Package ssh wraps the crypto/ssh package with a higher-level API for building
SSH servers. The goal of the API was to make it as simple as using net/http, so
the API is very similar.
You should be able to build any SSH server using only this package, which wraps
relevant types and some functions from crypto/ssh. However, you still need to
use crypto/ssh for building SSH clients.
ListenAndServe starts an SSH server with a given address, handler, and options. The
handler is usually nil, which means to use DefaultHandler. Handle sets DefaultHandler:
ssh.Handle(func(s ssh.Session) {
io.WriteString(s, "Hello world\n")
})
log.Fatal(ssh.ListenAndServe(":2222", nil))
If you don't specify a host key, it will generate one every time. This is convenient
except you'll have to deal with clients being confused that the host key is different.
It's a better idea to generate or point to an existing key on your system:
log.Fatal(ssh.ListenAndServe(":2222", nil, ssh.HostKeyFile("/Users/progrium/.ssh/id_rsa")))
Although all options have functional option helpers, another way to control the
server's behavior is by creating a custom Server:
s := &ssh.Server{
Addr: ":2222",
Handler: sessionHandler,
PublicKeyHandler: authHandler,
}
s.AddHostKey(hostKeySigner)
log.Fatal(s.ListenAndServe())
This package automatically handles basic SSH requests like setting environment
variables, requesting PTY, and changing window size. These requests are
processed, responded to, and any relevant state is updated. This state is then
exposed to you via the Session interface.
The one big feature missing from the Session abstraction is signals. This was
started, but not completed. Pull Requests welcome!
*/
package ssh

View File

@@ -1,77 +0,0 @@
package ssh
import (
"io/ioutil"
gossh "golang.org/x/crypto/ssh"
)
// PasswordAuth returns a functional option that sets PasswordHandler on the server.
func PasswordAuth(fn PasswordHandler) Option {
return func(srv *Server) error {
srv.PasswordHandler = fn
return nil
}
}
// PublicKeyAuth returns a functional option that sets PublicKeyHandler on the server.
func PublicKeyAuth(fn PublicKeyHandler) Option {
return func(srv *Server) error {
srv.PublicKeyHandler = fn
return nil
}
}
// HostKeyFile returns a functional option that adds HostSigners to the server
// from a PEM file at filepath.
func HostKeyFile(filepath string) Option {
return func(srv *Server) error {
pemBytes, err := ioutil.ReadFile(filepath)
if err != nil {
return err
}
signer, err := gossh.ParsePrivateKey(pemBytes)
if err != nil {
return err
}
srv.AddHostKey(signer)
return nil
}
}
// HostKeyPEM returns a functional option that adds HostSigners to the server
// from a PEM file as bytes.
func HostKeyPEM(bytes []byte) Option {
return func(srv *Server) error {
signer, err := gossh.ParsePrivateKey(bytes)
if err != nil {
return err
}
srv.AddHostKey(signer)
return nil
}
}
// NoPty returns a functional option that sets PtyCallback to return false,
// denying PTY requests.
func NoPty() Option {
return func(srv *Server) error {
srv.PtyCallback = func(ctx Context, pty Pty) bool {
return false
}
return nil
}
}
// WrapConn returns a functional option that sets ConnCallback on the server.
func WrapConn(fn ConnCallback) Option {
return func(srv *Server) error {
srv.ConnCallback = fn
return nil
}
}

View File

@@ -1,332 +0,0 @@
package ssh
import (
"context"
"errors"
"fmt"
"net"
"sync"
"time"
gossh "golang.org/x/crypto/ssh"
)
// ErrServerClosed is returned by the Server's Serve, ListenAndServe,
// and ListenAndServeTLS methods after a call to Shutdown or Close.
var ErrServerClosed = errors.New("ssh: Server closed")
// Server defines parameters for running an SSH server. The zero value for
// Server is a valid configuration. When both PasswordHandler and
// PublicKeyHandler are nil, no client authentication is performed.
type Server struct {
Addr string // TCP address to listen on, ":22" if empty
Handler Handler // handler to invoke, ssh.DefaultHandler if nil
HostSigners []Signer // private keys for the host key, must have at least one
Version string // server version to be sent before the initial handshake
PasswordHandler PasswordHandler // password authentication handler
PublicKeyHandler PublicKeyHandler // public key authentication handler
PtyCallback PtyCallback // callback for allowing PTY sessions, allows all if nil
ConnCallback ConnCallback // optional callback for wrapping net.Conn before handling
LocalPortForwardingCallback LocalPortForwardingCallback // callback for allowing local port forwarding, denies all if nil
IdleTimeout time.Duration // connection timeout when no activity, none if empty
MaxTimeout time.Duration // absolute connection timeout, none if empty
channelHandlers map[string]channelHandler
mu sync.Mutex
listeners map[net.Listener]struct{}
conns map[*gossh.ServerConn]struct{}
doneChan chan struct{}
}
// internal for now
type channelHandler func(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx *sshContext)
func (srv *Server) ensureHostSigner() error {
if len(srv.HostSigners) == 0 {
signer, err := generateSigner()
if err != nil {
return err
}
srv.HostSigners = append(srv.HostSigners, signer)
}
return nil
}
func (srv *Server) config(ctx *sshContext) *gossh.ServerConfig {
srv.channelHandlers = map[string]channelHandler{
"session": sessionHandler,
"direct-tcpip": directTcpipHandler,
}
config := &gossh.ServerConfig{}
for _, signer := range srv.HostSigners {
config.AddHostKey(signer)
}
if srv.PasswordHandler == nil && srv.PublicKeyHandler == nil {
config.NoClientAuth = true
}
if srv.Version != "" {
config.ServerVersion = "SSH-2.0-" + srv.Version
}
if srv.PasswordHandler != nil {
config.PasswordCallback = func(conn gossh.ConnMetadata, password []byte) (*gossh.Permissions, error) {
ctx.applyConnMetadata(conn)
if ok := srv.PasswordHandler(ctx, string(password)); !ok {
return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
}
return ctx.Permissions().Permissions, nil
}
}
if srv.PublicKeyHandler != nil {
config.PublicKeyCallback = func(conn gossh.ConnMetadata, key gossh.PublicKey) (*gossh.Permissions, error) {
ctx.applyConnMetadata(conn)
if ok := srv.PublicKeyHandler(ctx, key); !ok {
return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
}
ctx.SetValue(ContextKeyPublicKey, key)
return ctx.Permissions().Permissions, nil
}
}
return config
}
// Handle sets the Handler for the server.
func (srv *Server) Handle(fn Handler) {
srv.Handler = fn
}
// Close immediately closes all active listeners and all active
// connections.
//
// Close returns any error returned from closing the Server's
// underlying Listener(s).
func (srv *Server) Close() error {
srv.mu.Lock()
defer srv.mu.Unlock()
srv.closeDoneChanLocked()
err := srv.closeListenersLocked()
for c := range srv.conns {
c.Close()
delete(srv.conns, c)
}
return err
}
// shutdownPollInterval is how often we poll for quiescence
// during Server.Shutdown. This is lower during tests, to
// speed up tests.
// Ideally we could find a solution that doesn't involve polling,
// but which also doesn't have a high runtime cost (and doesn't
// involve any contentious mutexes), but that is left as an
// exercise for the reader.
var shutdownPollInterval = 500 * time.Millisecond
// Shutdown gracefully shuts down the server without interrupting any
// active connections. Shutdown works by first closing all open
// listeners, and then waiting indefinitely for connections to close.
// If the provided context expires before the shutdown is complete,
// then the context's error is returned.
func (srv *Server) Shutdown(ctx context.Context) error {
srv.mu.Lock()
lnerr := srv.closeListenersLocked()
srv.closeDoneChanLocked()
srv.mu.Unlock()
ticker := time.NewTicker(shutdownPollInterval)
defer ticker.Stop()
for {
srv.mu.Lock()
conns := len(srv.conns)
srv.mu.Unlock()
if conns == 0 {
return lnerr
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
}
// Serve accepts incoming connections on the Listener l, creating a new
// connection goroutine for each. The connection goroutines read requests and then
// calls srv.Handler to handle sessions.
//
// Serve always returns a non-nil error.
func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
if err := srv.ensureHostSigner(); err != nil {
return err
}
if srv.Handler == nil {
srv.Handler = DefaultHandler
}
var tempDelay time.Duration
srv.trackListener(l, true)
defer srv.trackListener(l, false)
for {
conn, e := l.Accept()
if e != nil {
select {
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
if ne, ok := e.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
time.Sleep(tempDelay)
continue
}
return e
}
go srv.handleConn(conn)
}
}
func (srv *Server) handleConn(newConn net.Conn) {
if srv.ConnCallback != nil {
cbConn := srv.ConnCallback(newConn)
if cbConn == nil {
newConn.Close()
return
}
newConn = cbConn
}
ctx, cancel := newContext(srv)
conn := &serverConn{
Conn: newConn,
idleTimeout: srv.IdleTimeout,
closeCanceler: cancel,
}
if srv.MaxTimeout > 0 {
conn.maxDeadline = time.Now().Add(srv.MaxTimeout)
}
defer conn.Close()
sshConn, chans, reqs, err := gossh.NewServerConn(conn, srv.config(ctx))
if err != nil {
// TODO: trigger event callback
return
}
srv.trackConn(sshConn, true)
defer srv.trackConn(sshConn, false)
ctx.SetValue(ContextKeyConn, sshConn)
ctx.applyConnMetadata(sshConn)
go gossh.DiscardRequests(reqs)
for ch := range chans {
handler, found := srv.channelHandlers[ch.ChannelType()]
if !found {
ch.Reject(gossh.UnknownChannelType, "unsupported channel type")
continue
}
go handler(srv, sshConn, ch, ctx)
}
}
// ListenAndServe listens on the TCP network address srv.Addr and then calls
// Serve to handle incoming connections. If srv.Addr is blank, ":22" is used.
// ListenAndServe always returns a non-nil error.
func (srv *Server) ListenAndServe() error {
addr := srv.Addr
if addr == "" {
addr = ":22"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(ln)
}
// AddHostKey adds a private key as a host key. If an existing host key exists
// with the same algorithm, it is overwritten. Each server config must have at
// least one host key.
func (srv *Server) AddHostKey(key Signer) {
// these are later added via AddHostKey on ServerConfig, which performs the
// check for one of every algorithm.
srv.HostSigners = append(srv.HostSigners, key)
}
// SetOption runs a functional option against the server.
func (srv *Server) SetOption(option Option) error {
return option(srv)
}
func (srv *Server) getDoneChan() <-chan struct{} {
srv.mu.Lock()
defer srv.mu.Unlock()
return srv.getDoneChanLocked()
}
func (srv *Server) getDoneChanLocked() chan struct{} {
if srv.doneChan == nil {
srv.doneChan = make(chan struct{})
}
return srv.doneChan
}
func (srv *Server) closeDoneChanLocked() {
ch := srv.getDoneChanLocked()
select {
case <-ch:
// Already closed. Don't close again.
default:
// Safe to close here. We're the only closer, guarded
// by srv.mu.
close(ch)
}
}
func (srv *Server) closeListenersLocked() error {
var err error
for ln := range srv.listeners {
if cerr := ln.Close(); cerr != nil && err == nil {
err = cerr
}
delete(srv.listeners, ln)
}
return err
}
func (srv *Server) trackListener(ln net.Listener, add bool) {
srv.mu.Lock()
defer srv.mu.Unlock()
if srv.listeners == nil {
srv.listeners = make(map[net.Listener]struct{})
}
if add {
// If the *Server is being reused after a previous
// Close or Shutdown, reset its doneChan:
if len(srv.listeners) == 0 && len(srv.conns) == 0 {
srv.doneChan = nil
}
srv.listeners[ln] = struct{}{}
} else {
delete(srv.listeners, ln)
}
}
func (srv *Server) trackConn(c *gossh.ServerConn, add bool) {
srv.mu.Lock()
defer srv.mu.Unlock()
if srv.conns == nil {
srv.conns = make(map[*gossh.ServerConn]struct{})
}
if add {
srv.conns[c] = struct{}{}
} else {
delete(srv.conns, c)
}
}

View File

@@ -1,301 +0,0 @@
package ssh
import (
"bytes"
"context"
"errors"
"fmt"
"net"
"sync"
"github.com/anmitsu/go-shlex"
gossh "golang.org/x/crypto/ssh"
)
// Session provides access to information about an SSH session and methods
// to read and write to the SSH channel with an embedded Channel interface from
// cypto/ssh.
//
// When Command() returns an empty slice, the user requested a shell. Otherwise
// the user is performing an exec with those command arguments.
//
// TODO: Signals
type Session interface {
gossh.Channel
// User returns the username used when establishing the SSH connection.
User() string
// RemoteAddr returns the net.Addr of the client side of the connection.
RemoteAddr() net.Addr
// LocalAddr returns the net.Addr of the server side of the connection.
LocalAddr() net.Addr
// Environ returns a copy of strings representing the environment set by the
// user for this session, in the form "key=value".
Environ() []string
// Exit sends an exit status and then closes the session.
Exit(code int) error
// Command returns a shell parsed slice of arguments that were provided by the
// user. Shell parsing splits the command string according to POSIX shell rules,
// which considers quoting not just whitespace.
Command() []string
// PublicKey returns the PublicKey used to authenticate. If a public key was not
// used it will return nil.
PublicKey() PublicKey
// Context returns the connection's context. The returned context is always
// non-nil and holds the same data as the Context passed into auth
// handlers and callbacks.
//
// The context is canceled when the client's connection closes or I/O
// operation fails.
Context() context.Context
// Permissions returns a copy of the Permissions object that was available for
// setup in the auth handlers via the Context.
Permissions() Permissions
// Pty returns PTY information, a channel of window size changes, and a boolean
// of whether or not a PTY was accepted for this session.
Pty() (Pty, <-chan Window, bool)
// Signals registers a channel to receive signals sent from the client. The
// channel must handle signal sends or it will block the SSH request loop.
// Registering nil will unregister the channel from signal sends. During the
// time no channel is registered signals are buffered up to a reasonable amount.
// If there are buffered signals when a channel is registered, they will be
// sent in order on the channel immediately after registering.
Signals(c chan<- Signal)
MaskedReqs() chan *gossh.Request
}
// maxSigBufSize is how many signals will be buffered
// when there is no signal channel specified
const maxSigBufSize = 128
func sessionHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx *sshContext) {
ch, reqs, err := newChan.Accept()
if err != nil {
// TODO: trigger event callback
return
}
sess := &session{
Channel: ch,
conn: conn,
handler: srv.Handler,
ptyCb: srv.PtyCallback,
maskedReqs: make(chan *gossh.Request, 5),
ctx: ctx,
}
sess.handleRequests(reqs)
}
type session struct {
sync.Mutex
gossh.Channel
conn *gossh.ServerConn
handler Handler
handled bool
exited bool
pty *Pty
winch chan Window
env []string
ptyCb PtyCallback
cmd []string
ctx *sshContext
sigCh chan<- Signal
sigBuf []Signal
maskedReqs chan *gossh.Request
}
func (sess *session) Write(p []byte) (n int, err error) {
if sess.pty != nil {
m := len(p)
// normalize \n to \r\n when pty is accepted.
// this is a hardcoded shortcut since we don't support terminal modes.
p = bytes.Replace(p, []byte{'\n'}, []byte{'\r', '\n'}, -1)
p = bytes.Replace(p, []byte{'\r', '\r', '\n'}, []byte{'\r', '\n'}, -1)
n, err = sess.Channel.Write(p)
if n > m {
n = m
}
return
}
return sess.Channel.Write(p)
}
func (sess *session) PublicKey() PublicKey {
sessionkey := sess.ctx.Value(ContextKeyPublicKey)
if sessionkey == nil {
return nil
}
return sessionkey.(PublicKey)
}
func (sess *session) Permissions() Permissions {
// use context permissions because its properly
// wrapped and easier to dereference
perms := sess.ctx.Value(ContextKeyPermissions).(*Permissions)
return *perms
}
func (sess *session) Context() context.Context {
return sess.ctx.Context
}
func (sess *session) Exit(code int) error {
sess.Lock()
defer sess.Unlock()
if sess.exited {
return errors.New("Session.Exit called multiple times")
}
sess.exited = true
status := struct{ Status uint32 }{uint32(code)}
_, err := sess.SendRequest("exit-status", false, gossh.Marshal(&status))
if err != nil {
return err
}
close(sess.maskedReqs)
return sess.Close()
}
func (sess *session) User() string {
return sess.conn.User()
}
func (sess *session) RemoteAddr() net.Addr {
return sess.conn.RemoteAddr()
}
func (sess *session) LocalAddr() net.Addr {
return sess.conn.LocalAddr()
}
func (sess *session) Environ() []string {
return append([]string(nil), sess.env...)
}
func (sess *session) Command() []string {
return append([]string(nil), sess.cmd...)
}
func (sess *session) Pty() (Pty, <-chan Window, bool) {
if sess.pty != nil {
return *sess.pty, sess.winch, true
}
return Pty{}, sess.winch, false
}
func (sess *session) Signals(c chan<- Signal) {
sess.Lock()
defer sess.Unlock()
sess.sigCh = c
if len(sess.sigBuf) > 0 {
go func() {
for _, sig := range sess.sigBuf {
sess.sigCh <- sig
}
}()
}
}
func (sess *session) MaskedReqs() chan *gossh.Request {
return sess.maskedReqs
}
func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
for req := range reqs {
switch req.Type {
case "shell", "exec":
if sess.handled {
req.Reply(false, nil)
continue
}
sess.handled = true
req.Reply(true, nil)
var payload = struct{ Value string }{}
gossh.Unmarshal(req.Payload, &payload)
sess.cmd, _ = shlex.Split(payload.Value, true)
go func() {
sess.handler(sess)
sess.Exit(0)
}()
case "env":
if sess.handled {
req.Reply(false, nil)
continue
}
var kv struct{ Key, Value string }
gossh.Unmarshal(req.Payload, &kv)
sess.env = append(sess.env, fmt.Sprintf("%s=%s", kv.Key, kv.Value))
req.Reply(true, nil)
case "signal":
var payload struct{ Signal string }
gossh.Unmarshal(req.Payload, &payload)
sess.Lock()
if sess.sigCh != nil {
sess.sigCh <- Signal(payload.Signal)
} else {
if len(sess.sigBuf) < maxSigBufSize {
sess.sigBuf = append(sess.sigBuf, Signal(payload.Signal))
}
}
sess.Unlock()
case "pty-req":
if sess.handled || sess.pty != nil {
req.Reply(false, nil)
continue
}
ptyReq, ok := parsePtyRequest(req.Payload)
if !ok {
req.Reply(false, nil)
continue
}
if sess.ptyCb != nil {
ok := sess.ptyCb(sess.ctx, ptyReq)
if !ok {
req.Reply(false, nil)
continue
}
}
sess.pty = &ptyReq
sess.winch = make(chan Window, 1)
sess.winch <- ptyReq.Window
defer func() {
// when reqs is closed
close(sess.winch)
}()
req.Reply(ok, nil)
case "window-change":
if sess.pty == nil {
req.Reply(false, nil)
continue
}
win, ok := parseWinchRequest(req.Payload)
if ok {
sess.pty.Window = win
sess.winch <- win
}
req.Reply(ok, nil)
case agentRequestType:
// TODO: option/callback to allow agent forwarding
setAgentRequested(sess)
req.Reply(true, nil)
default:
// TODO: debug log
}
sess.maskedReqs <- req
}
}

View File

@@ -1,109 +0,0 @@
package ssh
import (
"crypto/subtle"
"net"
)
type Signal string
// POSIX signals as listed in RFC 4254 Section 6.10.
const (
SIGABRT Signal = "ABRT"
SIGALRM Signal = "ALRM"
SIGFPE Signal = "FPE"
SIGHUP Signal = "HUP"
SIGILL Signal = "ILL"
SIGINT Signal = "INT"
SIGKILL Signal = "KILL"
SIGPIPE Signal = "PIPE"
SIGQUIT Signal = "QUIT"
SIGSEGV Signal = "SEGV"
SIGTERM Signal = "TERM"
SIGUSR1 Signal = "USR1"
SIGUSR2 Signal = "USR2"
)
// DefaultHandler is the default Handler used by Serve.
var DefaultHandler Handler
// Option is a functional option handler for Server.
type Option func(*Server) error
// Handler is a callback for handling established SSH sessions.
type Handler func(Session)
// PublicKeyHandler is a callback for performing public key authentication.
type PublicKeyHandler func(ctx Context, key PublicKey) bool
// PasswordHandler is a callback for performing password authentication.
type PasswordHandler func(ctx Context, password string) bool
// PtyCallback is a hook for allowing PTY sessions.
type PtyCallback func(ctx Context, pty Pty) bool
// ConnCallback is a hook for new connections before handling.
// It allows wrapping for timeouts and limiting by returning
// the net.Conn that will be used as the underlying connection.
type ConnCallback func(conn net.Conn) net.Conn
// LocalPortForwardingCallback is a hook for allowing port forwarding
type LocalPortForwardingCallback func(ctx Context, destinationHost string, destinationPort uint32) bool
// Window represents the size of a PTY window.
type Window struct {
Width int
Height int
}
// Pty represents a PTY request and configuration.
type Pty struct {
Term string
Window Window
// HELP WANTED: terminal modes!
}
// Serve accepts incoming SSH connections on the listener l, creating a new
// connection goroutine for each. The connection goroutines read requests and
// then calls handler to handle sessions. Handler is typically nil, in which
// case the DefaultHandler is used.
func Serve(l net.Listener, handler Handler, options ...Option) error {
srv := &Server{Handler: handler}
for _, option := range options {
if err := srv.SetOption(option); err != nil {
return err
}
}
return srv.Serve(l)
}
// ListenAndServe listens on the TCP network address addr and then calls Serve
// with handler to handle sessions on incoming connections. Handler is typically
// nil, in which case the DefaultHandler is used.
func ListenAndServe(addr string, handler Handler, options ...Option) error {
srv := &Server{Addr: addr, Handler: handler}
for _, option := range options {
if err := srv.SetOption(option); err != nil {
return err
}
}
return srv.ListenAndServe()
}
// Handle registers the handler as the DefaultHandler.
func Handle(handler Handler) {
DefaultHandler = handler
}
// KeysEqual is constant time compare of the keys to avoid timing attacks.
func KeysEqual(ak, bk PublicKey) bool {
//avoid panic if one of the keys is nil, return false instead
if ak == nil || bk == nil {
return false
}
a := ak.Marshal()
b := bk.Marshal()
return (len(a) == len(b) && subtle.ConstantTimeCompare(a, b) == 1)
}

View File

@@ -1,58 +0,0 @@
package ssh
import (
"fmt"
"io"
"net"
gossh "golang.org/x/crypto/ssh"
)
// direct-tcpip data struct as specified in RFC4254, Section 7.2
type forwardData struct {
DestinationHost string
DestinationPort uint32
OriginatorHost string
OriginatorPort uint32
}
func directTcpipHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx *sshContext) {
d := forwardData{}
if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
newChan.Reject(gossh.ConnectionFailed, "error parsing forward data: "+err.Error())
return
}
if srv.LocalPortForwardingCallback == nil || !srv.LocalPortForwardingCallback(ctx, d.DestinationHost, d.DestinationPort) {
newChan.Reject(gossh.Prohibited, "port forwarding is disabled")
return
}
dest := fmt.Sprintf("%s:%d", d.DestinationHost, d.DestinationPort)
var dialer net.Dialer
dconn, err := dialer.DialContext(ctx, "tcp", dest)
if err != nil {
newChan.Reject(gossh.ConnectionFailed, err.Error())
return
}
ch, reqs, err := newChan.Accept()
if err != nil {
dconn.Close()
return
}
go gossh.DiscardRequests(reqs)
go func() {
defer ch.Close()
defer dconn.Close()
io.Copy(ch, dconn)
}()
go func() {
defer ch.Close()
defer dconn.Close()
io.Copy(dconn, ch)
}()
}

View File

@@ -1,89 +0,0 @@
package ssh
import (
"crypto/rand"
"crypto/rsa"
"encoding/binary"
"golang.org/x/crypto/ssh"
)
func generateSigner() (ssh.Signer, error) {
key, err := rsa.GenerateKey(rand.Reader, 768)
if err != nil {
return nil, err
}
return ssh.NewSignerFromKey(key)
}
func parsePtyRequest(s []byte) (pty Pty, ok bool) {
term, s, ok := parseString(s)
if !ok {
return
}
width32, s, ok := parseUint32(s)
if width32 < 1 {
ok = false
}
if !ok {
return
}
height32, _, ok := parseUint32(s)
if height32 < 1 {
ok = false
}
if !ok {
return
}
pty = Pty{
Term: term,
Window: Window{
Width: int(width32),
Height: int(height32),
},
}
return
}
func parseWinchRequest(s []byte) (win Window, ok bool) {
width32, s, ok := parseUint32(s)
if width32 < 1 {
ok = false
}
if !ok {
return
}
height32, _, ok := parseUint32(s)
if height32 < 1 {
ok = false
}
if !ok {
return
}
win = Window{
Width: int(width32),
Height: int(height32),
}
return
}
func parseString(in []byte) (out string, rest []byte, ok bool) {
if len(in) < 4 {
return
}
length := binary.BigEndian.Uint32(in)
if uint32(len(in)) < 4+length {
return
}
out = string(in[4 : 4+length])
rest = in[4+length:]
ok = true
return
}
func parseUint32(in []byte) (uint32, []byte, bool) {
if len(in) < 4 {
return 0, nil, false
}
return binary.BigEndian.Uint32(in), in[4:], true
}

View File

@@ -1,33 +0,0 @@
package ssh
import gossh "golang.org/x/crypto/ssh"
// PublicKey is an abstraction of different types of public keys.
type PublicKey interface {
gossh.PublicKey
}
// The Permissions type holds fine-grained permissions that are specific to a
// user or a specific authentication method for a user. Permissions, except for
// "source-address", must be enforced in the server application layer, after
// successful authentication.
type Permissions struct {
*gossh.Permissions
}
// A Signer can create signatures that verify against a public key.
type Signer interface {
gossh.Signer
}
// ParseAuthorizedKey parses a public key from an authorized_keys file used in
// OpenSSH according to the sshd(8) manual page.
func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []string, rest []byte, err error) {
return gossh.ParseAuthorizedKey(in)
}
// ParsePublicKey parses an SSH public key formatted for use in
// the SSH wire protocol according to RFC 4253, section 6.6.
func ParsePublicKey(in []byte) (out PublicKey, err error) {
return gossh.ParsePublicKey(in)
}

View File

@@ -1,75 +0,0 @@
# This is the official list of Go-MySQL-Driver authors for copyright purposes.
# If you are submitting a patch, please add your name or the name of the
# organization which holds the copyright to this list in alphabetical order.
# Names should be added to this file as
# Name <email address>
# The email address is not required for organizations.
# Please keep the list sorted.
# Individual Persons
Aaron Hopkins <go-sql-driver at die.net>
Achille Roussel <achille.roussel at gmail.com>
Arne Hormann <arnehormann at gmail.com>
Asta Xie <xiemengjun at gmail.com>
Bulat Gaifullin <gaifullinbf at gmail.com>
Carlos Nieto <jose.carlos at menteslibres.net>
Chris Moos <chris at tech9computers.com>
Daniel Nichter <nil at codenode.com>
Daniël van Eeden <git at myname.nl>
Dave Protasowski <dprotaso at gmail.com>
DisposaBoy <disposaboy at dby.me>
Egor Smolyakov <egorsmkv at gmail.com>
Evan Shaw <evan at vendhq.com>
Frederick Mayle <frederickmayle at gmail.com>
Gustavo Kristic <gkristic at gmail.com>
Hanno Braun <mail at hannobraun.com>
Henri Yandell <flamefew at gmail.com>
Hirotaka Yamamoto <ymmt2005 at gmail.com>
ICHINOSE Shogo <shogo82148 at gmail.com>
INADA Naoki <songofacandy at gmail.com>
Jacek Szwec <szwec.jacek at gmail.com>
James Harr <james.harr at gmail.com>
Jeff Hodges <jeff at somethingsimilar.com>
Jeffrey Charles <jeffreycharles at gmail.com>
Jian Zhen <zhenjl at gmail.com>
Joshua Prunier <joshua.prunier at gmail.com>
Julien Lefevre <julien.lefevr at gmail.com>
Julien Schmidt <go-sql-driver at julienschmidt.com>
Justin Nuß <nuss.justin at gmail.com>
Kamil Dziedzic <kamil at klecza.pl>
Kevin Malachowski <kevin at chowski.com>
Lennart Rudolph <lrudolph at hmc.edu>
Leonardo YongUk Kim <dalinaum at gmail.com>
Lion Yang <lion at aosc.xyz>
Luca Looz <luca.looz92 at gmail.com>
Lucas Liu <extrafliu at gmail.com>
Luke Scott <luke at webconnex.com>
Maciej Zimnoch <maciej.zimnoch@codilime.com>
Michael Woolnough <michael.woolnough at gmail.com>
Nicola Peduzzi <thenikso at gmail.com>
Olivier Mengué <dolmen at cpan.org>
oscarzhao <oscarzhaosl at gmail.com>
Paul Bonser <misterpib at gmail.com>
Peter Schultz <peter.schultz at classmarkets.com>
Rebecca Chin <rchin at pivotal.io>
Runrioter Wung <runrioter at gmail.com>
Shuode Li <elemount at qq.com>
Soroush Pour <me at soroushjp.com>
Stan Putrya <root.vagner at gmail.com>
Stanley Gunawan <gunawan.stanley at gmail.com>
Xiangyu Hu <xiangyu.hu at outlook.com>
Xiaobing Jiang <s7v7nislands at gmail.com>
Xiuming Chen <cc at cxm.cc>
Zhenye Xie <xiezhenye at gmail.com>
# Organizations
Barracuda Networks, Inc.
Google Inc.
Keybase Inc.
Pivotal Inc.
Stripe Inc.

Some files were not shown because too many files have changed in this diff Show More