├── CODEOWNERS ├── rpm ├── sharkey-client.sysconfig ├── sharkey-server.sysconfig ├── sharkey-client.service ├── build_rpm.sh ├── sharkey-server.service └── sharkey.spec ├── sharkey.png ├── test ├── test.db ├── keys │ ├── server_ca.pub │ └── server_ca ├── integration │ ├── known_hosts │ ├── client_entry.sh │ ├── id_rsa.pub │ ├── client_config.yaml │ ├── server_config.yaml │ └── id_rsa ├── ssh │ ├── ssh_host_rsa_key.pub │ ├── alice_rsa.pub │ ├── known_hosts │ ├── signed_cert.pub │ └── alice_rsa ├── client_config.yaml ├── server_config.yaml ├── git_server_config.yaml └── tls │ ├── CertAuth.crl │ ├── generate_certs.sh │ ├── badCert.crt │ ├── testCert.crt │ ├── client.crt │ ├── proxy.crt │ ├── server.crt │ ├── client.key │ ├── proxy.key │ ├── server.key │ ├── testCert.key │ ├── badCert.key │ ├── CertAuth.crt │ └── CertAuth.key ├── dancing-sharks.png ├── pkg ├── server │ ├── test.db │ ├── api │ │ ├── testdata │ │ │ ├── test.db │ │ │ ├── server_ca.pub │ │ │ ├── ssh_host_rsa_key.pub │ │ │ ├── ssh_alice_rsa.pub │ │ │ ├── next_server_ca.pub │ │ │ ├── proxy.crt │ │ │ ├── proxy2.crt │ │ │ ├── server_ca │ │ │ └── next_server_ca │ │ ├── authority.go │ │ ├── known_hosts.go │ │ ├── sharkey.go │ │ ├── github.go │ │ ├── enroll.go │ │ ├── github_test.go │ │ └── server_test.go │ ├── config │ │ ├── testdata │ │ │ ├── badSpiffeConfig.yaml │ │ │ ├── goodSpiffeConfig.yaml │ │ │ ├── mixedSpiffeConfig.yaml │ │ │ ├── badConfigType.yaml │ │ │ └── badSpiffeConfigPlus.yaml │ │ ├── config_test.go │ │ └── config.go │ ├── storage │ │ ├── sqlite_test.go │ │ ├── mysql_test.go │ │ ├── storage.go │ │ ├── storage_test.go │ │ ├── sqlite.go │ │ └── mysql.go │ ├── cli │ │ └── cli.go │ ├── telemetry │ │ ├── telemetry.go │ │ └── middleware.go │ └── cert │ │ └── cert.go ├── common │ └── version │ │ └── version.go └── client │ ├── testdata │ └── ssh_host_rsa_key.pub │ ├── cli │ └── cli.go │ ├── client_test.go │ └── sharkey_client.go ├── db ├── sqlite │ ├── dbconf.yml │ └── migrations │ │ ├── 20160714175054_initial.sql │ │ ├── 20200731141840_add_github_table.sql │ │ └── 20190909141059_add_cert_type_column.sql └── mysql │ ├── dbconf.yml │ └── migrations │ ├── 20160714175317_initial.sql │ ├── 20200731141840_add_github_table.sql │ └── 20190909161140_add_cert_type_column.sql ├── .gitignore ├── cmd ├── sharkey-client │ └── sharkey-client.go └── sharkey-server │ └── sharkey-server.go ├── .github ├── dependabot.yml └── workflows │ ├── lint.yaml │ └── tests.yml ├── docker.sh ├── BUG-BOUNTY.md ├── CONTRIBUTING.md ├── Dockerfile ├── examples ├── client.yml └── server.yml ├── DockerfileClientTest ├── go.mod ├── integration-test.sh ├── LICENSE ├── README.md └── go.sum /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @square/idinfra-staff 2 | -------------------------------------------------------------------------------- /rpm/sharkey-client.sysconfig: -------------------------------------------------------------------------------- 1 | SHARKEY_CONFIG=/etc/sharkey/client.yml 2 | -------------------------------------------------------------------------------- /sharkey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/sharkey/HEAD/sharkey.png -------------------------------------------------------------------------------- /test/test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/sharkey/HEAD/test/test.db -------------------------------------------------------------------------------- /dancing-sharks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/sharkey/HEAD/dancing-sharks.png -------------------------------------------------------------------------------- /pkg/server/test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/sharkey/HEAD/pkg/server/test.db -------------------------------------------------------------------------------- /db/sqlite/dbconf.yml: -------------------------------------------------------------------------------- 1 | development: 2 | driver: sqlite3 3 | open: .data/development.db 4 | -------------------------------------------------------------------------------- /pkg/server/api/testdata/test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/square/sharkey/HEAD/pkg/server/api/testdata/test.db -------------------------------------------------------------------------------- /test/keys/server_ca.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG6gucRFY8Fn+Xr+qek28EOCtO22FpNA22HZ4HqXBov6 sharkey_test_ca 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /sharkey-server 2 | /sharkey-client 3 | /cmd/sharkey-server/sharkey-server 4 | /cmd/sharkey-client/sharkey-client 5 | .data 6 | -------------------------------------------------------------------------------- /pkg/server/config/testdata/badSpiffeConfig.yaml: -------------------------------------------------------------------------------- 1 | auth_proxy: 2 | allowed_spiffe_ids: 3 | - spifffe://proxy.com 4 | - spiffee://proxy2.com/ -------------------------------------------------------------------------------- /pkg/server/config/testdata/goodSpiffeConfig.yaml: -------------------------------------------------------------------------------- 1 | auth_proxy: 2 | allowed_spiffe_ids: 3 | - spiffe://proxy.com 4 | - spiffe://proxy2.com -------------------------------------------------------------------------------- /pkg/server/config/testdata/mixedSpiffeConfig.yaml: -------------------------------------------------------------------------------- 1 | auth_proxy: 2 | allowed_spiffe_ids: 3 | - spiffe://proxy.com 4 | - spiffee://proxy2.com/ -------------------------------------------------------------------------------- /test/integration/known_hosts: -------------------------------------------------------------------------------- 1 | @cert-authority * ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG6gucRFY8Fn+Xr+qek28EOCtO22FpNA22HZ4HqXBov6 sharkey_test_ca 2 | -------------------------------------------------------------------------------- /pkg/server/config/testdata/badConfigType.yaml: -------------------------------------------------------------------------------- 1 | auth_proxy: 2 | allowed_spiffe_ids: 3 | something: 4 | - test 5 | - "spiffe://proxy.com" 6 | -------------------------------------------------------------------------------- /rpm/sharkey-server.sysconfig: -------------------------------------------------------------------------------- 1 | SHARKEY_CONFIG=/etc/sharkey/server.yml 2 | SHARKEY_MIGRATIONS=/etc/sharkey/db/sqlite 3 | #SHARKEY_MIGRATIONS=/etc/sharkey/db/mysql 4 | -------------------------------------------------------------------------------- /pkg/common/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | const ( 4 | version = "0.1.0" 5 | ) 6 | 7 | func Version() string { 8 | return version 9 | } 10 | -------------------------------------------------------------------------------- /pkg/server/config/testdata/badSpiffeConfigPlus.yaml: -------------------------------------------------------------------------------- 1 | auth_proxy: 2 | hostname: "proxy.com" 3 | allowed_spiffe_ids: 4 | - spifffe://proxy.com 5 | - spiffee://proxy2.com/ -------------------------------------------------------------------------------- /cmd/sharkey-client/sharkey-client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/square/sharkey/pkg/client/cli" 8 | ) 9 | 10 | func main() { 11 | var logger = logrus.New() 12 | cli.Run(os.Args[1:], logger) 13 | } 14 | -------------------------------------------------------------------------------- /cmd/sharkey-server/sharkey-server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/square/sharkey/pkg/server/cli" 8 | ) 9 | 10 | func main() { 11 | var logger = logrus.New() 12 | cli.Run(os.Args[1:], logger) 13 | } 14 | -------------------------------------------------------------------------------- /test/integration/client_entry.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## Docker container entry point 4 | 5 | if [ -z "$SHARKEY_CONFIG" ]; then 6 | echo "No configuration file specified, aborting." 7 | exit 1 8 | fi 9 | 10 | /usr/sbin/sshd 11 | /usr/bin/sharkey-client --config="$SHARKEY_CONFIG" 12 | -------------------------------------------------------------------------------- /rpm/sharkey-client.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Sharkey Client 3 | 4 | [Service] 5 | EnvironmentFile=/etc/sysconfig/sharkey-client 6 | ExecStart=/usr/sbin/sharkey-client --config=${SHARKEY_CONFIG} 7 | Restart=on-failure 8 | User=sharkey-client 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /rpm/build_rpm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | DIR=$(dirname $(readlink -f $0)) 8 | 9 | rev=$(git rev-parse HEAD) 10 | 11 | tar -C "${DIR}/../.." -zcf ~/rpmbuild/SOURCES/sharkey-${rev}.tar.gz --transform s/sharkey/sharkey-${rev}/ sharkey 12 | rpmbuild -ba ${DIR}/sharkey.spec 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: github.com/sirupsen/logrus 10 | versions: 11 | - 1.7.1 12 | - 1.8.0 13 | - dependency-name: github.com/armon/go-metrics 14 | versions: 15 | - 0.3.6 16 | -------------------------------------------------------------------------------- /db/mysql/dbconf.yml: -------------------------------------------------------------------------------- 1 | # Development database set-up only, NOT PRODUCTION: 2 | # 3 | # $ mysql -u root 4 | # mysql> CREATE DATABASE sharkey_development; 5 | # mysql> CREATE USER 'sharkey'@'localhost'; 6 | # mysql> GRANT ALL PRIVILEGES ON sharkey_development.* TO 'sharkey'@'localhost' 7 | # mysql> FLUSH PRIVILEGES; 8 | development: 9 | driver: mysql 10 | open: sharkey:@/sharkey_development 11 | -------------------------------------------------------------------------------- /db/sqlite/migrations/20160714175054_initial.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- SQL in section 'Up' is executed when this migration is applied 3 | CREATE TABLE hostkeys( 4 | id INTEGER PRIMARY KEY AUTOINCREMENT, 5 | hostname VARCHAR(255) NOT NULL UNIQUE, 6 | pubkey BLOB NOT NULL 7 | ); 8 | 9 | -- +goose Down 10 | -- SQL section 'Down' is executed when this migration is rolled back 11 | DROP TABLE hostkeys; 12 | -------------------------------------------------------------------------------- /test/ssh/ssh_host_rsa_key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA10P57aCLgj5kLDNY2VKnNaVaJ+yk46NBEM8nEgdd4YCQttttMGDAygMfAFdnsB5+OohlHqBMh4h/PoGIFQTTXyi9oaMUWHa1z1/dTxUeyjdUEEzbvzYwzNc0V3Ral0QWfvExN0xjcQ31HcFibnGZSlc1tQyyH6rP6L+iAsj9XEWb1h9qQhTR/TN77CELw08LH06B3+gWhBnvF35xLupAK/WqEKoYW8lpUNU3oO7TPloLuZvuwpc53LNV/miclXgoS30T888h5JsJRdN8fR0SLmkRx/cqzvM+BeRTaWNprwnSOG8fELelHtIzyNeiSCZylQYtRe6PvOOUbyyTLV29Vw== 2 | -------------------------------------------------------------------------------- /pkg/server/api/testdata/server_ca.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCjvu3htgpnsNwH+VrY2Y6Zc8k2FLMPRdWioNycEv+fOZPPa3Kf/4DNXIUcIFHN/1g65Akzlkx8E7900DkDlC5XPsQSmRTQUnr3UnHCFZFhvLhJvMILkU8A79PjLfQOJ9DByCDvWZXeQ587ZVTnkzPi/xGPcYp4mRdeJQh3H/6eMUYNitnCU/qdw6NmeCgW4uYBmek2rtiagU6TOyPhpjUEA9rVutZF8V8Z4LkBoknP7RXnVontFtd2fvTd7SJ1UMz7MJNTLymsl20c1D/tfxrRz+nOthUoQvUoofGD8x5zLgyAiWBgY3P/mulbXglfnbkEO+GrtMy6yugcoSDb2uCj 2 | -------------------------------------------------------------------------------- /docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## Docker container entry point 4 | 5 | if [ -z "$SHARKEY_CONFIG" ]; then 6 | echo "No configuration file specified, aborting." 7 | exit 1 8 | fi 9 | 10 | if ! [ -z "$SHARKEY_MIGRATIONS" ]; then 11 | /usr/bin/sharkey-server migrate --config="$SHARKEY_CONFIG" --migrations="$SHARKEY_MIGRATIONS" 12 | fi 13 | 14 | exec /usr/bin/sharkey-server --config="$SHARKEY_CONFIG" "$@" 15 | -------------------------------------------------------------------------------- /pkg/client/testdata/ssh_host_rsa_key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA10P57aCLgj5kLDNY2VKnNaVaJ+yk46NBEM8nEgdd4YCQttttMGDAygMfAFdnsB5+OohlHqBMh4h/PoGIFQTTXyi9oaMUWHa1z1/dTxUeyjdUEEzbvzYwzNc0V3Ral0QWfvExN0xjcQ31HcFibnGZSlc1tQyyH6rP6L+iAsj9XEWb1h9qQhTR/TN77CELw08LH06B3+gWhBnvF35xLupAK/WqEKoYW8lpUNU3oO7TPloLuZvuwpc53LNV/miclXgoS30T888h5JsJRdN8fR0SLmkRx/cqzvM+BeRTaWNprwnSOG8fELelHtIzyNeiSCZylQYtRe6PvOOUbyyTLV29Vw== 2 | -------------------------------------------------------------------------------- /rpm/sharkey-server.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Sharkey Server 3 | 4 | [Service] 5 | EnvironmentFile=/etc/sysconfig/sharkey-server 6 | ExecStartPre=/usr/sbin/sharkey-server --config=${SHARKEY_CONFIG} migrate --migrations=${SHARKEY_MIGRATIONS} 7 | ExecStart=/usr/sbin/sharkey-server --config=${SHARKEY_CONFIG} start 8 | Restart=on-failure 9 | User=sharkey 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /test/ssh/alice_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD4LhZ8tlWZ5y+AQHHYs5pBHhAwojcNd1mvJNQmjLL+qSmt7mRjr46EP2m3PD8XgBWoGkGuI+8abIBIXwbzSIGfbuR+hWeyphH1rn6P0r6/3UChXf1h3q7IsC/T9fPMx/Zl4AkLIazuvrJ55P67Nu8/HiIDI5KvoiP22Uh3sCFnrShc3vlWi4q8yhpQM6DCSC+ff9jnpNeT9lBgFJbM3wqJANLBMUMuGV4fobdfvSXgqdHgoyKqMN3K0As2ETXbFiA6FcY16NIk9BxI633jZwkkWFaFlVZKNdk8LvLw5ovGnTNszwz0UCusfkNQihjFdSJzWWZ5LGXWQ99Mt3I58Ll5 alice@alice.local 2 | -------------------------------------------------------------------------------- /BUG-BOUNTY.md: -------------------------------------------------------------------------------- 1 | Serious about security 2 | ====================== 3 | 4 | Square recognizes the important contributions the security research community 5 | can make. We therefore encourage reporting security issues with the code 6 | contained in this repository. 7 | 8 | If you believe you have discovered a security vulnerability, please follow the 9 | guidelines at . 10 | 11 | -------------------------------------------------------------------------------- /pkg/server/api/testdata/ssh_host_rsa_key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA10P57aCLgj5kLDNY2VKnNaVaJ+yk46NBEM8nEgdd4YCQttttMGDAygMfAFdnsB5+OohlHqBMh4h/PoGIFQTTXyi9oaMUWHa1z1/dTxUeyjdUEEzbvzYwzNc0V3Ral0QWfvExN0xjcQ31HcFibnGZSlc1tQyyH6rP6L+iAsj9XEWb1h9qQhTR/TN77CELw08LH06B3+gWhBnvF35xLupAK/WqEKoYW8lpUNU3oO7TPloLuZvuwpc53LNV/miclXgoS30T888h5JsJRdN8fR0SLmkRx/cqzvM+BeRTaWNprwnSOG8fELelHtIzyNeiSCZylQYtRe6PvOOUbyyTLV29Vw== 2 | -------------------------------------------------------------------------------- /test/integration/id_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDqQciTWTWVgaHyheyCPQAu5EbNse+Ii4/DRAxLyp5/Pjy4upC05zIbwfqQjaI++SIkc8S7lIvYJ4ApvT0iPlT7X2RAcqcNsaJNxX58LyvE+RRkdepd1NKr6dVfd+ZxqhqE4reaZ+qvtvV5idauqeqx1IzilahMo0mOC2RGqLRKwC6LKgDAlBYaQgVFu/3BIXEukBKIdxGfFsJy1HGTKsDR+Hpz7KuPwD91+dX0r9w2bfFg+9pgLuMA21WSJ3VkiL9GBO2rSQergVcUmnfc+WQD72m8E03v1uMRODdtwNzrCr1QQXxuxs/uS6QWW0A1ep+z2CSmIJxMORVHLhPnNLGF cdenny@cdenny.local 2 | -------------------------------------------------------------------------------- /test/ssh/known_hosts: -------------------------------------------------------------------------------- 1 | server ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCZayASblguMM4nHIvQabB7S/eglET8bylujuK8xHdQWmcGDftz5hl/UvmYI4JkrWCLkYLrFBguHShGCoRDZPqSrfAYD5/GVij2TQvl9Mpa2SEvufby/R7lvi1bETvNlr53+0qwyRvGDFKXskaM+GbDTadqbA5+SH74dvFSH8Brosa3aut92jNiGyluqVtboKDQDgSHsi42c4F4gjlAX4WdVGiKSI+s+jhhO+RXsP1S+qYEApyHkLsf8CtimPhw6d0ItF6DT/Grr+2uMQV/BEvuQbIOfg+BFXhtzpgBXAVdeVFZ5JZaVeLw2iw+HgZ/2aAja7oClxu92JAq+dIpWXBX root@2da0903ff372 2 | 3 | -------------------------------------------------------------------------------- /db/sqlite/migrations/20200731141840_add_github_table.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- SQL in this section is executed when the migration is applied. 3 | CREATE TABLE github_user_mappings( 4 | sso_identity VARCHAR(255) PRIMARY KEY NOT NULL UNIQUE, 5 | github_username VARCHAR(255) NOT NULL UNIQUE 6 | ); 7 | 8 | -- +goose Down 9 | -- SQL in this section is executed when the migration is rolled back. 10 | DROP TABLE github_user_mappings; -------------------------------------------------------------------------------- /pkg/server/api/testdata/ssh_alice_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD4LhZ8tlWZ5y+AQHHYs5pBHhAwojcNd1mvJNQmjLL+qSmt7mRjr46EP2m3PD8XgBWoGkGuI+8abIBIXwbzSIGfbuR+hWeyphH1rn6P0r6/3UChXf1h3q7IsC/T9fPMx/Zl4AkLIazuvrJ55P67Nu8/HiIDI5KvoiP22Uh3sCFnrShc3vlWi4q8yhpQM6DCSC+ff9jnpNeT9lBgFJbM3wqJANLBMUMuGV4fobdfvSXgqdHgoyKqMN3K0As2ETXbFiA6FcY16NIk9BxI633jZwkkWFaFlVZKNdk8LvLw5ovGnTNszwz0UCusfkNQihjFdSJzWWZ5LGXWQ99Mt3I58Ll5 alice@alice.local 2 | -------------------------------------------------------------------------------- /test/client_config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | tls: 3 | ca: test/tls/CertAuth.crt 4 | cert: test/tls/testCert.crt 5 | key: test/tls/testCert.key 6 | request_addr: "https://127.0.0.1:8080" 7 | host_keys: 8 | - plain: "/etc/ssh/ssh_host_rsa_key.pub" 9 | signed: "/etc/ssh/ssh_host_rsa_key-cert.pub" 10 | known_hosts: test/ssh/known_hosts 11 | sleep: "2s" 12 | sudo: "/usr/bin/sudo" 13 | ssh_reload: ["/usr/sbin/service", "ssh", "restart"] 14 | -------------------------------------------------------------------------------- /test/keys/server_ca: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACBuoLnERWPBZ/l6/qnpNvBDgrTtthaTQNth2eB6lwaL+gAAAJi08x3ptPMd 4 | 6QAAAAtzc2gtZWQyNTUxOQAAACBuoLnERWPBZ/l6/qnpNvBDgrTtthaTQNth2eB6lwaL+g 5 | AAAECjrDlhoUNpKP2jAn1Rvqh+za7rMFPcbeOwHFlotsvUZ26gucRFY8Fn+Xr+qek28EOC 6 | tO22FpNA22HZ4HqXBov6AAAAEW1tY0BncmF2bGF4LmxvY2FsAQIDBA== 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /db/mysql/migrations/20160714175317_initial.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- SQL in section 'Up' is executed when this migration is applied 3 | CREATE TABLE hostkeys( 4 | id INTEGER PRIMARY KEY AUTO_INCREMENT, 5 | hostname VARCHAR(255) NOT NULL UNIQUE, 6 | pubkey BLOB NOT NULL 7 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 8 | 9 | 10 | -- +goose Down 11 | -- SQL section 'Down' is executed when this migration is rolled back 12 | DROP TABLE hostkeys; 13 | -------------------------------------------------------------------------------- /db/mysql/migrations/20200731141840_add_github_table.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- SQL in this section is executed when the migration is applied. 3 | CREATE TABLE github_user_mappings( 4 | sso_identity VARCHAR(255) PRIMARY KEY NOT NULL UNIQUE, 5 | github_username VARCHAR(255) NOT NULL UNIQUE 6 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 7 | 8 | -- +goose Down 9 | -- SQL in this section is executed when the migration is rolled back. 10 | DROP TABLE github_user_mappings; -------------------------------------------------------------------------------- /db/mysql/migrations/20190909161140_add_cert_type_column.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- SQL in section 'Up' is executed when this migration is applied 3 | ALTER TABLE hostkeys 4 | ADD COLUMN cert_type ENUM('host_cert', 'user_cert') NOT NULL; 5 | UPDATE hostkeys SET cert_type = 'host_cert'; 6 | CREATE INDEX idx_hostkeys_cert_type ON hostkeys(cert_type); 7 | 8 | -- +goose Down 9 | -- SQL section 'Down' is executed when this migration is rolled back 10 | DROP INDEX idx_hostkeys_cert_type ON hostkeys; 11 | ALTER TABLE hostkeys DROP COLUMN cert_type; 12 | -------------------------------------------------------------------------------- /pkg/server/api/testdata/next_server_ca.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCqKlHg+wnShhJkNLLiP7F7BNxi0mRIZutuw6PEa0Ec55MynEIhWzDkyplH06WdfhJkx+moBr3unvqdm+0V0iWOhzkKf44U4Zi1KvbFLG9hAbeYbIg40oEbNj8j06c+Q05avVKiRuZavYcf70DCnEJSeA4ki6YrL1xdEARA6YwMHxIyJGSHuA4kLBsjtLPVl1x1BzFbFJtL5RhZuRUFhNLCUBYcV7MhbsECWYcER5L1Tnxx4OLRHACsdFUvNy+jFXHvWccH2FcSw7M0E6nZwVLTb/0jwypUag//BhZJFHT7KR0rbPDw93hLEHRBqf4TjC4HwjB/v1d5bNpyAkfKGMI5eGIhQTnO7JAHpTXZ/3Beib06fP3JWDRfSj4KGEpFjAxc+eIHSl8R4dsxp0Ym8Z2U9buSYzf6dVP8eEOV4Cp5hVTRzTk5EdO2pSO+DOxgmWXDX9Gu4GRwQ24FNCI2k3jyspT8S4LRSgCsC00paLIsqUiwqxWcfqBRtuI3IVKm26U= -------------------------------------------------------------------------------- /db/sqlite/migrations/20190909141059_add_cert_type_column.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- SQL in section 'Up' is executed when this migration is applied 3 | -- cert_type = 2 means that host cert is the default 4 | -- Aee storage/sqlite.go for more details 5 | ALTER TABLE hostkeys 6 | ADD COLUMN cert_type INTEGER NOT NULL DEFAULT 2; 7 | UPDATE hostkeys SET cert_type = 2; 8 | CREATE INDEX idx_hostkeys_cert_type ON hostkeys(cert_type); 9 | 10 | -- +goose Down 11 | -- SQL section 'Down' is executed when this migration is rolled back 12 | -- It's not possible to drop a column in SQLite 13 | DROP INDEX idx_hostkeys_cert_type; 14 | -------------------------------------------------------------------------------- /pkg/server/storage/sqlite_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/square/sharkey/pkg/server/config" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestSqlite(t *testing.T) { 12 | dbfile, err := os.CreateTemp("", "sharkey-test-db") 13 | require.NoError(t, err) 14 | defer os.Remove(dbfile.Name()) 15 | 16 | cfg := config.Database{Address: dbfile.Name()} 17 | storage, err := NewSqlite(cfg) 18 | require.NoError(t, err) 19 | require.NoError(t, storage.Migrate("../../../db/sqlite/migrations")) 20 | 21 | testStorage(t, storage) 22 | testGitHubStorage(t, storage) 23 | } 24 | -------------------------------------------------------------------------------- /test/integration/client_config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | tls: 3 | ca: /build/test/tls/CertAuth.crt 4 | cert: /build/test/tls/client.crt 5 | key: /build/test/tls/client.key 6 | request_addr: "https://server:8080" 7 | host_keys: 8 | - plain: "/etc/ssh/ssh_host_rsa_key.pub" 9 | signed: "/etc/ssh/ssh_host_rsa_key-cert.pub" 10 | - plain: "/etc/ssh/ssh_host_ed25519_key.pub" 11 | signed: "/etc/ssh/ssh_host_ed25519_key-cert.pub" 12 | - plain: "/etc/ssh/ssh_host_ecdsa_key.pub" 13 | signed: "/etc/ssh/ssh_host_ecdsa_key-cert.pub" 14 | known_hosts: /etc/ssh/known_hosts 15 | sleep: "10s" 16 | ssh_reload: ["/usr/sbin/service", "ssh", "restart"] 17 | -------------------------------------------------------------------------------- /test/server_config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | db: 3 | address: test/test.db 4 | type: sqlite 5 | tls: 6 | ca: test/tls/CertAuth.crt 7 | cert: test/tls/testCert.crt 8 | key: test/tls/testCert.key 9 | signing_key: test/keys/server_ca 10 | host_cert_duration: 168h 11 | user_cert_duration: 24h 12 | listen_addr: "127.0.0.1:8080" 13 | auth_proxy: 14 | hostname: proxy.example.com 15 | username_header: X-Forwarded-User 16 | ssh: 17 | user_cert_extensions: 18 | - "permit-X11-forwarding" 19 | - "permit-agent-forwarding" 20 | - "permit-port-forwarding" 21 | - "permit-pty" 22 | - "permit-user-rc" 23 | telemetry: 24 | address: "127.0.0.1:8200" 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | If you would like to contribute code to Sharkey you can do so through GitHub by 5 | forking the repository and sending a pull request. 6 | 7 | When submitting code, please make every effort to follow existing conventions 8 | and style in order to keep the code as readable as possible. Please also make 9 | sure your code compiles and passes tests. 10 | 11 | Before your code can be accepted into the project you must also sign the 12 | [Individual Contributor License Agreement (CLA)][1]. 13 | 14 | 15 | [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 16 | -------------------------------------------------------------------------------- /test/integration/server_config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | db: 3 | address: /tmp/test.db 4 | type: sqlite 5 | tls: 6 | ca: /build/test/tls/CertAuth.crt 7 | cert: /build/test/tls/server.crt 8 | key: /build/test/tls/server.key 9 | signing_key: /build/test/keys/server_ca 10 | host_cert_duration: 168h 11 | user_cert_duration: 24h 12 | listen_addr: "0.0.0.0:8080" 13 | aliases: 14 | "client": 15 | - "localhost" 16 | auth_proxy: 17 | hostname: proxy 18 | username_header: X-Forwarded-User 19 | ssh: 20 | user_cert_extensions: 21 | - "permit-X11-forwarding" 22 | - "permit-agent-forwarding" 23 | - "permit-port-forwarding" 24 | - "permit-pty" 25 | - "permit-user-rc" -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | golangci: 9 | name: Lint 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 https://github.com/actions/checkout/releases/tag/v4 13 | 14 | - name: Set up Go 15 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 https://github.com/actions/setup-go/releases/tag/v5 16 | with: 17 | go-version: '1.23' 18 | 19 | - name: Download Go modules 20 | run: go mod download 21 | 22 | - name: golangci-lint 23 | uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3 https://github.com/golangci/golangci-lint-action/releases/tag/v3 24 | with: 25 | version: v1.64.8 26 | -------------------------------------------------------------------------------- /test/git_server_config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | db: 3 | address: test/test.db 4 | type: sqlite 5 | tls: 6 | ca: test/tls/CertAuth.crt 7 | cert: test/tls/testCert.crt 8 | key: test/tls/testCert.key 9 | signing_key: test/keys/server_ca 10 | host_cert_duration: 168h 11 | user_cert_duration: 24h 12 | listen_addr: "127.0.0.1:8080" 13 | auth_proxy: 14 | hostname: proxy.example.com 15 | username_header: X-Forwarded-User 16 | ssh: 17 | user_cert_extensions: 18 | - "permit-X11-forwarding" 19 | - "permit-agent-forwarding" 20 | - "permit-port-forwarding" 21 | - "permit-pty" 22 | - "permit-user-rc" 23 | github: 24 | include_user_identity: true 25 | # fill with github app info 26 | app_id: 1 27 | installation_id: 1 28 | # github app private key file location 29 | private_key_path: "" 30 | organization_name: "" 31 | sync_interval: "5m" 32 | sync_enabled: false -------------------------------------------------------------------------------- /pkg/client/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/square/sharkey/pkg/client" 8 | "github.com/square/sharkey/pkg/common/version" 9 | "gopkg.in/alecthomas/kingpin.v2" 10 | "gopkg.in/yaml.v2" 11 | ) 12 | 13 | func Run(args []string, logger *logrus.Logger) { 14 | logger.Println("Starting client") 15 | 16 | app := kingpin.New("sharkey-client", "Certificate client of the ssh-ca system.") 17 | configPath := app.Flag("config", "Path to config file for client.").Required().String() 18 | app.Version(version.Version()) 19 | 20 | kingpin.MustParse(app.Parse(args)) 21 | 22 | data, err := os.ReadFile(*configPath) 23 | if err != nil { 24 | logger.WithError(err).Fatalln("Error reading config file") 25 | } 26 | 27 | var conf client.Config 28 | if err := yaml.Unmarshal(data, &conf); err != nil { 29 | logger.WithError(err).Fatalln("Error parsing config file") 30 | } 31 | 32 | client.Run(&conf, logger) 33 | } 34 | -------------------------------------------------------------------------------- /test/tls/CertAuth.crl: -------------------------------------------------------------------------------- 1 | -----BEGIN X509 CRL----- 2 | MIICgjBsAgEBMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNVBAMTCENlcnRBdXRoFw0x 3 | OTA0MTYxOTM1MzBaFw0yOTA0MTYxOTM1MjVaMACgIzAhMB8GA1UdIwQYMBaAFOj3 4 | bwMyDx4Oaa/DVxxSV9UPYiKYMA0GCSqGSIb3DQEBCwUAA4ICAQCSriZpGe0Dm9c9 5 | OUwRnMnA8RlMxIJWCj6QSooaWQuFe3Tn6Qta8L4FbffLKJsRmElUhavRRjkUhVYK 6 | 1aT+RnIm7/DG/ouXRl6WPx872SF43cL+7r+YgTiym4FgtVoRMrDcTN7j06jAioiz 7 | Sa4zJxg2B8DM2yH8oOQpj26LDmsKpgv+kxf/gcxhiWw4OJaTIpfVImLMTIoFBmSY 8 | +zF0tZNo1ZtGIwHhRSPr/uYC+QHG4b3hdMOXfkayE/PhjfPRrH9Sqn2eWBKX/z3i 9 | 0u2hS9MHj4esFoJSrz0T+cto2Hlx4m8lZF6SZoxVB5xsu9FTmL2jhIMaj3BqcUBI 10 | r8sAd0XhXnSTu1T6bPsE0kFo1efFaKUGxeir2QLcnK+3PowH2IBrSdgs2xx7FJXN 11 | 8ALrKVpFrXgcOmyLtO/Gx55CMr0Q6UZO9U5rYkVLfEAKez/aVqRaXvaPlpTDDysE 12 | eT3c2nFI9ONiFz4BYcXA7V9b5xoVDR2g2TZYRtAAeCgFDioBj9o8Ih1RK750q8xV 13 | JVh6sIpFs4IaYm8I3XJXN83TXl12MxsdDajneWHGiMGJGVCbkcPmqmHvdUHN+Z76 14 | bVuwb2x/vG4ZXDb0dgIFxlMnD33k9RNv1i4koXnHXV4fN/2hCn8Jcyn5CzAi0LtC 15 | lSOfrGI+6mTAb4CXjTNPJT9mWkHZ3Q== 16 | -----END X509 CRL----- 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile for square/sharkey (server). 2 | # 3 | # Building: 4 | # docker build --rm -t square/sharkey-server . 5 | # 6 | # Basic usage: 7 | # docker run -e SHARKEY_CONFIG=/path/to/config -e SHARKEY_MIGRATIONS=/path/to/migration/dir square/sharkey-server 8 | # 9 | # This image only contains the server component of sharkey, 10 | # the client will have to be deployed separately 11 | 12 | FROM golang:1.23 as build 13 | 14 | WORKDIR /app 15 | 16 | COPY go.mod . 17 | COPY go.sum . 18 | 19 | # Download dependencies 20 | RUN go mod download 21 | 22 | # Copy source 23 | COPY . . 24 | 25 | # Build & set-up 26 | RUN cp docker.sh /usr/bin/entrypoint.sh && \ 27 | chmod +x /usr/bin/entrypoint.sh && \ 28 | go build -buildvcs=false -o /usr/bin/sharkey-server github.com/square/sharkey/cmd/sharkey-server 29 | 30 | 31 | # Create a multi-stage build with the binary 32 | FROM golang:1.20 33 | 34 | COPY --from=build /usr/bin/sharkey-server /usr/bin/sharkey-server 35 | COPY --from=build /usr/bin/entrypoint.sh /usr/bin/entrypoint.sh 36 | 37 | ENTRYPOINT ["/usr/bin/entrypoint.sh"] 38 | -------------------------------------------------------------------------------- /test/tls/generate_certs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | BASEDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 6 | DIR=$(mktemp -d) 7 | 8 | function cleanup() { 9 | echo "Cleanup..." 10 | rm -rf "$DIR" 11 | } 12 | 13 | trap cleanup EXIT 14 | 15 | # Generate new root cert 16 | certstrap --depot-path "$DIR" init --common-name "CertAuth" --expires "10 years" 17 | 18 | # Generate server cert 19 | certstrap --depot-path "$DIR" request-cert --ip 127.0.0.1 --domain server 20 | certstrap --depot-path "$DIR" sign --CA CertAuth --expires="10 years" server 21 | 22 | # Generate client cert 23 | certstrap --depot-path "$DIR" request-cert --ip 127.0.0.1 --domain client 24 | certstrap --depot-path "$DIR" sign --CA CertAuth --expires="10 years" client 25 | 26 | # Generate proxy cert 27 | certstrap --depot-path "$DIR" request-cert --ip 127.0.0.1 --domain proxy 28 | certstrap --depot-path "$DIR" sign --CA CertAuth --expires="10 years" proxy 29 | 30 | for i in proxy.key proxy crt client.key client.crt server.key server.crt CertAuth.key CertAuth.crt; do 31 | mv "$DIR/$i" "$BASEDIR/" 32 | done 33 | -------------------------------------------------------------------------------- /examples/client.yml: -------------------------------------------------------------------------------- 1 | # Server address 2 | request_addr: "https://sharkey-server.example:8080" 3 | 4 | # TLS config for making requests 5 | # --- 6 | tls: 7 | ca: /path/to/ca-bundle.pem 8 | cert: /path/to/client-certificate.pem 9 | key: /path/to/client-certificate-key.pem 10 | 11 | # List of host keys for OpenSSH server 12 | host_keys: 13 | # Here, 'key' is the public key, and 'cert' is where to install the signed cert 14 | - plain: "/etc/ssh/ssh_host_rsa_key.pub" 15 | signed: "/etc/ssh/ssh_host_rsa_key-cert.pub" 16 | # You can specify multiple host keys (e.g. if you have both RSA, ED25519 keys) 17 | - plain: "/etc/ssh/ssh_host_ed25519_key.pub" 18 | signed: "/etc/ssh/ssh_host_ed25519_key-cert.pub" 19 | 20 | # Where to install the known_hosts file 21 | known_hosts: /etc/ssh/known_hosts 22 | 23 | # If set to true, only install authorities in known_hosts file (ignore other machine's host keys). 24 | known_hosts_authorities_only: false 25 | 26 | # How often to refresh/request new certificate 27 | sleep: "24h" 28 | 29 | # Path to sudo binary 30 | sudo: "/usr/bin/sudo" 31 | 32 | # Command to restart ssh 33 | # If sudo is set as well, this command will be prefixed with 'sudo' 34 | ssh_reload: ["/usr/sbin/service", "ssh", "restart"] 35 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 https://github.com/actions/checkout/releases/tag/v4 15 | - name: Set up MySQL 16 | uses: mirromutth/mysql-action@de1fba8b3f90ce8db80f663a7043be3cf3231248 # v1.1 https://github.com/mirromutth/mysql-action/releases/tag/v1.1 17 | with: 18 | mysql database: 'sharkey_test' 19 | mysql root password: 'root' 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 https://github.com/actions/setup-go/releases/tag/v5 23 | with: 24 | go-version: '1.23.x' 25 | 26 | - name: Go Build 27 | run: go build -v ./... 28 | 29 | - name: Go Test 30 | run: go test -v ./... 31 | 32 | - name: Build Server Container 33 | run: docker build -t server:latest -f Dockerfile . 34 | 35 | - name: Build Client Container 36 | run: docker build -t client:latest -f DockerfileClientTest . 37 | 38 | - name: Integration Test 39 | run: ./integration-test.sh 40 | -------------------------------------------------------------------------------- /pkg/server/api/testdata/proxy.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDQTCCAimgAwIBAgIRAL/faIWkOsEXsWsStLcl7oAwDQYJKoZIhvcNAQELBQAw 3 | FDESMBAGA1UEAwwJc2VydmVyX2NhMB4XDTIzMDIyODIyMzgxMVoXDTI0MDgyODIy 4 | NDcxN1owEDEOMAwGA1UEAxMFcHJveHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw 5 | ggEKAoIBAQDFxNzFNoaEK/Fa0pSr57DBZ6CfLRK7phhyx7H7lXcq0kwcNZJo3Qq4 6 | rYiLS1axORMa3m37VEh6sW4zE5Sf9rlXHGmJq2dBWcmHlQ6Kynzcd3fJbwZJP2jl 7 | yJeqPvhm4MJph8rDsZooArs28qCjw7g3aeXr95NN7q0aVV4ErE8z5rhQUvWlCjWU 8 | UUcnaZVZWzNjZsuBCnrsI61K/iJ24izxsMEg2eOSFOk731qhxSjD1bx2fvt0RjxT 9 | Nb9M+5h6Czm9Qs2aLgaI58DBHiUEJh68zwp57cXnmZnd5MnHBtfl+L830M1evp1E 10 | czd+VIuXx1PzSgJN4WYVh/lxRiZ0nDNJAgMBAAGjgZEwgY4wDgYDVR0PAQH/BAQD 11 | AgO4MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQU2sg7 12 | 7l5HQjGU3fAPjltswYlirVIwHwYDVR0jBBgwFoAUqPd5BkQt34TIuOSgKRBBt1T5 13 | cSkwHQYDVR0RBBYwFIYSc3BpZmZlOi8vcHJveHkuY29tMA0GCSqGSIb3DQEBCwUA 14 | A4IBAQB285NoFrZZBPCkOZO0MvGWeU9Za1REs3hAsjiY/TlvZT/1YrJenrFANy+H 15 | Xqk+NQR8xfUI3VCqUv8vwo0ySn+dAtKCuSyBl/WL6JqrSAF8gx/elquttPi8n+kF 16 | x3q10E2fXEH0hAS2Yv28jq5l0agHGnevm7hf4hGt3qXnLGCRZno1FScvdpMFDsH/ 17 | 13mw+UKtWsKV8HAfs8s8S/rqKOSmoSy115kTB3cFG8BCliLF6MWNSuxE4ovI+EXb 18 | QmJ+skUPWnsTg8PnzjsIdeqV6ANH2pfWFN+zmei4FBHZCc1lkXdPF12DBWguXSvc 19 | FDOfkoTvvlyOE8qFzaEC9bnozBCr 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /pkg/server/api/testdata/proxy2.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDQzCCAiugAwIBAgIRAIct1XDddF/R7ssukNdZHV4wDQYJKoZIhvcNAQELBQAw 3 | FDESMBAGA1UEAwwJc2VydmVyX2NhMB4XDTIzMDMwMzE4NTkyNFoXDTI0MDgyODIy 4 | NDcxNlowETEPMA0GA1UEAxMGcHJveHkyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 5 | MIIBCgKCAQEAx25kDMqI28fIpMBl0OKt6Xhj0hAeSPkhk2xdPSaDjdJt6I+U4hPQ 6 | dJjoCCATXxgFz4OXnfVCzuXf0BTYLtWrYkUmVk1kYjjDuOsnEKk6whVaMxIzdvn2 7 | pOjBO1qr31ldyOGGEnBDvz+qYahKqdILTHnZ89LQDC1jjUX0ApYqQ6/z397ouC26 8 | SU8r0nV+S9+ZEeABvAxT66IHBT2AKn+cRTlLOAb1anVMtKVqMxtI69UUKG24ciiC 9 | exFZW0Ty1ElzMoA0uVbFQSZND3Bv4z7UgUGXLGD8l8w9Ygu8Vg7GsOctCtVYDacn 10 | ksgoxKWTteJSo52R9tGVhFEVHSGfFbpSpQIDAQABo4GSMIGPMA4GA1UdDwEB/wQE 11 | AwIDuDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFEFx 12 | ZfgCWRy2ZEQL78xqvDikYhTEMB8GA1UdIwQYMBaAFKj3eQZELd+EyLjkoCkQQbdU 13 | +XEpMB4GA1UdEQQXMBWGE3NwaWZmZTovL3Byb3h5Mi5jb20wDQYJKoZIhvcNAQEL 14 | BQADggEBAKJ2Rk/aHRWCHuagRoywhTRGmGqXReeUQs01Gf8hl6BUC+fPmGUMgE40 15 | 4GhnOOh7xtNnQw2g++Jk7quftKVNv0yD1Vb+DEpqyNoSBxuHf/J1NVJi1IJkFs0M 16 | hPuM3Rmf8mCHhEyUi9Bgc+H4xQ80vc5wzi2nRn3TszlEZVT6uaWhG/xG9pXcTA6u 17 | f1vfLwWETpytxiGVMqKMaU+35VT4RvW7lKcR2nJKIhyZEsbyHEKPO9GskMrGp6df 18 | OBlf11cnpVgdhicmjStDr/d+Ao7h82S3A3CwvH0xK72gvSNAOfLTjde/wCdIJ3jg 19 | SLVSN14IDrgWjVMA3zSaX24Mk9WOLgU= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /test/ssh/signed_cert.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgIgfxnl6UL4dzZbfOh3Y7O9YyAN8+ZFmUI4owdwjxsL0AAAABIwAAAQEA10P57aCLgj5kLDNY2VKnNaVaJ+yk46NBEM8nEgdd4YCQttttMGDAygMfAFdnsB5+OohlHqBMh4h/PoGIFQTTXyi9oaMUWHa1z1/dTxUeyjdUEEzbvzYwzNc0V3Ral0QWfvExN0xjcQ31HcFibnGZSlc1tQyyH6rP6L+iAsj9XEWb1h9qQhTR/TN77CELw08LH06B3+gWhBnvF35xLupAK/WqEKoYW8lpUNU3oO7TPloLuZvuwpc53LNV/miclXgoS30T888h5JsJRdN8fR0SLmkRx/cqzvM+BeRTaWNprwnSOG8fELelHtIzyNeiSCZylQYtRe6PvOOUbyyTLV29VwAAAAAAAAABAAAAAgAAAAZzZXJ2ZXIAAAAKAAAABnNlcnZlcgAAAABXoTz7AAAAAFeqd3sAAAAAAAAAAAAAAAAAAAEXAAAAB3NzaC1yc2EAAAADAQABAAABAQCjvu3htgpnsNwH+VrY2Y6Zc8k2FLMPRdWioNycEv+fOZPPa3Kf/4DNXIUcIFHN/1g65Akzlkx8E7900DkDlC5XPsQSmRTQUnr3UnHCFZFhvLhJvMILkU8A79PjLfQOJ9DByCDvWZXeQ587ZVTnkzPi/xGPcYp4mRdeJQh3H/6eMUYNitnCU/qdw6NmeCgW4uYBmek2rtiagU6TOyPhpjUEA9rVutZF8V8Z4LkBoknP7RXnVontFtd2fvTd7SJ1UMz7MJNTLymsl20c1D/tfxrRz+nOthUoQvUoofGD8x5zLgyAiWBgY3P/mulbXglfnbkEO+GrtMy6yugcoSDb2uCjAAABDwAAAAdzc2gtcnNhAAABAAOhVGFV3dU7YZtKU6SQcQPzBakIN9+LuXt2T+jR/1EU3vijPUsQje31J6x3TRNa9lEHkHm4cmiM4yR89W3YSNPp/wOzkjroDLnHsG1WRCiZMRg1QaFiOj+zfp8UXXkre/f7XkioWgs49mJmDRWcgGAho/Uyr4J6W1TaLBpJtwC5+NLXdNgx3JIKBcAf0xFhMCjR7Oinbs3o+ElAenASDYcKlC4XcZEVFeUWZ6aEKvqgGxtyd/T0n7zhR7xUkXHCibJuoAxTd9//9y9KMooHAWRmGdM4vdHkBAULJFxmezTs9hDvqP+LTlJhsk1co5T5VFrHRdxrBXX6Sq2bYcJ/Fiw= -------------------------------------------------------------------------------- /pkg/server/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "github.com/square/sharkey/pkg/common/version" 6 | "github.com/square/sharkey/pkg/server/api" 7 | "github.com/square/sharkey/pkg/server/config" 8 | "gopkg.in/alecthomas/kingpin.v2" 9 | ) 10 | 11 | // Run is the main entry point to the server. It parses command line flags and config file. 12 | func Run(args []string, logger *logrus.Logger) { 13 | app := kingpin.New("sharkey-server", "Certificate issuer of the ssh-ca system.") 14 | app.Version(version.Version()) 15 | 16 | configPath := app.Flag("config", "Path to config file for server.").Required().ExistingFile() 17 | 18 | startCmd := app.Command("start", "Run the sharkey server.") 19 | 20 | migrateCmd := app.Command("migrate", "Set up database/run migrations.") 21 | migrationsDir := migrateCmd.Flag("migrations", "Path to migrations directory.").ExistingDir() 22 | 23 | command := kingpin.MustParse(app.Parse(args)) 24 | 25 | conf, err := config.Load(*configPath) 26 | if err != nil { 27 | logger.WithError(err).Fatalf("Error loading configuration file") 28 | } 29 | 30 | switch command { 31 | case startCmd.FullCommand(): 32 | api.Run(&conf, logger) 33 | case migrateCmd.FullCommand(): 34 | api.Migrate(*migrationsDir, &conf, logger) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkg/server/api/authority.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2016 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package api 18 | 19 | import ( 20 | "bytes" 21 | "net/http" 22 | 23 | "golang.org/x/crypto/ssh" 24 | ) 25 | 26 | func (c *Api) Authority(w http.ResponseWriter, r *http.Request) { 27 | if !clientAuthenticated(r) { 28 | http.Error(w, "no client certificate provided", http.StatusUnauthorized) 29 | return 30 | } 31 | 32 | var buffer bytes.Buffer 33 | for _, entry := range c.conf.ExtraAuthorities { 34 | buffer.WriteString("@cert-authority * ") 35 | buffer.WriteString(entry) 36 | buffer.WriteRune('\n') 37 | } 38 | 39 | buffer.WriteString("@cert-authority * ") 40 | buffer.Write(ssh.MarshalAuthorizedKey(c.signer.PublicKey())) 41 | 42 | _, _ = w.Write(buffer.Bytes()) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/server/telemetry/telemetry.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "github.com/armon/go-metrics" 5 | "github.com/armon/go-metrics/datadog" 6 | ) 7 | 8 | const ( 9 | // Prepended to metrics 10 | Service = "sharkey" 11 | 12 | // metrics related to github, should be used with other tags 13 | GitHub = "github" 14 | 15 | // metrics related to background sync jobs, should be used with a broader tag such as "github" 16 | SyncJob = "sync_job" 17 | 18 | // metrics related to fetching, should be used with a broader tag such as "github" 19 | Fetch = "fetch" 20 | 21 | // tags that describe the metric being fetched, should be used with other tags 22 | Calls = "calls" 23 | Latency = "latency" 24 | Count = "count" 25 | Success = "success" 26 | ) 27 | 28 | type Telemetry struct { 29 | Metrics *metrics.Metrics 30 | } 31 | 32 | func CreateTelemetry(addr string) (*Telemetry, error) { 33 | var sink metrics.MetricSink 34 | if addr == "" { 35 | sink = &metrics.BlackholeSink{} 36 | } else { 37 | var err error 38 | sink, err = datadog.NewDogStatsdSink(addr, "") 39 | if err != nil { 40 | return nil, err 41 | } 42 | } 43 | 44 | metricsImpl, err := metrics.New(metrics.DefaultConfig(Service), sink) 45 | metricsImpl.EnableHostname = false 46 | if err != nil { 47 | return nil, err 48 | } 49 | return &Telemetry{ 50 | Metrics: metricsImpl, 51 | }, nil 52 | } 53 | -------------------------------------------------------------------------------- /test/tls/badCert.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEMzCCAhugAwIBAgIQaS25XZ7jmw/3tJMsvu58YzANBgkqhkiG9w0BAQsFADAT 3 | MREwDwYDVQQDEwhDZXJ0QXV0aDAeFw0xNjA2MjEyMjEwMDVaFw0xODA2MjEyMjEw 4 | MDVaMBIxEDAOBgNVBAMTB2JhZENlcnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw 5 | ggEKAoIBAQCYLGGBkttDCDMfnvP2jF6oqjIVNKSgFoSYf/W3ZF6StsD6u5BGC71+ 6 | Fz3JNt6zc4wszYBdd2Av9D9erROthxhKWnpJKNcm6RLHf+g+UGYaUoYd5Z67KosH 7 | QDtJB2F/AZCwI4hc0whF+XdbU4HIFDyngTo4q3qjoorIXVb9VnHTifpQ7ZZQGxhn 8 | 716alSWQMQEt+0IWOtVBcxhyXEimYsC6WCxsLVEJdWiogehIRRp8cdpf0BDHJ3OO 9 | HWw8J5f7jTa2twKxDb1gPfDquatqweH00f0QFRIyGZ5XtbXGTbs3UeSKZyZ3flHh 10 | zbh6IWlpAm92nrKfj0phxsP17YzaIEPjAgMBAAGjgYMwgYAwDgYDVR0PAQH/BAQD 11 | AgO4MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUBBWM 12 | 9rEHrEUv2cSTVDtoVoTm8tswHwYDVR0jBBgwFoAUtDNotmE28mGxZE+uxArreMUY 13 | rV0wDwYDVR0RBAgwBocEfwEBATANBgkqhkiG9w0BAQsFAAOCAgEAshTrP/hbKjFh 14 | qf0CAOFSLQYn8Z+3ZgAx4G3ClVq2XBFc8mLqFn4SGw31aKXHIXJpQZaPUkaTde52 15 | tcRnmdjedin9a+/J+UenTCNfjIAGYNZ0R/zc/iP7j806DKRDIJRmw9b8j5h2h9vO 16 | Y+cm2viP3DQ9faek2exiTJ9wWHtqO0VaK0JKHeiDyd48rWMwZbGrJIgATAOJqfL+ 17 | Qd8JH5wMX4m/nPWTTKW2nT3v8Tt0ULz3JCO2bpltIcPun0HQX69cK2UWqoChyEkl 18 | wGjmJiAPukJwZnFBKdiCBtIA+DTTAlCrjudAeyp719vARFvrtUL/15KWL9oq/gy9 19 | bD0GpvICJ7HV+sgjgE+zX7svekTfHt9KQYNdQDAoUjNpVq0Pm6IIpF30rHVxjwmC 20 | Qrp7PrlB20FWz6gEvwImz70gaL4xlZn4hk2aScVGr+gn8aBDB78EfMy859SAjDSD 21 | Euba1PNh+IH0/24IWcdT0ZZEPnj4KsRA1s58ONGFuaYX4gyIx4jwXoCZHhO+9Zkf 22 | 0oOSpluW9HqVu5CNr2buY+I2NcTKcLiaAiLfYufjOKehgxKalZWPMBmSZI2C2msj 23 | CbDI1S2IlA6B5tRC9rPF0kpDzCqvEEmtBPKiKMXLfSd3DGuB2nWhU5YHLMMC0vyw 24 | xxj8Jwb8iDQQYabjiG6Gdil6OZm/a9k= 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /test/tls/testCert.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIENTCCAh2gAwIBAgIRAISATQqrpk6wmM1ONlRufA0wDQYJKoZIhvcNAQELBQAw 3 | EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMTYwNjIxMjE1OTE2WhcNMTgwNjIxMjE1 4 | OTE2WjATMREwDwYDVQQDEwh0ZXN0Q2VydDCCASIwDQYJKoZIhvcNAQEBBQADggEP 5 | ADCCAQoCggEBAMFR4ZxCi5L9kyq+KPVA8ERDP4IPJicUbD/Pc+FI8FJx78+8QHzV 6 | rZlepnMef/kaC/JAh9UXdbYazPsY0m1O+5Jdm4mNAUXXbvhs/9JHl50S8nftGqEy 7 | EqgrFmPw6yBiHKQcjfvWVyeJ/OyriU/EsSJAS64PxpqBX2hqZ7k+RIRs/s4hmiJT 8 | 9A+j5iHZcc6HW4nwJcC7rP5e+oZ5JTVokGbhDTAqw2keiyEvL2g8KfAfhA6ocb+m 9 | Oz3x5jI7bYuJx03ssMzZDQLc4tP+2e7hAObjBOWADEG2+qSM/XR+tbkeD4ZQsDU+ 10 | 2foK5snOXHLbY9/cs7RI0+nnHs007KgLS0MCAwEAAaOBgzCBgDAOBgNVHQ8BAf8E 11 | BAMCA7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBRf 12 | QMxXMzMeEE5PkmYfS3W8oywMITAfBgNVHSMEGDAWgBS0M2i2YTbyYbFkT67ECut4 13 | xRitXTAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4ICAQBqgEe4nBdi 14 | ZOl/A5MdXgRhCtI2ocFSPQFJnAP76X45zJSrFUxQ1lwCdT8lzDj8GkPxl8aFlvZJ 15 | Dd3bUkxtRULwE09+X2JgIKoXPzg1H9o2ZgA7KCSuS2uM+ol4Ai/LT04Ld2fKq9NZ 16 | vecL0NC5Y6sZZyUdZjo7qWAS1tyBu1g2pBcAkdLcQ6qZo2g2IKXU84K0o7TPy45o 17 | hPUpOyl7v0iSoY9vDwJxCjNDbyN5DvmEOEFra9uLvoIR1ohPyAZTDWCQMn+Da2Id 18 | nMb5UkRfsomSjI2Z97VsU06bR53IPHu2na3SGx0pBqxpopCJXLSFt8nmSyyhc1Gz 19 | CJm6rwJw+l0/9zUlYbz1MYWk2LlycsMfvym3CvYAm+uS30JW40D/KjQ6E53qn/82 20 | Jf6QrXGhT6iI3G9VE3vKNY1LeEjUWlrqTxtBiJCR6FloN6sFkfN8XvxooA1BXTyQ 21 | WaGQU2BfSKJjgD1A+PWKlKRbUGIEuElJM9qpYG1mz2sHhav8ixgDc4dA3nnD6e1L 22 | blsi4vBSzWWgPqkLMsfj7dYKeVTYhxiCmdaU6xS3cZAkqyZyLBZ4xuInweXF9JHE 23 | DVmKedJRzm4+oIdBaA2ruLC0TLMg1ro+3pvhVr3jHadUxE9xc3TtaP4WrQTj194c 24 | XTI0UgskKwwt4lHDCvA2PszZY9LXXj+w+w== 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /test/tls/client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEOjCCAiKgAwIBAgIQEn+AAPGDiY+do1UNz2IXnDANBgkqhkiG9w0BAQsFADAT 3 | MREwDwYDVQQDEwhDZXJ0QXV0aDAeFw0xOTA4MTMwMDM1NTFaFw0yOTA4MTMwMDM1 4 | NDNaMBExDzANBgNVBAMTBmNsaWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 5 | AQoCggEBAJoeTCFw8g/1f2p2/DDNPlRzpaW8VwFhWJPZ06NKfAfXDAllt2CZrcRf 6 | DlSpLM3zlkGm1nScq/bIJSwsliRc99hcE6LrwLvSSF+1hFAf1SzPZRNm7CcKJJUz 7 | Y/6bOjLsHdrRe+TJucRf/AumbV+JcYn7Hok6IoJN2ix2xkRsj/EoD406wkIcc4fC 8 | 0cQqa3u+mqrMOGC8cVPHXY4jFaA0VP5X6N2ap2TvCcZ8SevQl1stAuwgs09RwS5A 9 | k3D5z5ag755McqlwDBD1x7zgXTksZbhs80zdH3qC2hF44oBneyk1Bh4H4T/i/lDI 10 | os7641z6wL9F44PZcwhMNvw9cloVWG0CAwEAAaOBizCBiDAOBgNVHQ8BAf8EBAMC 11 | A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBTwqeuW 12 | vL8Z5LvH4GSp1xH9H+FSYjAfBgNVHSMEGDAWgBT++OAJk0MVudlyXQXi1AkCVCQ3 13 | NzAXBgNVHREEEDAOggZjbGllbnSHBH8AAAEwDQYJKoZIhvcNAQELBQADggIBAJV9 14 | 0N/a5ODU8gK2wFdwpbVqivTb6bE2y5c2Nqs9rGMSetWgpI66bxgxd+BK+LPqZfqi 15 | jZ8V73m55Bz+LRGHkQDI6YDI2zZMhuiWxOg9wldaAfG65c8qTZGR3Z2rrvhKgeY6 16 | V/8DUdRNbElUFHKI0FW43lH7Tj9Gcs5hOeYdkYjkdHcdK5PMRwIC6eqkobsMLww7 17 | 3A/KTNZiXhUdd/KW4eFpSv1HKxVa2GUj5xhdDeKFi4WvMxbIRcbZQ7oemoVTcpN7 18 | Gs+H9JbbdMWtArHQBRucT1wALNkA+j+fTOSFU9f3PL/+jMGPAk2S/MoB1fLs81VH 19 | eKu6MH7vK12+HA4qs0g4VKVvzQGhaYw5rO3D+9oICHKerrapj6RcRE2TO2KnMBkU 20 | 6xoU9j9hYOPhK8JD74F7aspWVrxFVEJhNT1hbbNBL4vFO8D+V5ygsOfjNw/tV4qr 21 | LjjuPXVpJg1YePL3CWswHtC+X3ihGb61lewhj2FBtNo6O3ROP24kPlFmKNqvsxzI 22 | DdN3nkZCFZ2Yac/oYsalaTc4L4Hfe03HfCOUatQIx4RTEbqMvlDbAhmceJIVRYXD 23 | Jfnn9NgHVTw/zcewEmxBWEIeYTJ/2kneWsB/uaX4lA4JuiGrcD3x6R3Hl34k3Wvw 24 | d3WpVB1K5TRxvvb6K/20EPhxiz5Wgd+k9IRN15Mt 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /test/tls/proxy.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEODCCAiCgAwIBAgIQCtHzDY2YGw3G5NB+YESg1DANBgkqhkiG9w0BAQsFADAT 3 | MREwDwYDVQQDEwhDZXJ0QXV0aDAeFw0xOTA5MTAwMDU4MjNaFw0yOTA4MTMwMDM1 4 | NDNaMBAxDjAMBgNVBAMTBXByb3h5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 5 | CgKCAQEA5pZz/Y/szraKelWLxnCcdD//5qUBK3PyrkviknZS0xVbfPxqCRQUjRwP 6 | mzmxLaRDdPcK87pssxDmjr+yg6whJ1n6+2LVO9g8+4YBR/ahZr30EfpGKctjKgWA 7 | GBU6AdQe2Lx4/rplkUMDwJTFIuodJHzYbpr9F0bFPfwv8dwyV5P2P1tTyneo1Oyj 8 | iJWoTimNIibguQ51DnU3v+F5kweLbXdHV5HLfEPwTSE3vtSUh9pNO0yKBF510bVI 9 | 5ZYR86REQmOrrBtG0DORzEQoXhMrMQ5/UkqNmKKKDAiM4FVaokea/zK7lTuWlUmy 10 | f4oNaC+RFLLu/koZsoe6WgJqIdAkqwIDAQABo4GKMIGHMA4GA1UdDwEB/wQEAwID 11 | uDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFDHDxOVs 12 | //HIw1G7k9Nn6wE9YYtJMB8GA1UdIwQYMBaAFP744AmTQxW52XJdBeLUCQJUJDc3 13 | MBYGA1UdEQQPMA2CBXByb3h5hwR/AAABMA0GCSqGSIb3DQEBCwUAA4ICAQCuP8Jj 14 | Il0NjBJ9V+V6bsS01aeiJx0skXtgQjLNnbXrfDQkwSOn6JGPItsHdsp3wBBmCQWC 15 | upfPKlKuF4Cn8ACdMMConQv9Hd3TTuCTpRMXGlDnZW1PzhRB2JzR+af3csR3isrJ 16 | ArIt5JfbwG0z71tbamvhu3DNVFL5hAhEEQsU3zCd3xFp+3erxLdtEbzTk+ZsXRX0 17 | IObDbqilSLm9a6Z+M50JlZ/cvjh4J1G0uOQwX6NwUJqvbYnPRVWSC3QMe5TbyPak 18 | LgRSrau5e7DP18AifcevTZEM1gE1i7jyB11Lf64HrvuNHM/TcqqD9uOa3yW+uXea 19 | CULTGz0Jbm87OgB882YMMfCxUJBj6oS/FVErTrnOFiyOxU4ljZyAUx31ul1VihLQ 20 | TT3ILME9whb3IisBa/hA7X+2FYnQYtr825gc/vWxHFsj6+XatnDQO2F68RluOqYd 21 | llWDh2gZPaJLAm9tTPDgLB+IU0DLSEgzvDLUxi0Yo3b5NH/EEIuNeAeT2kdhCXxS 22 | f400r8fZKhFzvbhvTJMtp44r05P7clyYDMhvxoGqlI0/mHhobJba4/Rk37EMa7op 23 | 82X7Q0exaVDRYya7KBxvOHpVLJ4WfyztsmJw1EbyN1TYizRdYDh0BBB4+AF+gUnp 24 | B7yxVGirgwySNxTIfQqEYy1OUzQY7709PoYfXA== 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /DockerfileClientTest: -------------------------------------------------------------------------------- 1 | # Dockerfile for integration testing 2 | # 3 | # Not intended to be used for an actual setup 4 | FROM golang:1.23 5 | 6 | RUN apt-get update && apt-get install -y openssh-server 7 | RUN mkdir /var/run/sshd 8 | RUN echo 'root:password' | chpasswd 9 | RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config 10 | 11 | # SSH login fix. Otherwise user is kicked off after login 12 | RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd 13 | 14 | ENV NOTVISIBLE "in users profile" 15 | RUN echo "export VISIBLE=now" >> /etc/profile 16 | 17 | # Setup user cert based ssh 18 | ADD test/keys/server_ca.pub /etc/ssh/ca_user_key.pub 19 | 20 | WORKDIR /app 21 | 22 | COPY go.mod . 23 | COPY go.sum . 24 | 25 | # Download dependencies 26 | RUN go mod download 27 | 28 | # Copy source 29 | COPY . . 30 | 31 | # Build & set-up 32 | RUN go build -buildvcs=false -o /usr/bin/sharkey-client github.com/square/sharkey/cmd/sharkey-client && \ 33 | cp test/integration/client_entry.sh /usr/bin/entrypoint.sh && \ 34 | chmod +x /usr/bin/entrypoint.sh 35 | 36 | RUN echo "HostCertificate /etc/ssh/ssh_host_rsa_key-cert.pub" >> /etc/ssh/sshd_config 37 | RUN echo "HostCertificate /etc/ssh/ssh_host_ecdsa_key-cert.pub" >> /etc/ssh/sshd_config 38 | RUN echo "HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub" >> /etc/ssh/sshd_config 39 | RUN echo "TrustedUserCAKeys /etc/ssh/ca_user_key.pub" >> /etc/ssh/sshd_config 40 | 41 | # Need to add ssh user for testing user certs 42 | RUN useradd -ms /bin/bash alice 43 | 44 | ENTRYPOINT ["/usr/bin/entrypoint.sh"] 45 | -------------------------------------------------------------------------------- /test/tls/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEOzCCAiOgAwIBAgIRAKhMifnZml5FzClFU9zXMKAwDQYJKoZIhvcNAQELBQAw 3 | EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMTkwODEzMDAzNTUwWhcNMjkwODEzMDAz 4 | NTQzWjARMQ8wDQYDVQQDEwZzZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw 5 | ggEKAoIBAQDJ/icRfV9kNBVockTw4jsyyI3XXulq9k7Mv+Kmsi3zNISfNino0oiU 6 | 9sm1wDdxrJ+h2I5/HiOi4f4OsEiDyrcDFX+mFyAYbjxpVauYRlVsoO4L6CSLNObl 7 | y4utM8I1hIWhSWCWfRC4Tc+VYt17jgQdD2FucKZx4e7H9EHznrhjMXbhkTjawi37 8 | LzH2y0sVMAr7vavOWRAXQvG3yASBCkrmbe1qIn4WsEQ50UPKFOvnHch+y4Ya51ay 9 | W3omc6W3bAEJhdZ+y4P2JJ1cID7p76qAWwte9w7eAl1Xgr6EohcoS7QxERC7XWms 10 | EtGue4NddYNGLGZM4Hc97v8xwv8hCNtZAgMBAAGjgYswgYgwDgYDVR0PAQH/BAQD 11 | AgO4MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQUDWVI 12 | vI2HUgt/aLzO2ooFQwPJlfQwHwYDVR0jBBgwFoAU/vjgCZNDFbnZcl0F4tQJAlQk 13 | NzcwFwYDVR0RBBAwDoIGc2VydmVyhwR/AAABMA0GCSqGSIb3DQEBCwUAA4ICAQAa 14 | hWjCNF5LWqJCaO/6xgcGo2VOviz9Xaoh4f4XVHL3aS5KjxKzjrKHop5oSKmXQa31 15 | b0IAp2cSwxee2cL2oHE996ZfN7qbmu3zmcrakc+u6WH+GNibUfzvQs6FblcT+rUh 16 | uoFauP3rMgQ+1lieqi9c77XX8EDhsN5kSncpdyT6arn8Rj3aaW24J/MIL6WxPlYt 17 | BhPzX/bbjs3gQCM8m6VOZc6QTp/5wTMi8VYJQ1syiPNjfZtW3+xPFBrCiZSsmK/6 18 | NgGuA0xOzsPPXqcnd3dD8WSUlcQHj3kFh68ccrpX8qFPGxX5E8SBWNTg0eLPoG0o 19 | xpFHLjANquITFFRNnRASj54Q15bunRKVoTO1SSQRjj38jngWKkj1aYOYbtlFCfe7 20 | rM9mYMN2D0LEmbi+XDZcbfjUfGUS5BGSFJgTrNrtgQZLg7oBChVOvlglAwynP6gB 21 | OM48FzEuub6TwV1EVwpyW2vaLlrAbuaakuYnkBo8OIBms/PnZvEK7qzfoTkRs3Ki 22 | wa7ncUPX3FgGjTE2weL25yS8L1vWuXcYCbaYd2S8QDjg4GmVp0xRFpUzcDQVlfe7 23 | F9Q6LzcSU99g3I7KI2wuvGCkmzimKlhpnZfRBqaztkUDyJ+Bscr8VxmR+gXAW0PF 24 | tDxg0HxlCGGufm1ay5BRZdqgRWYcY4VMP5c+h+/SAQ== 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /test/tls/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAmh5MIXDyD/V/anb8MM0+VHOlpbxXAWFYk9nTo0p8B9cMCWW3 3 | YJmtxF8OVKkszfOWQabWdJyr9sglLCyWJFz32FwTouvAu9JIX7WEUB/VLM9lE2bs 4 | JwoklTNj/ps6Muwd2tF75Mm5xF/8C6ZtX4lxifseiToigk3aLHbGRGyP8SgPjTrC 5 | Qhxzh8LRxCpre76aqsw4YLxxU8ddjiMVoDRU/lfo3ZqnZO8JxnxJ69CXWy0C7CCz 6 | T1HBLkCTcPnPlqDvnkxyqXAMEPXHvOBdOSxluGzzTN0feoLaEXjigGd7KTUGHgfh 7 | P+L+UMiizvrjXPrAv0Xjg9lzCEw2/D1yWhVYbQIDAQABAoIBAFTCcuY1YrtKLhAU 8 | bjA8wJnbnG9g/IzCx99QfiehEEOTjoggi9Cx1DJagNwoyn5eB/YFVo59l8m6W09H 9 | Gi/XfWtdgGMquy3QwYL5plIWn7vsN8+K3DfASUxRHh5pqoFVADpW2YeDNgp9K3YQ 10 | yrgvM+VJ2YppEY+OV336uKHq/uMGZboM0/UMAqK/0nHWpoxqhFXo5k5nXI6ZjBaJ 11 | 37nraOk7xw8KSimYKeEYjfCbahkz9bu9nfE+tF2k1ajM1SAEVwySyz2ljFZC8ZsA 12 | oqYA4scVPukDkYYB2he9Gv8ip2xCSOnxK88NWorlgGspPW1lVhuyXwkvZSdDM2k9 13 | PnPusoECgYEAwwV6AXfYZ5Dg1toTlQqClFTcdFjLF83cKNeJplQMb4Q7+dNwZnyG 14 | XS1m6A4Eubct5qhqwzXXw3z9S6bwGL4PEXYE0qXvsuqW+jZfZxEu4y8RrvlY9LeZ 15 | jjqs2W5SEuWd/r9VDBpGzHbT2c675Wrx2y488dPn0Wd7GQvUTzI8vk0CgYEAyk66 16 | puFvT6PSRIrroS+0hcg+nhnQukSuLq6625te1/EhS03bk4iVw/MXkALR4G9F93fI 17 | 4DDMBaF1QmwWhyVTGCTQl+uwEFXJFJSZy3jUHgqsg48gDwzId5nrQq2nqu/flLXp 18 | P2LtC/xKPBDgMGQRsX1mXenFOSwangSFS1khUqECgYEAgMNx7dw6Rw8yVMmCGmrp 19 | EpUBRdSGq73hOhotqWNfHpY5n1bKpPBdKtJaWqc+2Xwn016ptyAqyMkS2MttRXjf 20 | rBC3WHn1TLV1X9lcnkmLIrcmPtglstYyjeUR0TH1AMMY0WV0+tuymTdv4ySLjQtS 21 | ivv5g1X9fpaLgVr9IBk0YBUCgYAKXz7yj1xFmQCOwxCRkwCOW1XahThOVHcZrZum 22 | 5rBWIeazFarMRZRoF25906cu+oV7yohh9h5/q2d3oFMHWsKH2ltXbp34OG22wGei 23 | Ju+5GpL9q7jZDK66cwm8wWp3ORUdarYqqce9dZHYwoS79mVx1BwLdJDH/ZrfvxuW 24 | YVt0AQKBgHTq2GZF3R558HPxElyoI5V2fypKwiiYshG1FlK6dnxhhjsdMFUb6/M1 25 | Uble0LJfaM3Im1S189fkXTlihqHqxCcRZVhRRhN1tpJ/CvjADTcGR5lZ53JkrMsS 26 | mD8U7f6lHbIQyOeMVzHzIh1PSsz6lz2l6Xz62P1la+UMa5slFgI+ 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/tls/proxy.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpgIBAAKCAQEA5pZz/Y/szraKelWLxnCcdD//5qUBK3PyrkviknZS0xVbfPxq 3 | CRQUjRwPmzmxLaRDdPcK87pssxDmjr+yg6whJ1n6+2LVO9g8+4YBR/ahZr30EfpG 4 | KctjKgWAGBU6AdQe2Lx4/rplkUMDwJTFIuodJHzYbpr9F0bFPfwv8dwyV5P2P1tT 5 | yneo1OyjiJWoTimNIibguQ51DnU3v+F5kweLbXdHV5HLfEPwTSE3vtSUh9pNO0yK 6 | BF510bVI5ZYR86REQmOrrBtG0DORzEQoXhMrMQ5/UkqNmKKKDAiM4FVaokea/zK7 7 | lTuWlUmyf4oNaC+RFLLu/koZsoe6WgJqIdAkqwIDAQABAoIBAQCyXfl1T8rHkA6I 8 | WPoZU2zCw+roeATynus/hjXKQ7sHy0KM6RgA08ad7PHpifQTMTh5FswjArcowM8H 9 | 5yNolVLEBOePY8E8XKW3js4Y45+wQQm2ilmR5OFdVQnkFy9a0MHXt9sEeB0vA564 10 | bpwbyOsoGWa2EC/svHLA4v4XYdSTRspMLZxVtGkKaR06aOxeQlyWYrLGoi+4uiiv 11 | Ff5eaWnMpjtGGBd2AK7ktKkWGDp2IJmOhJGLiUALzzFuBO7vfi59wuPfFZHNY3tR 12 | v3NLsQTRg3WnA+GT3TTj8DsWGQO8EKbxwJz6hW22dNzYZ6YhgzP5ysgU+ocXtf/U 13 | CNeS4XnZAoGBAPbDRiG6+/MFVRtdn6bZvEnSEzlm5PrN+FoqjGM6wbY/B4syywwZ 14 | Sf79pYe3K2r5EgWtzRygu5Akgu6hDm1kYDjTN3z1d4lkr3qZlJWBbfCqM61FpUsk 15 | WniZsRQ4/AMdZghvJopDtS/gEzH+axtLSU1Y5kpHvFbPV4wNwIT92O49AoGBAO84 16 | LGbhP50c+ry5O8UX8OHEVkj1ZGbmx019qmGcMuets5mFQZI+9JCqUPjrirgjI+zN 17 | UlehCbfqNlnRM1TZ78x+ZqAxXi83tWSzhx1JWJM0KarYaFsdHOIo1gjVi0ugWaL+ 18 | Irs2fxKfxL0GZJW+/IwC8oXMpMH7ejFy491VuzUHAoGBAM+8bNoQk/Ju1ssG8lSA 19 | kOaEl/KHENWjeCRQFYej+1f1j+A7jnxM/eJQnn22UjLoI6fsPPSQy9X5JtDFfWPS 20 | UL8F8XydGVjtDl1j1ZZXM7qbTo3cR98OZ6uHl2+y2VW19iO7jvJvNaVsOjyoNnrv 21 | 0nHvGVAHPCLIwqcBHMXY/jrxAoGBAMoUR934tcZBLsayK32JheER/FnRgikzFnWt 22 | jHq8enwfzjIH0aZ7LBnw5koAn8SBWt21bLO9w/nrDlK9WIF7QQkcVhFI4uk8RC13 23 | QtJInVxsmi5KdY1SgI3ENVptGiieoloAGNLRbHMNKCMN/XZKSgj0jG7euudrSoPv 24 | K/JTc7uNAoGBANPWpINOHWyUadCGoGi5A5cmhRKvw9EJmOIAMIYpaGPbeKAVkkhQ 25 | ecex/Kya0Y7BNAKGsU12Rd0xYAyhQQndmnCZ9zG1VIrJ1hzLsXuawDbaYLx/50UK 26 | IURTbYCB0mjcW4z887LXayUtuT413KrklQrW0vaMuWO49N16kJ7XDbbL 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/tls/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAyf4nEX1fZDQVaHJE8OI7MsiN117pavZOzL/iprIt8zSEnzYp 3 | 6NKIlPbJtcA3cayfodiOfx4jouH+DrBIg8q3AxV/phcgGG48aVWrmEZVbKDuC+gk 4 | izTm5cuLrTPCNYSFoUlgln0QuE3PlWLde44EHQ9hbnCmceHux/RB8564YzF24ZE4 5 | 2sIt+y8x9stLFTAK+72rzlkQF0Lxt8gEgQpK5m3taiJ+FrBEOdFDyhTr5x3IfsuG 6 | GudWslt6JnOlt2wBCYXWfsuD9iSdXCA+6e+qgFsLXvcO3gJdV4K+hKIXKEu0MREQ 7 | u11prBLRrnuDXXWDRixmTOB3Pe7/McL/IQjbWQIDAQABAoIBAEgwesn3YqYvLw96 8 | 90SXtcx6fKbiFs3RZWwrj9c/isiGlndIJkY9J+8FHCXGoooPxaVT/elUXiwSVHfv 9 | bJsdUbbachpr3V6d1x1WLtNnH5SJF0pOFvFhYLvzuOGXw+rYh+GSleBypg0YUf3z 10 | rXBA2xt/qbSMsg+1TV9M3l0w9nkMS8qk21hi4WMIx5IQ9hVueaNb1OEBCSeRrVax 11 | zcg0d5Jjgd97PZHxObjDGWLSseFHNkY+WBYAK57IPEK6tmw5xhCyvufO6I8AyoD3 12 | cHeXIOKsZ8aSFpwPwJ+yescim7+GIyKxy3AkpPpXAs9o++bgGXrjZyVP/ZxSFm1D 13 | YxYITGECgYEA76fF8Tvx+fO+4puhsOq0r2qkPIlyAo6V1vsWy/XJrrh9IzJFbMV2 14 | mfmSMol7uHtMxJPtA6EjfiGA2H9h6DtmDyppGKc5G5m3R9CeCeiR+v7bhL0wXVSn 15 | Cwv5KQUpVxS9zD2Traf2HD0J1ik/f4k98ndRmMogvhEBcLi8QsW9VM0CgYEA18TQ 16 | s3wejRvPalJkE1SZfZqzm/EBb5cdzbiR9MdMarOOxIu0nZQhHsOG/Ab/PBPzNXrD 17 | 8kjiPDbWRRwNYKK8GWkl4h+O5/UljU/al6W2V/s+Oc5T8EXDJeHKIXhq7N2bCV2b 18 | 4UXmE7aPeSTg9GAp1XV12pjQnGH8JpSMwRtGQL0CgYA/DsoKe1dkCTqEraaRwm/g 19 | aBLmytNw0MukUVTiPb3fdzOV+zhBMoPOZ9iL65jeJbNBVhrbBZ8tJOueC4ZbcKWd 20 | /+6/SeDA3mVXRBERUlx2ynBAMPd01z1Mrs3UeUMzYoW+I8Wjv7oGHBlmfFv01sux 21 | 7KtPpH+RAzB51GHUv+rdUQKBgB3mnm1hE/LGGdZIGKo9HRA08hL4MRS/wleR7lgX 22 | jlVzEKPYIG1966ERw5EFfzVaJmgQ3TqkMwXZK5RkMM5lft+enKtbaDho1o1gtZAy 23 | XLSQkqz8FNHFOSf0xEgjId41T5jaqhFr2Fh/Ah7tp2b2NJXqMyzZ5H7nbUQDbgYM 24 | u42ZAoGBAKegiY8Iksn49Htf5ZqPhuJQIo4RQat9DHs9XRqHqlsAFEmZ/rp5ENXI 25 | /eBwcMxox64H8INIyGKwqaMD1/KpxUQVLPYFNj7gaZkC6jaCRyvu0Auxopwslp78 26 | V2g6tTqeBfAlSNblbryZoxf16/04xq2eDBoxzMVk+qFpFmjr4WXO 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/tls/testCert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAwVHhnEKLkv2TKr4o9UDwREM/gg8mJxRsP89z4UjwUnHvz7xA 3 | fNWtmV6mcx5/+RoL8kCH1Rd1thrM+xjSbU77kl2biY0BRddu+Gz/0keXnRLyd+0a 4 | oTISqCsWY/DrIGIcpByN+9ZXJ4n87KuJT8SxIkBLrg/GmoFfaGpnuT5EhGz+ziGa 5 | IlP0D6PmIdlxzodbifAlwLus/l76hnklNWiQZuENMCrDaR6LIS8vaDwp8B+EDqhx 6 | v6Y7PfHmMjtti4nHTeywzNkNAtzi0/7Z7uEA5uME5YAMQbb6pIz9dH61uR4PhlCw 7 | NT7Z+grmyc5ccttj39yztEjT6ecezTTsqAtLQwIDAQABAoIBAE5NTRWOupvqC57n 8 | cQ2NpQPxPRr/6dMwaXwbGfOpKHYsYhcBSBmILw2NEdxVAT6zdx1DWAFOxEXjax1V 9 | e3383Nb3BVXcgCR60x7af2/7wYREtWMv7XZXIsls0l/eqE3wj2tFiZj0w05njMOL 10 | 400k/R5DbqPtDeNs/Wj8mFCm0PfVvbyIffp3m5rDOuDkjAEf+jiINNvlvDCKzaDo 11 | CZleKbopTrLCK7IFwFWI7GNB33u/iWxqe8SDArR/loyvwYTzKRufLHnW3W15tPP3 12 | a2t3aPVZx2zY4IDxYWQY0MeuPmuOshv5SeLBmu86DXQE9KsdVITTjCLlLD41GvfN 13 | 37GixgkCgYEA5bkKXNdyqqhi03ofsCPbnO/akSCjAVBRjz/76MhgKHN6AP565by6 14 | b/PtzaQ90HP/AwM6ZlC3kTtKcpE58RjHyBk37UCt+B/gcIOMcNewRQOrFDozRIhX 15 | 592an3S1LmnyBUfNNm++jOABuvXery9lyGSd7FLoRHIaLoKGKqVP7P8CgYEA127a 16 | 3PCO7SA/O/z5dzzJjQHtQVa6jdHDpArw0vvwn39CbkNnfjIonleSH0mqoeo7egiu 17 | pea96V3V1r9QXz/DbuyQ9uHmcVI1hna18ZfGhTzBhgn9z8xBZUH2sFqpk7afQpZv 18 | INCWSGgdILKwfglCA/aIkziquLFBkk1bBhofrb0CgYAeJDxW2DIEcFmfM9vqiZns 19 | KpB8EFMy/e3lpNiRv5DWXeh5LurDMBMqU1A1dkJiEoY4R/kmqZqcZLIs/B8lIkI8 20 | YAq1h5IMB2q0eJ45xCMtuwB8g/JsIJOgKbR7DZ4kO+R0iupDJUBUTaQMeuxAAjER 21 | rRoHgw1Uxb/nsFqYR96H0wKBgED6X6saJ5HgExKN59SOEiCkvyHg/d2+siqtXhvU 22 | /6ur36aQUAvhJx0zPpCPUJcLpirVqY/Ce++CbPgbtis0eUbgtYyxcCcn65sF/TTE 23 | WY1gWOKL6vEdI3BeKADjJ5i1EW4tH5GfOGTYekidfNxXAIFff6wgAGY6mJN/H4BE 24 | qt9lAoGARjkoEJJJbC1h6MawlacJPKAdhPpQoWx72GE7UcyGB1PzEUwaJPsNOEIm 25 | e7dPZBhRc9yfgi6uEQWrGLbrv8m2yGCkIg7kEwd8orburN++n3X//3dZ/TsR9Ey8 26 | 5yHPY8aBIifIWgmvm1oABrMRhxd5x4DuDtSS1q4g9dl21E1ckJg= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /pkg/server/api/known_hosts.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2016 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package api 18 | 19 | import ( 20 | "bytes" 21 | "net/http" 22 | 23 | "golang.org/x/crypto/ssh" 24 | ) 25 | 26 | func (c *Api) KnownHosts(w http.ResponseWriter, r *http.Request) { 27 | if !clientAuthenticated(r) { 28 | http.Error(w, "no client certificate provided", http.StatusUnauthorized) 29 | return 30 | } 31 | 32 | hosts, err := c.GetKnownHosts() 33 | if err != nil { 34 | http.Error(w, err.Error(), http.StatusInternalServerError) 35 | return 36 | } 37 | _, _ = w.Write([]byte(hosts)) 38 | } 39 | 40 | func (c *Api) GetKnownHosts() (string, error) { 41 | var buffer bytes.Buffer 42 | for _, entry := range c.conf.ExtraKnownHosts { 43 | buffer.WriteString(entry) 44 | buffer.WriteRune('\n') 45 | } 46 | rows, err := c.storage.QueryHostkeys() 47 | if err != nil { 48 | return "", err 49 | } 50 | for rows.Next() { 51 | hostname, pubkey, err := rows.Get() 52 | if err != nil { 53 | return "", err 54 | } 55 | 56 | buffer.WriteString(hostname) 57 | buffer.WriteRune(' ') 58 | buffer.Write(ssh.MarshalAuthorizedKey(pubkey)) 59 | } 60 | return buffer.String(), nil 61 | } 62 | -------------------------------------------------------------------------------- /test/integration/id_rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEA6kHIk1k1lYGh8oXsgj0ALuRGzbHviIuPw0QMS8qefz48uLqQ 3 | tOcyG8H6kI2iPvkiJHPEu5SL2CeAKb09Ij5U+19kQHKnDbGiTcV+fC8rxPkUZHXq 4 | XdTSq+nVX3fmcaoahOK3mmfqr7b1eYnWrqnqsdSM4pWoTKNJjgtkRqi0SsAuiyoA 5 | wJQWGkIFRbv9wSFxLpASiHcRnxbCctRxkyrA0fh6c+yrj8A/dfnV9K/cNm3xYPva 6 | YC7jANtVkid1ZIi/RgTtq0kHq4FXFJp33PlkA+9pvBNN79bjETg3bcDc6wq9UEF8 7 | bsbP7kukFltANXqfs9gkpiCcTDkVRy4T5zSxhQIDAQABAoIBAQCt+ILcIzFvQeGi 8 | uiEGLAVZzcBjfJTWxEbVDlFPbD+/Ydo4mp1jLBwDj1DlT7pBqEXZ6nwdjtk4g0Vk 9 | Oh0PtFjqglJypnM38UcHGPexhFquIwko/oU6gBahA7yp9OKYYWrma3VNX4VkT60I 10 | QPzFpH0e6ipjEB/4IoA61Gz5KMsUgy9pSSNRGnnPT+bJVYNpowFWCvd8QmgjHTwK 11 | xFxoRPoZ1IsZY0vIiP+E3dD9c22+dT8R6lKa2Avbl5+D9wJNaM3WmMvysX+5YyJn 12 | UC9OsuX9xvF9fboEU/bLAa3Jj0KFjTIUtAz4P69PpGgrV/nrgQazMdZCFsC2VNZB 13 | BjjSrXWVAoGBAPwrL2niwt4zGihbmXu1Wb3fXYlLkxuV8gOHXKKEjXyaRcnB3Zor 14 | zttA6Gs6l/U0lP/6ntCWaRx7HRB7swLzpSvOjVNiHcSvCM/nkOzRWYdcGv1jJWfT 15 | jDIlgbt/TaMBPr2Zy1AUm8HDoQOiDAAk55U+jdHoXcDQ9SsXgZPeS+znAoGBAO3Q 16 | 7ihAYcDGVK5lR/LY1i+imGULP74a/6XFzOgC9OHi8tmFw5Kyxy5jwe/NygT3uOx7 17 | KJxrj42//5HeBQhNGG428RtJBPUi7Q4LpnNNoknQZMxY2WjE4RAjoJlhPWb26TrD 18 | 3kAwzTeridqaUO/wiYT//RNINwge+//o8q66BBSzAoGBAJVJv80xGWrQ2CiSaiBJ 19 | 8fIt3cNdgZ7wO5IJPjdcwCLHdo+GfXo7e0BkgfSRgMsDMT8GkaUtltbsr/1FLmqN 20 | 8fgoVZTK3pLFiTMEhdEd86HmTng6jTeVj0dU2yQ0rrLVFt7KwQoM2VVySs3Kzs37 21 | CztZCD8AERkI3EyBow32qf57AoGBAK3hq6CGonK/EL+Kkja/0Kt3qRGITg0D1JQy 22 | sgWZ088tjv74zOyABx6mFfDueJ11OyK9Ug48nvO3xHe0690L8ab0SQn4M8XAya8R 23 | WZzI0LZDxs+azyvJd/3C7vP2o1ybgCBVgjVQ+VuQ8vSBDFjDeOlj+niUvpgTf5G3 24 | k2mp3L39AoGAPHADmsNxF49pgRH5HT53Sanqs0i89U7hdsFwHFSbQ6Rws14uWA+L 25 | FdBjFf/p3v0d52yY1+R9Gwj/ZEn1zbdJAHWQzYCZkE5YMb/l63e5grBy4qu1Lx/j 26 | 9m2Yu/PvjF/nD+ZdCxc4PIZA6hHs9ep9WscGCikh+Ph/3LvswA+MsiY= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/tls/badCert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAmCxhgZLbQwgzH57z9oxeqKoyFTSkoBaEmH/1t2RekrbA+ruQ 3 | Rgu9fhc9yTbes3OMLM2AXXdgL/Q/Xq0TrYcYSlp6SSjXJukSx3/oPlBmGlKGHeWe 4 | uyqLB0A7SQdhfwGQsCOIXNMIRfl3W1OByBQ8p4E6OKt6o6KKyF1W/VZx04n6UO2W 5 | UBsYZ+9empUlkDEBLftCFjrVQXMYclxIpmLAulgsbC1RCXVoqIHoSEUafHHaX9AQ 6 | xydzjh1sPCeX+402trcCsQ29YD3w6rmrasHh9NH9EBUSMhmeV7W1xk27N1Hkimcm 7 | d35R4c24eiFpaQJvdp6yn49KYcbD9e2M2iBD4wIDAQABAoIBACg2ukHKtGTfetsl 8 | X2VNLQq3h2qxMbwYl0vRrPac8J0m7JKWFIdePOJgc/SDqpCTvXxthjms/V9O1ZxI 9 | csXNyK6FgIFq6Iig2VSdSZuGFaVpH4YxEyR1Fq6A2+ntqGcu27Sgfz3AixQq287n 10 | n4lnR6wga2lkb5Fkt/ZBLGbgfE4hZRE/LSUDw3IVR7r8R8CCrAnhyffpOZwv6JC8 11 | SjXs8HKJSpyuFH8kLse1wDoM8Zbb53fOT0EsdYY4kPrn7isfTZeAuZbI7bIiEHoq 12 | 27d2oHXFJS3cK8I4pkaMlpAF/LSAKN/B1k7ww1W4jmOiwqhUCaOjyTKUY0ivP8me 13 | FUXfdzkCgYEAw3qCsVESZ0ejnvI4z756SXr9/KrFRLgC3MgOy4W1QZGpwaLrrql2 14 | yzhsc/Dtyj+p0/nz5STkiGOvWbn6/t3Nyvl70MfRTwZ9rjVLCijcedETqxqE6O9V 15 | EzIKtSOXPK6DHGxDj6pbtievjKIyDBrHgBugKzZMXJYXKNU7JFZJmr0CgYEAx0mI 16 | K7kAM1aXnB5+cJ7+B6xmPIphEd5V3dpFSS6z/rsJTlyhzMAI0LsmpW7uBAMrtM79 17 | MPpCdwDGRS/m5wdJqUb5lCfm2qdEvNv2Ym51BkfQ5rR13t/u03C3JrQeBxBAkFVw 18 | 0jnd032D+DG66mDjj/IobAfQPdnWtfF42dSMkx8CgYEAqBaWfya5bnnZpnmehHTD 19 | 7p+F+1hU3Oxl+bdFkZhd7g31LP6NLtmlvsW77E6GCt2HiTFrmeSJZwZg6ATWYYYU 20 | ya6R20uXHh70v3IwHoJYY593DDB3jV0PiLwFHRGHoLRnJW+rAMR6rD2f5IwsAOCe 21 | H/ihV8cPDqY5L/F7M5nHHPkCgYEAh69j/EjofwvNC+mjMvC9iuHxdfTNpInZssRi 22 | +jHHMX+NFYJyU2LIXb2e0XJWsfqqrl9j+g3aZQXs+LxlnRTULWH6mcoVHvhWD7Q/ 23 | 68LDALSy4fEqc8XJ6MJMoRhvfWWuX9ccirYFd3J1AO5zEhNHvQEmTm9/dC02SAiR 24 | vNY/pAcCgYBPGNAxtGz4mJiwIlkhXGYNzryzZs7qlF/QsTOWWxJQ4XnNufOWtc+h 25 | imNzQvoUd8Uk3NjnwTxluSM0WhQG0LxueADRpRR5QqO0Ik0lNykupui2zmm79E4r 26 | hmU4l4k6qg0KRcLlaSs6ajZ0VdB45kh95MqCSruJm6HCWmSwMVx88w== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /pkg/server/api/testdata/server_ca: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAo77t4bYKZ7DcB/la2NmOmXPJNhSzD0XVoqDcnBL/nzmTz2ty 3 | n/+AzVyFHCBRzf9YOuQJM5ZMfBO/dNA5A5QuVz7EEpkU0FJ691JxwhWRYby4SbzC 4 | C5FPAO/T4y30DifQwcgg71mV3kOfO2VU55Mz4v8Rj3GKeJkXXiUIdx/+njFGDYrZ 5 | wlP6ncOjZngoFuLmAZnpNq7YmoFOkzsj4aY1BAPa1brWRfFfGeC5AaJJz+0V51aJ 6 | 7RbXdn703e0idVDM+zCTUy8prJdtHNQ/7X8a0c/pzrYVKEL1KKHxg/Mecy4MgIlg 7 | YGNz/5rpW14JX525BDvhq7TMusroHKEg29rgowIDAQABAoIBAQCJkPdjQD//mztP 8 | y7MCKcy+qOLrd3pzo1T3GzQcP23YRFQk131mZA6++TKvTYvMh/CFEV2VpHi/aQvZ 9 | RLIRiqVgENBDW6570j+SlwYRa3NHbHhbIqTXeQ2pmNMnskyus86TbuIUk+vv3lnh 10 | WaF6KFZYJ7iGDXqrFNrn8i7pfcy48w5WSZWechlKQKutlT/jl3WJl5FbDYoN615S 11 | Q9Hay1+2iqXQLwxQA553+A8aNMyA8DYGMKv/VvfCKpcbAWOuW2YNmjessyUBZLmy 12 | 18spBCPhfsPK1oMq+innNxNSMw9XM4ScDzt4yVDmkRjJAfTeEoKqBI+elyYH7rSd 13 | ZEDpoL65AoGBANONLfA3WXyGIULSYtwQjeYvy04CZyDtRgKNqmdoeF4Htm5zLKwO 14 | 1BsEbYXfgncoc/CUm8cTI54rYxJvjxZ9Hoezr+ISKiSTGW+5FmtBztI1MXydVoC9 15 | vHh6cqs9PiT9Fo27mTFcP9nde3B0/sNHMNcGOvzx0H5j3rHiayyzoQetAoGBAMYm 16 | Z1ZG/UPRWgyPTJP5BMs4CCBN4Y/CEtluMZd+GOQjuW+D0aRBsA8+VMNuW+HvEYVy 17 | PbJLUzfWY7NG9cb4pHF5LWTvwDZldmfsGyp72RC9YGsvpG4tBitJw3xo44JmG3Ue 18 | Z96/AXr9uLhVVJXGVet6wAUWG4o09XsCAex8ZNOPAoGAX2zdfe/Zo7v1IOk5wr6M 19 | 43pHoKag0k91Nw2kAgUz1mZwOh4l1m7R6mXy6WJKvWk38xt1sTTG6j/z7or35lMG 20 | BHxfKAC9lcXswWKh5DvJCTUJX4axUF1FuKqzMt7rO1AWblRi48sS2jl3xuBfUsK4 21 | GyYqUZLaU0jFWUQiyAU26/kCgYA0+wG4VOTU5D4Si6IrEnMQrxulH43G9Vo63rbN 22 | zb50CYVEZtd/9rxPZxgc5P4WS+jAaIpMiM1oaZ9gyQFgQQ0e9gqIKX6YTMq7AKtG 23 | 0bR5QFlub1+kwc8bde6Z7iWesRR4XTfim+mWbA6e4tnp7gz9GGYNNhsI+h4E1MWU 24 | 6LHAywKBgC+2Od9ymFe9pzg+73Oip+4/xLxhUhtF9VCmqxRFbd4QwXK1qSjFvrfs 25 | jubE9AC+8QsPc9Ks12y6pmGQE7lRxjI1eBEAzgBi5246gh3cFN8bCdB4mwIiuaET 26 | R7E9iUfMbhSXCmZbHHpjgazQr8sTKgoZz9ZNk4KAoHjadZCZDmgA 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/tls/CertAuth.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0 3 | QXV0aDAeFw0xOTA4MTMwMDM1NDRaFw0yOTA4MTMwMDM1NDRaMBMxETAPBgNVBAMT 4 | CENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1h/jvLcK 5 | GY8Wt6FziolUzAPbENoxRfPPwawiC2vN1mR/Riap8+y6cKjEKuOlHNLXBY7+Iqp3 6 | 4T4CYBcmQobkJ1f60Uym2fFUKKhMN3k9ZqsF0rl25QYhtTz+uBB2MIfyl2PucAMD 7 | JuPN09WAyGbLk+hLQ8RkYrw/j2bMWFtJVKReXa1D3I0PNGOO28Hm0pl68ZTCuHXA 8 | Jftrk+aZr2es5E2b/KRO1azAu5Jh9ldHiDeLOvoO1XsB1EIvUwLb8Gj1vTakiZaT 9 | Q70d7ToFhsGsjcEtE4GsqvzNyEDI+tnM3pmfjqyihHqRBF6iFW5/4bTncOfDu0gh 10 | C4O3Ig5UQHCkU+0+4B11tSWtebYpon8twCU9eih1je9tWTlx2yyGMqA4WSz/TocH 11 | kGC3KaRbtaptDsSJEGjSwsWwOu2gisaWXC8VZa0Zb38JzowHiNUCOcD6Ee30RnBL 12 | RCLX+6K4McphU7vw/Q0VMaSaehzuPX8jZ+HPylLsDT7Noa3p4HnGADPxUegc07q/ 13 | O8bH66+aoCVJoi0kUX2HE+hiOBU5aLU92iMQI0f1ebx0j1HugV6CG8KttZJJ3tGs 14 | tes8K98KXhG2t1cjf5fUikZ5Aorw7MAL2e8Y8PDs5a5e4UAcuWE0Q21hDutjYssC 15 | dDUX726zUas93LPpubE+Z4i3wEmnDWHHAZMCAwEAAaNFMEMwDgYDVR0PAQH/BAQD 16 | AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFP744AmTQxW52XJdBeLU 17 | CQJUJDc3MA0GCSqGSIb3DQEBCwUAA4ICAQBgQYgbpzTKbcro2mV6hY44hXWcVrsl 18 | 8boYilglx/pa/1AuTlNFcw3QZkCo8oS6SibuYk27SWIZfkirO4EYfBa3YRcxtsM3 19 | 4+/ZioGaK2+x3ivVgMtf+VODFYkqpDwe7fR/mq6oZSaFOKiygos48o+jqGVpJwun 20 | 2GMxz9p93O01Y4VUES8j65cxReVKDVbZFtFX4FCVstw4DckeTBW++R+decIQ+yHZ 21 | hD7RrapucmQ4/qDdlJ//8k+vzyCZ0JzXqPDQGrzjeZ0rB0bwCKA796+gvE6vdUIJ 22 | sjaV2Df3XCCOLYsrc+6E2+tGYwcFwZ1udxmeVIl7eDwe2/82n03NmA6EemD8kreB 23 | UD2xVImwThBDuld+XuTsTc5sBpwdCwkWsPQGEvioUw2q5A9rLEQy7xofKtO5cYE1 24 | xpMPScbR15pJ1ryVhrOLNX0nA5BGVivnCsg6a1er3H5wCnvDA92lYFrUU3V7dLQC 25 | pGo2y6+7MwuZ818AtKwlIWtuJHQGPHocGwz3z9cNqMR2l0s9ijZNKO3TopTS8YHy 26 | 253n0UZrJab+I1LoHVnwfS0RIhwerLnyZY9Z1B2rb2/HAxoqcVjwrZDH5fbbktpk 27 | x/Leageho+bxAdFo+DMEP9PymLMst0CplvEUCzHRdwIAHlW8Ue34D5BG5xfB6rT8 28 | w5raE7pWkTDvyQ== 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /test/ssh/alice_rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn 3 | NhAAAAAwEAAQAAAQEA+C4WfLZVmecvgEBx2LOaQR4QMKI3DXdZryTUJoyy/qkpre5kY6+O 4 | hD9ptzw/F4AVqBpBriPvGmyASF8G80iBn27kfoVnsqYR9a5+j9K+v91AoV39Yd6uyLAv0/ 5 | XzzMf2ZeAJCyGs7r6yeeT+uzbvPx4iAyOSr6Ij9tlId7AhZ60oXN75VouKvMoaUDOgwkgv 6 | n3/Y56TXk/ZQYBSWzN8KiQDSwTFDLhleH6G3X70l4KnR4KMiqjDdytALNhE12xYgOhXGNe 7 | jSJPQcSOt942cJJFhWhZVWSjXZPC7y8OaLxp0zbM8M9FArrH5DUIoYxXUic1lmeSxl1kPf 8 | TLdyOfC5eQAAA9AT06V/E9OlfwAAAAdzc2gtcnNhAAABAQD4LhZ8tlWZ5y+AQHHYs5pBHh 9 | AwojcNd1mvJNQmjLL+qSmt7mRjr46EP2m3PD8XgBWoGkGuI+8abIBIXwbzSIGfbuR+hWey 10 | phH1rn6P0r6/3UChXf1h3q7IsC/T9fPMx/Zl4AkLIazuvrJ55P67Nu8/HiIDI5KvoiP22U 11 | h3sCFnrShc3vlWi4q8yhpQM6DCSC+ff9jnpNeT9lBgFJbM3wqJANLBMUMuGV4fobdfvSXg 12 | qdHgoyKqMN3K0As2ETXbFiA6FcY16NIk9BxI633jZwkkWFaFlVZKNdk8LvLw5ovGnTNszw 13 | z0UCusfkNQihjFdSJzWWZ5LGXWQ99Mt3I58Ll5AAAAAwEAAQAAAQEAvUdxMRZi/Oj8KmV8 14 | Lpj0GZvTHzRopmWTSefdwbTnQyBDQHsjp3+aQzSV7QEO4V53pei4lRak4lNEF24aP+vZqH 15 | 4L2I7oQrEz21uE/S3u+yfEhg3IFR+f0EHHiHH5ygHr41DVtN6vJLreJedHfANuvoW9zQUR 16 | d9BCe57GDTqQj59ZQCH26i9iD7xji8dOiVoxz+w4zt7uuavrNnzWlcTgnogiayPlV+Brov 17 | d/NifeDoR14Cf/0D9Y70pgQOodnMxFPsvKWOaKoMxqI4M/HEC1JY1jGSDWdgG2RYJ1CAkH 18 | 4gN99oWioUfI5lPmqTaXBHaEeYAyqwnyifOC5GHTzqjRnQAAAIEAyObVL8xIA4ks2jPkyM 19 | hClVbAYUEYcZ02jdxdMafPrSwEoOyPdug0ucsH2PB4uLcwio1IfRMDZAmusW7pznsm70zy 20 | uR/iJb4qPwt6jKE4WdbHA/rxRa7MoXaTpKsM2eMQARh7nT3IlDkGHxqR6ueoAERYjOw6TD 21 | 84FIiP+Bs6EPcAAACBAP0cgmulOQ5YEW6Oz6t1ekcCPw/GVfdW0oKpXHl2j25qh+jjct3H 22 | IpR/CpzTw+jEeKg8oPpOV6awDd7eMgE1LNq1cSp3N9JJdHpsZylvKArtcUxXn7OKfj2Tza 23 | yUbkpSeH4OaoxCO0i/HDMrZaNZ04Yb57iHF8beX8xTUCA92tLbAAAAgQD7AyvGFl9lhOYi 24 | msX2C17OlxDaSEfgZ53EqAuOwUgDJqh6umWg9C/fNVXOQp1Ot3m4aUHwWJ2ooTkc1UftXT 25 | gMgKh5fLVldKYOTzygoI9wLAAt1u5j/OVBtrT2Gdsm3IMnS/I11OG21WjB2oMiBInDoq8i 26 | Zw7XNz8hFc4aNimzOwAAABZtYnljemtvd3NraUBtYXRiLmxvY2FsAQID 27 | -----END OPENSSH PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /pkg/server/storage/mysql_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "database/sql" 5 | "testing" 6 | 7 | "github.com/square/sharkey/pkg/server/config" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func purge(t *testing.T, db *sql.DB) { 12 | _, err := db.Exec("DROP TABLE IF EXISTS hostkeys") 13 | require.NoError(t, err) 14 | _, err = db.Exec("DROP TABLE IF EXISTS github_user_mappings") 15 | require.NoError(t, err) 16 | _, err = db.Exec("DROP TABLE IF EXISTS goose_db_version") 17 | require.NoError(t, err) 18 | 19 | rows, err := db.Query("show tables") 20 | require.NoError(t, err) 21 | require.False(t, rows.Next(), "All tables should have been cleaned up") 22 | } 23 | 24 | // TestMysql verifies the MySQL storage interface is respected. 25 | // Because it requires a running MySQL database, you can skip it with `go test -short` 26 | // It expects a Mysql running on localhost:3306 with the username root, password 'root', and a database 27 | // named sharkey_test, as our CI environment provides. That database will have its tables dropped. 28 | // Don't run tests in prod, and don't call your prod DB sharkey_test. 29 | func TestMysql(t *testing.T) { 30 | if testing.Short() { 31 | t.Skip("Skipping tests in MySQL mode") 32 | } else { 33 | t.Log("Testing against MySQL in non-short mode: Try `go test -short` to skip this") 34 | } 35 | 36 | cfg := config.Database{ 37 | Type: "mysql", 38 | Schema: "sharkey_test", 39 | Username: "root", 40 | Password: "root", 41 | } 42 | 43 | storage, err := NewMysql(cfg) 44 | require.NoError(t, err) 45 | 46 | // Drop data (if left over from previous test runs) 47 | purge(t, storage.DB) 48 | 49 | // Run migrations. 50 | require.NoError(t, storage.Migrate("../../../db/mysql/migrations")) 51 | 52 | require.NoError(t, storage.Ping()) 53 | 54 | testStorage(t, storage) 55 | testGitHubStorage(t, storage) 56 | 57 | // Drop data after test finishes 58 | purge(t, storage.DB) 59 | } 60 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/square/sharkey 2 | 3 | require ( 4 | bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c 5 | github.com/armon/go-metrics v0.3.5 6 | github.com/bradleyfalzon/ghinstallation/v2 v2.4.0 7 | github.com/felixge/httpsnoop v1.0.4 8 | github.com/go-sql-driver/mysql v1.9.3 9 | github.com/gorilla/handlers v1.5.2 10 | github.com/gorilla/mux v1.8.1 11 | github.com/mattn/go-sqlite3 v2.0.3+incompatible 12 | github.com/robfig/cron/v3 v3.0.1 13 | github.com/shurcooL/githubv4 v0.0.0-20200627185320-e003124d66e4 14 | github.com/sirupsen/logrus v1.7.0 15 | github.com/spiffe/go-spiffe/v2 v2.5.0 16 | github.com/stretchr/testify v1.11.1 17 | golang.org/x/crypto v0.41.0 18 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 19 | gopkg.in/yaml.v2 v2.4.0 20 | ) 21 | 22 | require ( 23 | filippo.io/edwards25519 v1.1.0 // indirect 24 | github.com/DataDog/datadog-go v3.2.0+incompatible // indirect 25 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect 26 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 27 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 // indirect 28 | github.com/cloudflare/circl v1.6.1 // indirect 29 | github.com/davecgh/go-spew v1.1.1 // indirect 30 | github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 31 | github.com/google/go-github/v52 v52.0.0 // indirect 32 | github.com/google/go-querystring v1.1.0 // indirect 33 | github.com/hashicorp/go-immutable-radix v1.2.0 // indirect 34 | github.com/hashicorp/golang-lru v0.5.4 // indirect 35 | github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28 // indirect 36 | github.com/lib/pq v1.8.0 // indirect 37 | github.com/pmezard/go-difflib v1.0.0 // indirect 38 | github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect 39 | github.com/zeebo/errs v1.4.0 // indirect 40 | github.com/ziutek/mymysql v0.0.0-20160623123511-8787d5581eb6 // indirect 41 | golang.org/x/net v0.42.0 // indirect 42 | golang.org/x/oauth2 v0.27.0 // indirect 43 | golang.org/x/sys v0.35.0 // indirect 44 | gopkg.in/yaml.v3 v3.0.1 // indirect 45 | ) 46 | 47 | go 1.23.0 48 | 49 | toolchain go1.24.4 50 | -------------------------------------------------------------------------------- /pkg/server/storage/storage.go: -------------------------------------------------------------------------------- 1 | // Package storage provides a storage backend for the Sharkey server. 2 | // It provides an API for recording issuance of certificates. 3 | // By default it is backed by a SQL database, but you could swap this package out for another 4 | // one if you prefer something else -- many key-value stores would probably work. 5 | package storage 6 | 7 | import ( 8 | "database/sql" 9 | "errors" 10 | 11 | "golang.org/x/crypto/ssh" 12 | 13 | "github.com/square/sharkey/pkg/server/config" 14 | ) 15 | 16 | type Storage interface { 17 | // Record an issuance of type (host or client) to principal with pubkey string 18 | // Returns an integer ID for the record (ie, database row primary key) 19 | RecordIssuance(certType uint32, principal string, pubkey ssh.PublicKey) (uint64, error) 20 | 21 | // Query all hostkeys 22 | QueryHostkeys() (ResultIterator, error) 23 | 24 | // Takes a path to DB migration locations 25 | Migrate(string) error 26 | 27 | // Record mapping of user identities to github username 28 | RecordGitHubMapping(mapping map[string]string) error 29 | 30 | // Retrieve github username given sso identity 31 | QueryGitHubMapping(ssoIdentity string) (string, error) 32 | 33 | Ping() error 34 | Close() error 35 | } 36 | 37 | // ResultIterator works like a typed sql.Rows: Call Next() and then Get() until Next() returns false 38 | type ResultIterator interface { 39 | Next() bool 40 | Get() (principal string, key ssh.PublicKey, err error) 41 | } 42 | 43 | // Given a database configuration, return an appropriate Storage interface 44 | func FromConfig(cfg config.Database) (Storage, error) { 45 | var storage Storage 46 | var err error 47 | 48 | switch cfg.Type { 49 | case "sqlite": 50 | storage, err = NewSqlite(cfg) 51 | case "mysql": 52 | storage, err = NewMysql(cfg) 53 | default: 54 | return nil, errors.New("Unknown database type: " + cfg.Type) 55 | } 56 | 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | err = storage.Ping() 62 | if err != nil { 63 | return nil, err 64 | } 65 | return storage, nil 66 | } 67 | 68 | // This is shared between sqlite & mysql 69 | type SqlResultIterator struct { 70 | *sql.Rows 71 | } 72 | 73 | var _ ResultIterator = &SqlResultIterator{} 74 | 75 | func (r *SqlResultIterator) Next() bool { 76 | return r.Rows.Next() 77 | } 78 | 79 | func (r *SqlResultIterator) Get() (string, ssh.PublicKey, error) { 80 | var hostname string 81 | var pubkey []byte 82 | err := r.Rows.Scan(&hostname, &pubkey) 83 | if err != nil { 84 | return "", nil, err 85 | } 86 | pk, _, _, _, err := ssh.ParseAuthorizedKey(pubkey) 87 | return hostname, pk, err 88 | } 89 | -------------------------------------------------------------------------------- /pkg/server/telemetry/middleware.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "time" 7 | 8 | "github.com/felixge/httpsnoop" 9 | "github.com/gorilla/mux" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type MetricsMiddleware struct { 14 | telemetry *Telemetry 15 | } 16 | 17 | func NewMetricsMiddleware(t *Telemetry) *MetricsMiddleware { 18 | return &MetricsMiddleware{t} 19 | } 20 | 21 | func (m *MetricsMiddleware) InstrumentHTTPEndpointStats(h http.Handler) http.Handler { 22 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 23 | start := time.Now() 24 | logger, w := makeMetricsResponseLogger(w) 25 | 26 | route := mux.CurrentRoute(r) 27 | path, err := route.GetPathTemplate() 28 | if err != nil { 29 | logrus.Warn("unable to retrieve path from route for metrics") 30 | } 31 | 32 | endpoint := m.parseRoute(path) 33 | 34 | h.ServeHTTP(w, r) 35 | 36 | if endpoint != "" { 37 | m.telemetry.Metrics.IncrCounter([]string{endpoint, Count}, 1) 38 | m.telemetry.Metrics.SetGauge([]string{endpoint, Latency}, float32(time.Since(start).Milliseconds())) 39 | if logger.Status() >= 500 { 40 | m.telemetry.Metrics.IncrCounter([]string{endpoint, "500"}, 1) 41 | } else if logger.Status() >= 400 { 42 | m.telemetry.Metrics.IncrCounter([]string{endpoint, "400"}, 1) 43 | } else if logger.Status() >= 200 && logger.status < 300 { 44 | m.telemetry.Metrics.IncrCounter([]string{endpoint, "200"}, 1) 45 | } 46 | } 47 | }) 48 | } 49 | 50 | func (m *MetricsMiddleware) parseRoute(routeName string) string { 51 | switch routeName { 52 | case "/enroll/{hostname}": 53 | return "enroll_host" 54 | default: 55 | return strings.ReplaceAll(strings.Trim(routeName, "/"), "/", "_") 56 | } 57 | } 58 | 59 | // metricsResponseLogger hooks onto an http ResponseWriter and tracks the http response code of that ResponseWriter 60 | // When the ResponseWriter writes the response code into the http header, we hook into that function and record 61 | // the response code 62 | type metricsResponseLogger struct { 63 | w http.ResponseWriter 64 | status int 65 | } 66 | 67 | func (l *metricsResponseLogger) WriteHeader(code int) { 68 | l.status = code 69 | } 70 | 71 | func (l *metricsResponseLogger) Status() int { 72 | return l.status 73 | } 74 | 75 | func makeMetricsResponseLogger(w http.ResponseWriter) (*metricsResponseLogger, http.ResponseWriter) { 76 | logger := &metricsResponseLogger{w: w, status: http.StatusOK} 77 | return logger, httpsnoop.Wrap(w, httpsnoop.Hooks{ 78 | WriteHeader: func(httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc { 79 | return logger.WriteHeader 80 | }, 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /pkg/server/api/testdata/next_server_ca: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn 3 | NhAAAAAwEAAQAAAYEAqipR4PsJ0oYSZDSy4j+xewTcYtJkSGbrbsOjxGtBHOeTMpxCIVsw 4 | 5MqZR9OlnX4SZMfpqAa97p76nZvtFdIljoc5Cn+OFOGYtSr2xSxvYQG3mGyIONKBGzY/I9 5 | OnPkNOWr1SokbmWr2HH+9AwpxCUngOJIumKy9cXRAEQOmMDB8SMiRkh7gOJCwbI7Sz1Zdc 6 | dQcxWxSbS+UYWbkVBYTSwlAWHFezIW7BAlmHBEeS9U58ceDi0RwArHRVLzcvoxVx71nHB9 7 | hXEsOzNBOp2cFS02/9I8MqVGoP/wYWSRR0+ykdK2zw8Pd4SxB0Qan+E4wuB8Iwf79XeWza 8 | cgJHyhjCOXhiIUE5zuyQB6U12f9wXom9Onz9yVg0X0o+ChhKRYwMXPniB0pfEeHbMadGJv 9 | GdlPW7kmM3+nVT/HhDleAqeYVU0c05ORHTtqUjvgzsYJllw1/RruBkcENuBTQiNpN48rKU 10 | /EuC0UoArAtNKWiyLKlIsKsVnH6gUbbiNyFSptulAAAFoNPjJibT4yYmAAAAB3NzaC1yc2 11 | EAAAGBAKoqUeD7CdKGEmQ0suI/sXsE3GLSZEhm627Do8RrQRznkzKcQiFbMOTKmUfTpZ1+ 12 | EmTH6agGve6e+p2b7RXSJY6HOQp/jhThmLUq9sUsb2EBt5hsiDjSgRs2PyPTpz5DTlq9Uq 13 | JG5lq9hx/vQMKcQlJ4DiSLpisvXF0QBEDpjAwfEjIkZIe4DiQsGyO0s9WXXHUHMVsUm0vl 14 | GFm5FQWE0sJQFhxXsyFuwQJZhwRHkvVOfHHg4tEcAKx0VS83L6MVce9ZxwfYVxLDszQTqd 15 | nBUtNv/SPDKlRqD/8GFkkUdPspHSts8PD3eEsQdEGp/hOMLgfCMH+/V3ls2nICR8oYwjl4 16 | YiFBOc7skAelNdn/cF6JvTp8/clYNF9KPgoYSkWMDFz54gdKXxHh2zGnRibxnZT1u5JjN/ 17 | p1U/x4Q5XgKnmFVNHNOTkR07alI74M7GCZZcNf0a7gZHBDbgU0IjaTePKylPxLgtFKAKwL 18 | TSlosiypSLCrFZx+oFG24jchUqbbpQAAAAMBAAEAAAGAK4xREAau6NWu9z4VWZl7TkRcMl 19 | 4tk+ni7qHa03WvYDpTjWw38FlqFeNTfvJHPBr7khcnUP0ItnyxHoy9DAyP1/37NxiVv/pM 20 | HnE0XhmVF3pdBgEgi4ozyEcFuaF89446CzbQYv9KDIbcgeu04xkiUACxfeDPUdX5CUgEDq 21 | i2UpPREEwH/kO4OsGe4HBqZYsq+jgxBWIsrCuhI7UeEB+B4ICmZ/J5wWCavIM07n9yuJyx 22 | dGNdKK7F7VyIQGEbK91ctBVvuu45Bnn/xCn0gxjQ0TBQl5CMkAWOYpKRyO2HqLBziXTNOM 23 | wR+FsoVnylhCDXhOHeMxMXGbZAncNQFe/y60l11PDTMtX8y87v25Y8CBoAawg8O5Ebn3ls 24 | kPV1iqzHz7BbHZqjLWk9W1FR9RNLqjINVu+X/1RBf/QvTdFYxZnvDgLm2RDgr+bfic+PWJ 25 | YDtGE5lAmV2axrelQ+CvcTbB3fPsP+M+UXn2gn+bUi2Mj0/ga9CwfWYMCXWS6VoHKBAAAA 26 | wH2yu/a47HsodmYCTJew7RZPgNQwdan56wSw6Czkc/58VMLeFD3WbW5NOqY0uvikqXc0Ot 27 | TBFmbZQIl6xvmg91SQ0mXOfskx7SLAa2H7h0Xrpj8ouK7aq9wbz0MEibLg+SVaS3YYKsWX 28 | 9HYo9Z03Q6W0EQG9epS09VryUhifGIJbobmuw7RIU/umXb5P2+LDLy3AlgSOXZFn4YPwba 29 | FyYpJB2cGfyHojcFMd2amCt+RtXMYB2he2mSz59T9B0zrsXAAAAMEA31gAfqSGo51keHEg 30 | zTF7qhgbQa887F91Q/FYCUDlO8/Gs3u5nnNZaU+Z5ynmsgO4xeyK1CP57GkZw8tka0G13G 31 | IVwXaRv95uRmrRm3hmFb1HTX9WfNOTlcKEryB+4LiiKmRa4wVzdvxoGOV8lVAOcmDxglDr 32 | lf4gxaNUzVa5v9rpUcXQnjQHGBWD5ASLcuEl6VWeZxxLJ4vEYjj3EWRigWnR1wgt67ZppE 33 | uDHunD8QWrrPKYzB5Us4UsjD/ohj3NAAAAwQDDC8qPVsyEGylVtZLsfPne5qkqaNoLJU80 34 | cSx1r8ZbZtPIlu/s9p2vTi7EK4gDmIVJwBvq9h6nIAvWgt9SYmv8br+rNsf+y/Rs2vgUL1 35 | 9MEIcwM5NWU8wQ0072cSHIjYfsfcvMM9Aw5mJb/26+q3/o635HjjGLiVILF174ZLUPlrB9 36 | +/p2uf6r/+lZtfAZK2ShfVGLPlyEFqUy49cV9uGknGqL2HjcAHT0VluwDsQD5lGBjq3/IV 37 | O6qUCJ4bzufTkAAAAobWJ5Y3prb3dza2lAbWJ5Y3prb3dza2ktbWFjYm9va3Byby5sb2Nh 38 | bAECAw== 39 | -----END OPENSSH PRIVATE KEY----- 40 | -------------------------------------------------------------------------------- /integration-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -o pipefail 5 | set -x 6 | 7 | TMPDIR=$(mktemp -d) 8 | 9 | function die() { 10 | echo "$1" 11 | exit 1 12 | } 13 | 14 | function wait_for_container() { 15 | echo "Waiting for $1..." 16 | for i in range 0 20; do 17 | if docker ps | grep -q "$1"; then 18 | return 19 | fi 20 | sleep 1 21 | done 22 | die "timed out waiting on container $1" 23 | } 24 | 25 | function cleanup() { 26 | echo "Cleanup..." 27 | docker logs server 28 | docker logs client 29 | docker stop client -t 20 || die "failed to stop 'client'" 30 | docker stop server -t 20 || die "failed to stop 'server'" 31 | rm -r "$TMPDIR" || die "failed to remove '$TMPDIR'" 32 | } 33 | 34 | trap cleanup EXIT 35 | 36 | BUILD_CONTEXT=/build 37 | SERVER_CONFIG="${BUILD_CONTEXT}/test/integration/server_config.yaml" 38 | MIGRATION_CONFIG="${BUILD_CONTEXT}/db/sqlite" 39 | CLIENT_CONFIG="${BUILD_CONTEXT}/test/integration/client_config.yaml" 40 | 41 | echo Starting sharkey server container 42 | 43 | # Start server 44 | docker run -d --rm \ 45 | --name=server \ 46 | -v "$PWD":"$BUILD_CONTEXT" \ 47 | -e SHARKEY_CONFIG="$SERVER_CONFIG" \ 48 | -e SHARKEY_MIGRATIONS="$MIGRATION_CONFIG" \ 49 | -p 12321:8080 \ 50 | server start 51 | 52 | wait_for_container server 53 | 54 | echo Starting sharkey client container 55 | 56 | # Start client 57 | docker run -d --rm \ 58 | --name client \ 59 | -v "$PWD":"$BUILD_CONTEXT" \ 60 | -e SHARKEY_CONFIG="$CLIENT_CONFIG" \ 61 | -p 14296:22 \ 62 | --link server \ 63 | --hostname client \ 64 | client 65 | 66 | wait_for_container client 67 | 68 | # Give client some time to finish initializing 69 | sleep 5 70 | 71 | echo "Starting integration test" 72 | 73 | echo "Signing user ssh key" 74 | # Sign user ssh key 75 | # NOTE: on MacOS ensure that your curl it built with openssl (and not SecureTransport) 76 | # or you won't be able to load client cert from PEM file 77 | curl --cert $PWD/test/tls/proxy.crt --key $PWD/test/tls/proxy.key \ 78 | https://localhost:12321/enroll_user -H "X-Forwarded-User: alice" \ 79 | -d @$PWD/test/ssh/alice_rsa.pub -k \ 80 | -o $TMPDIR/alice_rsa-cert.pub -sS 81 | 82 | ssh-keygen -L -f $TMPDIR/alice_rsa-cert.pub 83 | 84 | # SSH will want cert and identity file in the same dir 85 | cp $PWD/test/ssh/alice_rsa* $TMPDIR/ 86 | chmod 600 $TMPDIR/alice_rsa 87 | 88 | 89 | # Try sshing into container 90 | if ! grep -q "127.0.0.1 client" /etc/hosts; then 91 | echo "127.0.0.1 client" | sudo tee -a /etc/hosts 92 | fi 93 | 94 | echo Attempting to ssh into client container 95 | ssh -v -p 14296 -o "BatchMode yes" -o "UserKnownHostsFile=test/integration/known_hosts" -i $TMPDIR/alice_rsa alice@client true || die "failed to connect to 'client'" 96 | ssh -v -p 14296 -o "BatchMode yes" -o "UserKnownHostsFile=test/integration/known_hosts" -i $TMPDIR/alice_rsa alice@localhost true || die "failed to connect to 'localhost'" 97 | -------------------------------------------------------------------------------- /pkg/server/storage/storage_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | "golang.org/x/crypto/ssh" 9 | ) 10 | 11 | // testStorage validates a Storage implementation. Used by other tests. 12 | func testStorage(t *testing.T, storage Storage) { 13 | testKeyA, _, _, _, err := ssh.ParseAuthorizedKey([]byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJRDfioJ5P2ieBb8sqUxDjxuZMjY5l+dEfUVzpSvv1E7 testkey")) 14 | require.NoError(t, err) 15 | testKeyB, _, _, _, err := ssh.ParseAuthorizedKey([]byte("ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHFj3LLnX2LAeYxaXfYQxsCZ8gJaIQB07Rr/OkefJGLjE+xpYb5OJ3t3q2bN9hWfw0C3NTwfaoxQ5B7nvIE7Mq4=")) 16 | require.NoError(t, err) 17 | 18 | id1, err := storage.RecordIssuance(ssh.HostCert, "theHostName", testKeyA) 19 | require.NoError(t, err) 20 | 21 | id2, err := storage.RecordIssuance(ssh.HostCert, "other", testKeyB) 22 | require.NoError(t, err) 23 | 24 | assert.NotEqual(t, id1, id2, "Expected different IDs for different hostnames") 25 | 26 | rows, err := storage.QueryHostkeys() 27 | require.NoError(t, err) 28 | require.True(t, rows.Next()) 29 | hostname, pubkey, err := rows.Get() 30 | require.NoError(t, err) 31 | require.Equal(t, "theHostName", hostname) 32 | require.Equal(t, testKeyA, pubkey) 33 | 34 | require.True(t, rows.Next()) 35 | hostname, pubkey, err = rows.Get() 36 | require.NoError(t, err) 37 | require.Equal(t, "other", hostname) 38 | require.Equal(t, testKeyB, pubkey) 39 | 40 | require.False(t, rows.Next()) 41 | } 42 | 43 | func testGitHubStorage(t *testing.T, storage Storage) { 44 | username1 := "alice" 45 | gitUsername1 := "alice_git" 46 | username2 := "bob" 47 | gitUsername2 := "bob_git" 48 | initialMapping := map[string]string{ 49 | username1: gitUsername1, 50 | username2: gitUsername2, 51 | } 52 | err := storage.RecordGitHubMapping(initialMapping) 53 | require.NoError(t, err) 54 | 55 | queriedUsername1, err := storage.QueryGitHubMapping(username1) 56 | require.NoError(t, err) 57 | require.Equal(t, queriedUsername1, gitUsername1) 58 | 59 | queriedUsername2, err := storage.QueryGitHubMapping(username2) 60 | require.NoError(t, err) 61 | require.Equal(t, queriedUsername2, gitUsername2) 62 | 63 | username3 := "carol" 64 | gitUsername3 := "carol_git" 65 | modifiedGitUsername1 := "alice_git_modified" 66 | updatedMapping := map[string]string{ 67 | username3: gitUsername3, 68 | username1: modifiedGitUsername1, 69 | } 70 | 71 | err = storage.RecordGitHubMapping(updatedMapping) 72 | require.NoError(t, err) 73 | 74 | queriedModifiedUsername1, err := storage.QueryGitHubMapping(username1) 75 | require.NoError(t, err) 76 | require.Equal(t, queriedModifiedUsername1, modifiedGitUsername1) 77 | 78 | queriedUsername3, err := storage.QueryGitHubMapping(username3) 79 | require.NoError(t, err) 80 | require.Equal(t, queriedUsername3, gitUsername3) 81 | } 82 | -------------------------------------------------------------------------------- /pkg/server/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/spiffe/go-spiffe/v2/spiffeid" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | const ( 11 | goodSpiffeConfigYaml = "testdata/goodSpiffeConfig.yaml" 12 | badSpiffeConfigYaml = "testdata/badSpiffeConfig.yaml" 13 | mixedSpiffeConfigYaml = "testdata/mixedSpiffeConfig.yaml" 14 | badSpiffeConfigPlusYaml = "testdata/badSpiffeConfigPlus.yaml" 15 | badConfigTypeYaml = "testdata/badConfigType.yaml" 16 | ) 17 | 18 | func TestSpiffeIdGoodConfigFill(t *testing.T) { 19 | conf, err := Load(goodSpiffeConfigYaml) 20 | 21 | goodAp := AuthenticatingProxy{ 22 | AllowedSpiffeIds: []spiffeid.ID{ 23 | spiffeid.RequireFromString("spiffe://proxy.com"), 24 | spiffeid.RequireFromString("spiffe://proxy2.com"), 25 | }, 26 | } 27 | goodConf := Config{AuthenticatingProxy: &goodAp} 28 | require.NoError(t, err, "Failed to load configuration file") 29 | require.Equal(t, goodConf, conf, "Authenticated Proxies do not match") 30 | } 31 | 32 | func TestSpiffeIdBadConfigFill(t *testing.T) { 33 | conf, err := Load(badSpiffeConfigYaml) 34 | expectedBadSpiffeId, _ := spiffeid.FromString("") 35 | 36 | expected := AuthenticatingProxy{AllowedSpiffeIds: []spiffeid.ID{expectedBadSpiffeId, expectedBadSpiffeId}} 37 | 38 | require.Error(t, err, "Failed to catch SPIFFE error") 39 | require.Equal(t, &expected, conf.AuthenticatingProxy, "Authenticated Proxies do not match") 40 | } 41 | 42 | func TestSpiffeIdBadConfigPlus(t *testing.T) { 43 | expectedBadSpiffeId, _ := spiffeid.FromString("") 44 | expectedAp := AuthenticatingProxy{ 45 | Hostname: "proxy.com", 46 | AllowedSpiffeIds: []spiffeid.ID{expectedBadSpiffeId, expectedBadSpiffeId}, 47 | } 48 | 49 | conf, err := Load(badSpiffeConfigPlusYaml) 50 | 51 | require.Error(t, err, "Failed to catch SPIFFE error") 52 | require.Equal(t, &expectedAp, conf.AuthenticatingProxy, "Authenticated Proxies do not match") 53 | } 54 | 55 | func TestSpiffeIdMixedConfig(t *testing.T) { 56 | expectedBadSpiffeId, _ := spiffeid.FromString("") 57 | expectedAp := AuthenticatingProxy{ 58 | AllowedSpiffeIds: []spiffeid.ID{spiffeid.RequireFromString("spiffe://proxy.com"), expectedBadSpiffeId}, 59 | } 60 | 61 | conf, err := Load(mixedSpiffeConfigYaml) 62 | 63 | require.Error(t, err, "Failed to catch SPIFFE errors") 64 | require.ErrorContains(t, err, "failed to parse: [1]", "Failed to identify the appropriately failed SPIFFE ID") 65 | require.Equal(t, &expectedAp, conf.AuthenticatingProxy, "Authenticated Proxies do not match") 66 | } 67 | 68 | func TestSpiffeIdBadTypeConfig(t *testing.T) { 69 | 70 | conf, err := Load(badConfigTypeYaml) 71 | 72 | expectedConf := Config{AuthenticatingProxy: nil} 73 | 74 | require.Error(t, err, "Failed to catch YAML errors") 75 | require.NotContains(t, err.Error(), "spiffe", "Incorrectly reported error as SPIFFE error") 76 | require.Equal(t, expectedConf.AuthenticatingProxy, conf.AuthenticatingProxy, "Authenticated Proxies do not match") 77 | } 78 | -------------------------------------------------------------------------------- /examples/server.yml: -------------------------------------------------------------------------------- 1 | # SQLite database 2 | # --- 3 | db: 4 | address: /path/to/sharkey.db 5 | type: sqlite 6 | 7 | # MySQL database 8 | # --- 9 | # db: 10 | # username: root 11 | # password: password 12 | # address: hostname:port 13 | # schema: ssh_ca 14 | # type: mysql 15 | # tls: # MySQL TLS config (optional) 16 | # ca: /path/to/mysql-ca-bundle.pem 17 | # cert: /path/to/mysql-client-cert.pem # MySQL client cert 18 | # key: /path/to/mysql-client-cert-key.pem # MySQL client cert key 19 | 20 | # Server listening address 21 | listen_addr: "0.0.0.0:8080" 22 | 23 | # TLS config for serving requests 24 | # --- 25 | tls: 26 | ca: /path/to/ca-bundle.pem 27 | cert: /path/to/server-certificate.pem 28 | key: /path/to/server-certificate-key.pem 29 | 30 | # Signing key (from ssh-keygen) 31 | signing_key: /path/to/ca-signing-key 32 | 33 | # Lifetime/validity duration for generated host certificates 34 | host_cert_duration: 168h 35 | 36 | # Lifetime/validity duration for generated user certificates 37 | user_cert_duration: 24h 38 | 39 | # Optional suffix to strip from client hostnames when generating certificates. 40 | # This is useful if all your machines have a common TLD/domain, and you want to 41 | # include an alias in the generated certificate that doesn't include that suffix. 42 | # Leave empty to disable 43 | strip_suffix: ".example.com" 44 | 45 | # Optional set of aliases for hosts. If a hostname matches an alias entry, the 46 | # listed principals will be added to its certificate. This is useful if you have 47 | # special hosts that are accessed via CNAME records. 48 | aliases: 49 | "host.example.com": 50 | - "alias1.example.com" 51 | - "alias2.example.com" 52 | 53 | # Optional set of extra entries to provide to clients when they fetch a known_hosts 54 | # file. This is useful if you have externally-managed servers in your infrastructure 55 | # that you want to tell clients about, of if you want to add CA entries to the 56 | # known_hosts file. 57 | extra_known_hosts: 58 | - "@cert-authority *.example.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBwhA8rKPESjDy4iqTlkBqUlBU2xjwtmFUHY6cutA9TYbB5H/mjxzUpnSNw/HyFWNpysjTSQtHWWBdJdJGU/0aDgFUwbduHeDFxviGVSkOxm2AYn7XJopzITZRqmAmsYXHUBa75RQb+UgIG7EpCoi8hF4ItJV+TT777j1irkXwlMmeDiJEaA+7bPNdUdGw8zRbk0CyeotYVD0griRtkXdfgnQAu+DvBwOuW/uiZaPz/rAVjt4b9fmp6pcFKI3RsBqqn5tQVhKCPVuSwqvIQ7CTVkMClYovlH1/zGe8PG1DHbM9irP98S5j3mVD9W5v3QILpsg24RIS14M8pLarlD6t root@authority" 59 | 60 | 61 | # User certs are issued to users who connect through an authenticating proxy 62 | # That user should connect with a user certificate and set the username 63 | # in a header. 64 | auth_proxy: 65 | # Hostname is validated against the incoming user certificate 66 | hostname: proxy.example.com 67 | # The HTTP header containing the username 68 | username_header: X-Forwarded-User 69 | 70 | # Optional settings related to SSH 71 | ssh: 72 | # List of extensions that should be set on the user certificate (default is no extensions) 73 | user_cert_extensions: 74 | - "permit-X11-forwarding" 75 | - "permit-agent-forwarding" 76 | - "permit-port-forwarding" 77 | - "permit-pty" 78 | - "permit-user-rc" 79 | -------------------------------------------------------------------------------- /pkg/server/cert/cert.go: -------------------------------------------------------------------------------- 1 | package cert 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/square/sharkey/pkg/server/config" 10 | "github.com/square/sharkey/pkg/server/storage" 11 | "golang.org/x/crypto/ssh" 12 | ) 13 | 14 | type Signer struct { 15 | signer ssh.Signer 16 | conf *config.Config 17 | storage storage.Storage 18 | } 19 | 20 | func NewSigner(signer ssh.Signer, conf *config.Config, storage storage.Storage) *Signer { 21 | return &Signer{ 22 | signer: signer, 23 | conf: conf, 24 | storage: storage, 25 | } 26 | } 27 | 28 | func (s *Signer) Sign(keyId string, principals []string, certType uint32, pubkey ssh.PublicKey, extensions map[string]string) (*ssh.Certificate, error) { 29 | serial, err := s.storage.RecordIssuance(certType, keyId, pubkey) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | nonce := make([]byte, 32) 35 | _, err = rand.Read(nonce) 36 | if err != nil { 37 | return nil, err 38 | } 39 | startTime := time.Now() 40 | duration, err := getDurationForCertType(s.conf, certType) 41 | if err != nil { 42 | return nil, err 43 | } 44 | endTime := startTime.Add(duration) 45 | template := ssh.Certificate{ 46 | Nonce: nonce, 47 | Key: pubkey, 48 | Serial: serial, 49 | CertType: certType, 50 | KeyId: keyId, 51 | ValidPrincipals: principals, 52 | ValidAfter: (uint64)(startTime.Unix()), 53 | ValidBefore: (uint64)(endTime.Unix()), 54 | Permissions: getPermissionsForCertType(&s.conf.SSH, certType), 55 | } 56 | 57 | if template.Extensions == nil { 58 | template.Extensions = extensions 59 | } else { 60 | for key, val := range extensions { 61 | template.Extensions[key] = val 62 | } 63 | } 64 | 65 | err = template.SignCert(rand.Reader, s.signer) 66 | if err != nil { 67 | return nil, err 68 | } 69 | return &template, nil 70 | } 71 | 72 | func (s *Signer) PublicKey() ssh.PublicKey { 73 | return s.signer.PublicKey() 74 | } 75 | 76 | func getPermissionsForCertType(cfg *config.SSH, certType uint32) (perms ssh.Permissions) { 77 | switch certType { 78 | case ssh.UserCert: 79 | if cfg != nil && len(cfg.UserCertExtensions) > 0 { 80 | perms.Extensions = make(map[string]string, len(cfg.UserCertExtensions)) 81 | for _, ext := range cfg.UserCertExtensions { 82 | perms.Extensions[ext] = "" 83 | } 84 | } 85 | } 86 | return 87 | } 88 | 89 | func getDurationForCertType(cfg *config.Config, certType uint32) (time.Duration, error) { 90 | var duration time.Duration 91 | var err error 92 | 93 | switch certType { 94 | case ssh.HostCert: 95 | duration, err = time.ParseDuration(cfg.HostCertDuration) 96 | case ssh.UserCert: 97 | duration, err = time.ParseDuration(cfg.UserCertDuration) 98 | default: 99 | err = fmt.Errorf("unknown cert type %d", certType) 100 | } 101 | 102 | return duration, err 103 | } 104 | 105 | func EncodeCert(certificate *ssh.Certificate) (string, error) { 106 | certString := base64.StdEncoding.EncodeToString(certificate.Marshal()) 107 | return fmt.Sprintf("%s-cert-v01@openssh.com %s\n", certificate.Key.Type(), certString), nil 108 | } 109 | -------------------------------------------------------------------------------- /test/tls/CertAuth.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKQIBAAKCAgEA1h/jvLcKGY8Wt6FziolUzAPbENoxRfPPwawiC2vN1mR/Riap 3 | 8+y6cKjEKuOlHNLXBY7+Iqp34T4CYBcmQobkJ1f60Uym2fFUKKhMN3k9ZqsF0rl2 4 | 5QYhtTz+uBB2MIfyl2PucAMDJuPN09WAyGbLk+hLQ8RkYrw/j2bMWFtJVKReXa1D 5 | 3I0PNGOO28Hm0pl68ZTCuHXAJftrk+aZr2es5E2b/KRO1azAu5Jh9ldHiDeLOvoO 6 | 1XsB1EIvUwLb8Gj1vTakiZaTQ70d7ToFhsGsjcEtE4GsqvzNyEDI+tnM3pmfjqyi 7 | hHqRBF6iFW5/4bTncOfDu0ghC4O3Ig5UQHCkU+0+4B11tSWtebYpon8twCU9eih1 8 | je9tWTlx2yyGMqA4WSz/TocHkGC3KaRbtaptDsSJEGjSwsWwOu2gisaWXC8VZa0Z 9 | b38JzowHiNUCOcD6Ee30RnBLRCLX+6K4McphU7vw/Q0VMaSaehzuPX8jZ+HPylLs 10 | DT7Noa3p4HnGADPxUegc07q/O8bH66+aoCVJoi0kUX2HE+hiOBU5aLU92iMQI0f1 11 | ebx0j1HugV6CG8KttZJJ3tGstes8K98KXhG2t1cjf5fUikZ5Aorw7MAL2e8Y8PDs 12 | 5a5e4UAcuWE0Q21hDutjYssCdDUX726zUas93LPpubE+Z4i3wEmnDWHHAZMCAwEA 13 | AQKCAgEAwrNsmYS2olcCCSe9sBMAECLRZ5l/hGQWXbzvmAMGwBPRxzARCcQQI7DR 14 | 703gLT1qV5uWal8ncqC3+DgUihmuDDhr0TUp4rMWG4ItC6QquNh6Cwqpmcbhj7NO 15 | yn/teGOlqxMrFJ8olow99IkG+TK/mlZ3Wb+SqFUUVoja9tzK6TQsunF6a2m4kaKV 16 | nC3MfWMh79mc1a38co0TXQEqbdyP0WjglfGx3YmgFu8cNKtYV/xplc8a/fNDzoYA 17 | EjNfWlOWX9737kQE1Gt6cuN3cvlYte0Z18rp/vCxDY4bMj4pk5+M/mbrwBrTEm4a 18 | Y/J+RfYBHSEhUYJcyeOS+lydL5s9T4kfubi1ehQW4x8Z7PlJIbLcoijg8/JvYoI3 19 | w9df8gCPyhp5ChBUi3V5A1SpvWwtLq1DJKDISYA2uvHpqzQ56CJW9ZhMWTHMka1R 20 | 7zDLmLZrrf1b89/fqy6YEG2Ug+7zeAmoZ6TaehLEVCT/bGX/vjfSc7jJPw+6Ygqx 21 | xJK60aBN2NnyKeZ4/rw+/KP4XN6Zsdz3DHjFkumDb5lJyPcSc4tci7CzNN7D9Gc2 22 | 7zxHzYGXELHyBuEHMzU0PZxlbrMVVQaX5TP8eSU6rWI06CNi4G1Kg/VNedAKj/ya 23 | jRDFYcgcIqA5NiFviw0JoElHuclxcwUVmO8xK1YpytysY/zt9UECggEBAODPofT7 24 | sChmhqFnf6S//Rm1MgMTTLm1zipkNKF5l9XBXbz8TB9oQtBrKD2X93tT65NiSJkR 25 | KwJYA/h8bZHh1nTAg/CBWcqmkn4Lbu+QQCuXBYmiq7DRlZy9B5RrfqfIT7ajATIl 26 | w+pY6zg5c3WBjrHIDvKaC8ea30FNYUa7AtqYznNVFDXTl3Cqfdo6QSiaCGY3p7Mz 27 | ByGP5iNB9Kx3DPvhWvZLgcrNb3SucMtW4BMKuv682OsND5i0+FmE80+jK0JRzk75 28 | c30jFHr7gVMov86Y+GC/5KkukJtd88FxVwPh+isSbLlf7bndd3L3DqfAfzHqAIZT 29 | yBjmbwpo4R7Wod8CggEBAPPUt2czLVRb6rREtp4fbTaiwdmxuZU+NNM15xaQJ92H 30 | 4RpJcTuJDR3nenyIEqH52ek2pmPSmD33FHNIWW0Ljin1pqm33izsI01gXWGivDrP 31 | mEurVN0EB7+ExgxsLGbI0xgrk6qJkcGMe6Rj4hsJ2/kkp1xYxtTO0AME+c03Te0g 32 | UYWSonEloZCDqsm0XEbJlIZPfyRCD2R4YHZO35CmLQMY03c7WfwP/8B4gIdilzve 33 | bIh5Fi1/J5ebtM6+hY/q4/musMlqaW6OUKPu6gg/XHRgjdq2M3KGnEn4kfTGYjLc 34 | ncnI+lp39axjNIIlEWq/rKlP6Pxoq5I7O245zjF43s0CggEBALZAXaIH8RWvmHF9 35 | QIPFiWCv/m1z1f2wIAVJ20kVFDycWGv/dY74rUHdvW/BpUZ8ED0ZrpU4z89En2Nx 36 | LDraBUv9TnlJxAt/4r8Acd0Q6pTsB/c+w0XDgzDgJibyxmkrAUSk/TE0YJG5qGea 37 | W7nlYFNMRqHytJ4LNfbwzm3S6kthGe1yCcJhWSJjdUfXdj9+MFT9xZyHP767s+zd 38 | aYfvs7z3QxDswboxtau4R4TxVxLY3iPp4Ukc+4nnHHcmS0JCm61WOJTZsgC+0bmp 39 | 73wFSwooBSQaLTviniT2k8g9JDfnmTIV68KUvFqz6kRveZgRfIxwkiBUB6H4Hgko 40 | YWjow60CggEAcx+pJHm+Wkk3zLtwucG+0AASpNVL+VIwoNSfckDIAcmpF787Tk/M 41 | OKVVaLzah02vfHRIJXOGXCNvrWqohlrhWaBZe2KL7QvlMyBflry+QMpJbtjLLbs8 42 | aUmoNK9SW4lQWeYhR3DCt+67Zgee9wM1sDGFL43e+xVWk0ZjJ2iIS4Bp3TY5qlCJ 43 | MvFnzupwcx+0F6IrNpRWLirD6LgeyG4dbIvpNC96sg9fwqFilgmPvD2tYtZBCWQy 44 | 7lsfCEpeR3AxCW9YmxC6DO/21R/1scUwwuXftDc4nUc/PZG2YCrWrIsLmP1ibxqa 45 | jTUd0qDr00oCnMJVD+/fqiB7U/TJDQSztQKCAQAnfPh8bpFMi9H96fegsfnLdVc8 46 | 6LiNikhAwVQfQnPQIPbW5Kbe/dDhK8fGjnbSOkHH2HrHIhY/5G+VKVHRJE54Bkxw 47 | yxLEVv1vyxI5iznKnFgD6UX74JCiM9xnUJFilCPHFImn3uzBr/4Yndcrfbio5Noh 48 | IMCYC0XWbKS5V88UZkTK8skRSkLhRguxJD36+4z3M2eVrc6TRhRIaQhFiQ6uxZtV 49 | 6YBS17Y4zbsXeP6Z85uAx3Nt34tysljC6J+Gy7Cjmc7eXeG7b7Y5nn1jyKxO2Nuf 50 | a6gy+DAmuW0L5ZyMMcv20XTkKzVA7o9SKZ8O07zoePlzAmh7rUGGqXxWLZfd 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /pkg/client/client_test.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2016 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package client 18 | 19 | import ( 20 | "fmt" 21 | "net/http" 22 | "net/http/httptest" 23 | "os" 24 | "testing" 25 | 26 | "github.com/sirupsen/logrus" 27 | "github.com/sirupsen/logrus/hooks/test" 28 | "github.com/stretchr/testify/assert" 29 | ) 30 | 31 | func TestEnroll(t *testing.T) { 32 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 | fmt.Fprintln(w, "Test response") 34 | })) 35 | c, err := generateClient(ts.URL) 36 | if err != nil { 37 | t.Fatalf("error generating Client: %s", err.Error()) 38 | } 39 | hook := test.NewLocal(c.logger) 40 | defer cleanup(c) 41 | defer ts.Close() 42 | c.enroll(c.conf.HostKeys[0].HostKey, c.conf.HostKeys[0].SignedCert) 43 | assert.Equal(t, 2, len(hook.Entries)) 44 | assert.Equal(t, logrus.InfoLevel, hook.Entries[0].Level) 45 | assert.Equal(t, logrus.InfoLevel, hook.Entries[1].Level) 46 | assert.Equal(t, "Installing updated SSH certificate", hook.Entries[0].Message) 47 | assert.Equal(t, "calling exec on commands", hook.Entries[1].Message) 48 | assert.Contains(t, hook.Entries[0].Data, "signedCert") 49 | assert.Contains(t, hook.Entries[1].Data, "commands") 50 | data, err := os.ReadFile(c.conf.HostKeys[0].SignedCert) 51 | if err != nil { 52 | t.Fatalf("error reading signed cert: %s", err.Error()) 53 | } 54 | if string(data) != "Test response\n" { 55 | t.Fatalf("signed cert contains wrong info: %s", string(data)) 56 | } 57 | } 58 | 59 | func TestKnownHosts(t *testing.T) { 60 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 61 | fmt.Fprintln(w, "Test response") 62 | })) 63 | c, err := generateClient(ts.URL) 64 | if err != nil { 65 | t.Fatalf("error generating Client: %s", err.Error()) 66 | } 67 | hook := test.NewLocal(c.logger) 68 | defer cleanup(c) 69 | defer ts.Close() 70 | c.makeKnownHosts() 71 | data, err := os.ReadFile(c.conf.KnownHosts) 72 | if err != nil { 73 | t.Fatalf("error reading signed cert: %s", err.Error()) 74 | } 75 | if string(data) != "Test response\n" { 76 | t.Fatalf("signed cert contains wrong info: %s", string(data)) 77 | } 78 | assert.Equal(t, 2, len(hook.Entries)) 79 | assert.Equal(t, logrus.InfoLevel, hook.Entries[0].Level) 80 | assert.Equal(t, logrus.InfoLevel, hook.Entries[1].Level) 81 | assert.Equal(t, "Installing known_hosts file", hook.Entries[0].Message) 82 | assert.Equal(t, "calling exec on commands", hook.Entries[1].Message) 83 | assert.Contains(t, hook.Entries[0].Data, "KnownHosts") 84 | assert.Contains(t, hook.Entries[1].Data, "commands") 85 | } 86 | 87 | func generateClient(url string) (*Client, error) { 88 | signedCertTmp, err := os.CreateTemp("", "sharkey-test") 89 | if err != nil { 90 | panic(err) 91 | } 92 | 93 | knownHostsTmp, err := os.CreateTemp("", "sharkey-test") 94 | if err != nil { 95 | panic(err) 96 | } 97 | 98 | conf := Config{ 99 | RequestAddr: url, 100 | HostKeys: []hostKey{ 101 | {"testdata/ssh_host_rsa_key.pub", signedCertTmp.Name()}, 102 | }, 103 | KnownHosts: knownHostsTmp.Name(), 104 | } 105 | 106 | logger, _ := test.NewNullLogger() 107 | 108 | return &Client{ 109 | conf: &conf, 110 | client: &http.Client{}, 111 | logger: logger, 112 | }, nil 113 | } 114 | 115 | func cleanup(c *Client) { 116 | os.Remove(c.conf.SignedCert) 117 | os.Remove(c.conf.KnownHosts) 118 | } 119 | -------------------------------------------------------------------------------- /pkg/server/storage/sqlite.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strings" 7 | 8 | "bitbucket.org/liamstask/goose/lib/goose" 9 | _ "github.com/mattn/go-sqlite3" 10 | "github.com/square/sharkey/pkg/server/config" 11 | "golang.org/x/crypto/ssh" 12 | ) 13 | 14 | // SqliteStorage implements the storage interface, using Sqlite for storage. 15 | type SqliteStorage struct { 16 | *sql.DB 17 | } 18 | 19 | const ( 20 | // following https://godoc.org/golang.org/x/crypto/ssh#pkg-constants conventions 21 | sqliteHostCert = 2 22 | sqliteUserCert = 1 23 | ) 24 | 25 | var _ Storage = &SqliteStorage{} 26 | 27 | func (s *SqliteStorage) RecordIssuance(certType uint32, principal string, pubkey ssh.PublicKey) (uint64, error) { 28 | pkdata := ssh.MarshalAuthorizedKey(pubkey) 29 | 30 | typ, err := certTypeToSQLite(certType) 31 | if err != nil { 32 | return 0, err 33 | } 34 | 35 | result, err := s.DB.Exec( 36 | "INSERT OR REPLACE INTO hostkeys (hostname, pubkey, cert_type) VALUES (?, ?, ?)", 37 | principal, pkdata, typ) 38 | if err != nil { 39 | return 0, fmt.Errorf("error recording issuance: %s", err.Error()) 40 | } 41 | 42 | id, err := result.LastInsertId() 43 | return uint64(id), err 44 | } 45 | 46 | func (s *SqliteStorage) QueryHostkeys() (ResultIterator, error) { 47 | rows, err := s.DB.Query("select hostname, pubkey from hostkeys") 48 | if err != nil { 49 | return &SqlResultIterator{}, err 50 | } 51 | return &SqlResultIterator{Rows: rows}, nil 52 | } 53 | 54 | func (s *SqliteStorage) RecordGitHubMapping(mapping map[string]string) error { 55 | // Prepare for batch insert 56 | insertEntries := make([]string, 0, len(mapping)) 57 | insertValues := make([]interface{}, 0, len(mapping)*2) 58 | deleteEntries := make([]string, 0, len(mapping)) 59 | deleteValues := make([]interface{}, 0, len(mapping)) 60 | for ssoIdentity, githubUser := range mapping { 61 | // Create one set of values for each mapping 62 | insertEntries = append(insertEntries, "(?, ?)") 63 | // Append matching values for mapping 64 | insertValues = append(insertValues, ssoIdentity) 65 | insertValues = append(insertValues, githubUser) 66 | 67 | deleteEntries = append(deleteEntries, "?") 68 | deleteValues = append(deleteValues, ssoIdentity) 69 | } 70 | 71 | // Delete if not found in GitHub results 72 | deleteStmt := fmt.Sprintf( 73 | "DELETE FROM github_user_mappings WHERE sso_identity NOT IN (%s)", 74 | strings.Join(deleteEntries, ",")) 75 | _, err := s.DB.Exec(deleteStmt, deleteValues...) 76 | if err != nil { 77 | return fmt.Errorf("error deleting mappings: %s", err.Error()) 78 | } 79 | 80 | // Create query with necessary number of (?, ?) values 81 | stmt := fmt.Sprintf( 82 | "INSERT OR REPLACE INTO github_user_mappings (sso_identity, github_username) VALUES %s", 83 | strings.Join(insertEntries, ",")) 84 | // Execute with blown up values that match into the (?, ?) blocks inserted into the statement 85 | if _, err := s.DB.Exec(stmt, insertValues...); err != nil { 86 | return fmt.Errorf("error recording mapping: %s", err.Error()) 87 | } 88 | 89 | return nil 90 | } 91 | 92 | func (s *SqliteStorage) QueryGitHubMapping(ssoIdentity string) (string, error) { 93 | row := s.DB.QueryRow("SELECT github_username FROM github_user_mappings WHERE sso_identity = ?", ssoIdentity) 94 | var githubUser string 95 | if err := row.Scan(&githubUser); err != nil { 96 | return "", err 97 | } 98 | 99 | return githubUser, nil 100 | } 101 | 102 | func (s *SqliteStorage) Migrate(migrationsDir string) error { 103 | gooseConf := goose.DBConf{ 104 | MigrationsDir: migrationsDir, 105 | Env: "sharkey", 106 | Driver: goose.DBDriver{ 107 | Name: "sqlite", 108 | Import: "github.com/go-sql-driver/mysql", 109 | Dialect: goose.Sqlite3Dialect{}, 110 | }, 111 | } 112 | 113 | desiredVersion, err := goose.GetMostRecentDBVersion(migrationsDir) 114 | if err != nil { 115 | return fmt.Errorf("unable to run migrations: %s", err) 116 | } 117 | 118 | err = goose.RunMigrationsOnDb(&gooseConf, migrationsDir, desiredVersion, s.DB) 119 | if err != nil { 120 | return fmt.Errorf("unable to run migrations: %s", err) 121 | } 122 | 123 | return nil 124 | } 125 | 126 | func NewSqlite(cfg config.Database) (*SqliteStorage, error) { 127 | db, err := sql.Open("sqlite3", cfg.Address) 128 | 129 | return &SqliteStorage{DB: db}, err 130 | } 131 | 132 | // certTypeToSQLite converts the certType uint32 into a valid SQLite value 133 | // or returns error 134 | func certTypeToSQLite(certType uint32) (int, error) { 135 | switch certType { 136 | case ssh.HostCert: 137 | return sqliteHostCert, nil 138 | case ssh.UserCert: 139 | return sqliteUserCert, nil 140 | default: 141 | return -1, fmt.Errorf("storage: unknown ssh cert type: %d", certType) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /rpm/sharkey.spec: -------------------------------------------------------------------------------- 1 | %global rev %(git rev-parse HEAD) 2 | %global shortrev %(r=%{rev}; echo ${r:0:12}) 3 | %global _dwz_low_mem_die_limit 0 4 | %define function gobuild { go build -a -ldflags "-B 0x$(head -c20 /dev/urandom|od -An -tx1|tr -d ' \n')" -v -x "$@"; } 5 | 6 | Name: sharkey 7 | Version: 0 8 | Release: 0.1.git%{shortrev}%{?dist} 9 | License: ASL 2.0 10 | Summary: Sharkey is a service for managing certificates for use by OpenSSH 11 | Url: https://github.com/square/sharkey 12 | Source0: https://github.com/square/%{name}/archive/%{rev}.tar.gz#/%{name}-%{rev}.tar.gz 13 | Requires: openssh 14 | Requires(pre): shadow-utils 15 | 16 | # e.g. el6 has ppc64 arch without gcc-go, so EA tag is required 17 | ExclusiveArch: %{?go_arches:%{go_arches}}%{!?go_arches:%{ix86} x86_64 %{arm}} 18 | # If go_compiler is not set to 1, there is no virtual provide. Use golang instead. 19 | BuildRequires: %{?go_compiler:compiler(go-compiler)}%{!?go_compiler:golang} 20 | 21 | %description 22 | Sharkey is a service for managing certificates for use by OpenSSH. 23 | 24 | %package server 25 | Summary: Sharkey is a service for managing certificates for use by OpenSSH. 26 | Version: %{version} 27 | Group: System Environment/Daemons 28 | %description server 29 | Sharkey-server is the server component to the Sharkey service for managing certificates for use by OpenSSH 30 | 31 | %package client 32 | Summary: Sharkey is a service for managing certificates for use by OpenSSH. 33 | Version: %{version} 34 | Group: System Environment/Daemons 35 | %description client 36 | Sharkey-client is the client component to the Sharkey service for managing certificates for use by OpenSSH 37 | 38 | %prep 39 | %setup -q -n %{name}-%{rev} 40 | 41 | %build 42 | mkdir -p src/github.com/square 43 | ln -s ../../../ src/github.com/square/sharkey 44 | 45 | %install 46 | export GOPATH=$(pwd):%{gopath} 47 | # Server 48 | %gobuild -o %{buildroot}%{_sbindir}/%{name}-server github.com/square/sharkey/server 49 | 50 | install -d %{buildroot}%{_unitdir} 51 | install -m 0644 rpm/sharkey-server.service %{buildroot}%{_unitdir}/%{name}-server.service 52 | install -d %{buildroot}/%{_sysconfdir}/sysconfig 53 | install -m 0644 rpm/sharkey-server.sysconfig %{buildroot}/%{_sysconfdir}/sysconfig/%{name}-server 54 | install -d %{buildroot}%{_sysconfdir}/sharkey 55 | install -m 0644 examples/server.yml %{buildroot}%{_sysconfdir}/%{name}/server.yml.example 56 | cp -r db %{buildroot}%{_sysconfdir}/sharkey/ 57 | 58 | # Client 59 | %gobuild -o %{buildroot}%{_sbindir}/%{name}-client github.com/square/sharkey/client 60 | install -m 0644 rpm/sharkey-client.service %{buildroot}%{_unitdir}/%{name}-client.service 61 | install -m 0644 rpm/sharkey-client.sysconfig %{buildroot}/%{_sysconfdir}/sysconfig/%{name}-client 62 | install -m 0644 examples/client.yml %{buildroot}%{_sysconfdir}/%{name}/client.yml.example 63 | 64 | %pre server 65 | getent group sharkey >/dev/null || groupadd -r sharkey 66 | getent passwd sharkey >/dev/null || \ 67 | useradd --system --gid sharkey --shell /sbin/nologin --home-dir %{_sysconfdir}/%{name} \ 68 | --comment "Sharkey server user" sharkey 69 | exit 0 70 | 71 | %pre client 72 | getent group sharkey-client >/dev/null || groupadd -r sharkey-client 73 | getent passwd sharkey-client >/dev/null || \ 74 | useradd --system --gid sharkey-client --shell /sbin/nologin --home-dir %{_sysconfdir}/%{name} \ 75 | --comment "Sharkey client user" sharkey-client 76 | exit 0 77 | 78 | %post server 79 | /usr/bin/systemctl daemon-reload >/dev/null 2>&1 80 | 81 | %post client 82 | /usr/bin/systemctl daemon-reload >/dev/null 2>&1 83 | 84 | %preun server 85 | if [ $1 -eq 0 ] ; then 86 | /usr/bin/systemctl stop %{name}-server >/dev/null 2>&1 87 | /usr/bin/systemctl disable %{name}-server >/dev/null 2>&1 88 | fi 89 | 90 | %preun client 91 | if [ $1 -eq 0 ] ; then 92 | /usr/bin/systemctl stop %{name}-client >/dev/null 2>&1 93 | /usr/bin/systemctl disable %{name}-client >/dev/null 2>&1 94 | fi 95 | 96 | %postun server 97 | if [ "$1" -ge "1" ] ; then 98 | /usr/bin/systemctl try-restart %{name}-server >/dev/null 2>&1 || : 99 | fi 100 | 101 | %postun client 102 | if [ "$1" -ge "1" ] ; then 103 | /usr/bin/systemctl try-restart %{name}-client >/dev/null 2>&1 || : 104 | fi 105 | 106 | %clean 107 | rm -rf %{buildroot} 108 | 109 | %files server 110 | %defattr(-,root,root,-) 111 | %{_sbindir}/%{name}-server 112 | %{_unitdir}/%{name}-server.service 113 | %config %{_sysconfdir}/sysconfig/%{name}-server 114 | %{_sysconfdir}/%{name}/server.yml.example 115 | %{_sysconfdir}/%{name}/db/mysql/migrations/* 116 | %{_sysconfdir}/%{name}/db/sqlite/migrations/* 117 | 118 | %files client 119 | %defattr(-,root,root,-) 120 | %{_sbindir}/%{name}-client 121 | %{_unitdir}/%{name}-client.service 122 | %config %{_sysconfdir}/sysconfig/%{name}-client 123 | %{_sysconfdir}/%{name}/client.yml.example 124 | 125 | %changelog 126 | * Tue Aug 02 2016 Ben Allen - 0-0.1.gita6ec80f356d3 127 | - Initial RPM release 128 | 129 | -------------------------------------------------------------------------------- /pkg/server/api/sharkey.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2016 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package api 18 | 19 | import ( 20 | "encoding/json" 21 | "net/http" 22 | "os" 23 | "time" 24 | 25 | "github.com/square/sharkey/pkg/server/cert" 26 | 27 | _ "bitbucket.org/liamstask/goose/lib/goose" 28 | "github.com/gorilla/handlers" 29 | "github.com/gorilla/mux" 30 | _ "github.com/mattn/go-sqlite3" 31 | "github.com/shurcooL/githubv4" 32 | "github.com/sirupsen/logrus" 33 | "github.com/square/sharkey/pkg/server/config" 34 | "github.com/square/sharkey/pkg/server/storage" 35 | "github.com/square/sharkey/pkg/server/telemetry" 36 | "golang.org/x/crypto/ssh" 37 | ) 38 | 39 | type statusResponse struct { 40 | Ok bool `json:"ok"` 41 | Status string `json:"status"` 42 | Messages []string `json:"messages"` 43 | } 44 | 45 | type Api struct { 46 | signer *cert.Signer 47 | storage storage.Storage 48 | conf *config.Config 49 | logger *logrus.Logger 50 | telemetry *telemetry.Telemetry 51 | gitHubClient *githubv4.Client 52 | } 53 | 54 | func Run(conf *config.Config, logger *logrus.Logger) { 55 | logger.Print("Starting http server") 56 | privateKey, err := os.ReadFile(conf.SigningKey) 57 | if err != nil { 58 | logger.WithError(err).Fatal("unable to read signing key file") 59 | } 60 | 61 | storage, err := storage.FromConfig(conf.Database) 62 | if err != nil { 63 | logger.WithError(err).Fatal("unable to setup database") 64 | } 65 | defer storage.Close() 66 | 67 | sshSigner, err := ssh.ParsePrivateKey(privateKey) 68 | if err != nil { 69 | logger.WithError(err).Fatal("unable to parse signing key data") 70 | } 71 | 72 | signer := cert.NewSigner(sshSigner, conf, storage) 73 | 74 | c := Api{ 75 | conf: conf, 76 | signer: signer, 77 | storage: storage, 78 | logger: logger, 79 | } 80 | 81 | if c.conf.Telemetry.Address == "" { 82 | logger.Warn("Telemetry address not found, using blackhole metrics sink") 83 | } 84 | telemetryImpl, err := telemetry.CreateTelemetry(c.conf.Telemetry.Address) 85 | if err != nil { 86 | logger.WithError(err).Fatal("unable to setup telemetry") 87 | } 88 | c.telemetry = telemetryImpl 89 | 90 | metricsMiddlware := telemetry.NewMetricsMiddleware(c.telemetry) 91 | 92 | handler := mux.NewRouter() 93 | handler.Use(metricsMiddlware.InstrumentHTTPEndpointStats) 94 | handler.Path("/enroll/{hostname}").Methods("POST").HandlerFunc(c.Enroll) 95 | handler.Path("/enroll_user").Methods("POST").HandlerFunc(c.EnrollUser) 96 | handler.Path("/known_hosts").Methods("GET").HandlerFunc(c.KnownHosts) 97 | handler.Path("/authority").Methods("GET").HandlerFunc(c.Authority) 98 | handler.Path("/_status").Methods("HEAD", "GET").HandlerFunc(c.Status) 99 | loggingHandler := handlers.LoggingHandler(logger.Writer(), handler) 100 | tlsConfig, err := config.BuildTLS(conf.TLS) 101 | if err != nil { 102 | logger.WithError(err).Fatal("issue with BuildTLS") 103 | } 104 | server := &http.Server{ 105 | Addr: conf.ListenAddr, 106 | TLSConfig: tlsConfig, 107 | Handler: loggingHandler, 108 | IdleTimeout: time.Minute * 5, 109 | } 110 | 111 | if c.conf.GitHub.SyncEnabled { 112 | c.gitHubClient = c.CreateGitHubClient() 113 | if err := c.StartGitHubUserMappingSyncJob(); err != nil { 114 | logger.WithError(err) 115 | } 116 | } 117 | 118 | err = server.ListenAndServeTLS(conf.TLS.Cert, conf.TLS.Key) 119 | logger.WithError(err).Fatal("issue with ListenAndServeTLS") 120 | } 121 | 122 | func Migrate(migrationsDir string, conf *config.Config, logger *logrus.Logger) { 123 | db, err := storage.FromConfig(conf.Database) 124 | if err != nil { 125 | logger.WithError(err).Fatal("unable to setup database") 126 | } 127 | defer db.Close() 128 | 129 | if err := db.Migrate(migrationsDir); err != nil { 130 | logger.WithError(err).Fatal("error migrating database") 131 | } 132 | } 133 | 134 | func (c *Api) Status(w http.ResponseWriter, r *http.Request) { 135 | resp := statusResponse{ 136 | Ok: true, 137 | Status: "ok", 138 | Messages: []string{}, 139 | } 140 | err := c.storage.Ping() 141 | if err != nil { 142 | resp.Ok = false 143 | resp.Status = "critical" 144 | resp.Messages = append(resp.Messages, err.Error()) 145 | } 146 | out, err := json.Marshal(resp) 147 | if err != nil { 148 | panic(err) 149 | } 150 | w.Header().Set("Content-Type", "application/json") 151 | if !resp.Ok { 152 | w.WriteHeader(http.StatusServiceUnavailable) 153 | } 154 | _, _ = w.Write(out) 155 | } 156 | -------------------------------------------------------------------------------- /pkg/server/api/github.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/square/sharkey/pkg/server/telemetry" 10 | 11 | "github.com/bradleyfalzon/ghinstallation/v2" 12 | "github.com/robfig/cron/v3" 13 | "github.com/shurcooL/githubv4" 14 | ) 15 | 16 | const ( 17 | pageLength = 100 18 | ) 19 | 20 | func (c *Api) CreateGitHubClient() *githubv4.Client { 21 | tr := http.DefaultTransport 22 | 23 | // We use a private key generated by GitHub along with the corresponding App ID and Installation ID 24 | // to authenticate to GitHub 25 | // https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/ 26 | // Ghinstallation creates a transport which we give to the githubv4 client for auth 27 | itr, err := ghinstallation.NewKeyFromFile( 28 | tr, c.conf.GitHub.AppId, c.conf.GitHub.InstallationId, c.conf.GitHub.PrivateKeyPath) 29 | if err != nil { 30 | c.logger.Fatalf("could not read github private key: %s", err) 31 | } 32 | 33 | return githubv4.NewClient(&http.Client{Transport: itr}) 34 | } 35 | 36 | func (c *Api) fetchUserMappings() (map[string]string, error) { 37 | fetchStart := time.Now() 38 | // Original GraphQL query to retrieve mapping of SAML Identity to GitHub Username 39 | // query { 40 | // organization(login: organization) { 41 | // samlIdentityProvider { 42 | // externalIdentities(first: 100) { 43 | // edges { 44 | // node { 45 | // guid 46 | // samlIdentity { 47 | // nameId 48 | // } 49 | // user { 50 | // login 51 | // } 52 | // } 53 | // } 54 | // } 55 | // } 56 | // } 57 | // } 58 | var query struct { 59 | Organization struct { 60 | SamlIdentityProvider struct { 61 | ExternalIdentities struct { 62 | Edges []struct { 63 | Node struct { 64 | Guid githubv4.ID 65 | SamlIdentity struct { 66 | NameId githubv4.String 67 | } 68 | User struct { 69 | Login githubv4.String 70 | } 71 | } 72 | } 73 | PageInfo struct { 74 | HasNextPage githubv4.Boolean 75 | EndCursor githubv4.String 76 | } 77 | } `graphql:"externalIdentities(first:$pageLength, after:$cursor)"` 78 | } 79 | } `graphql:"organization(login:$organization)"` 80 | } 81 | // Variables for the above query 82 | // Replaces the variables in the graphql extensions above 83 | // $pageLength corresponds to pageLength below 84 | variables := map[string]interface{}{ 85 | "organization": githubv4.String(c.conf.GitHub.OrganizationName), 86 | "cursor": (*githubv4.String)(nil), 87 | "pageLength": githubv4.Int(pageLength), 88 | } 89 | 90 | mapping := map[string]string{} 91 | for { 92 | if err := c.gitHubClient.Query(context.Background(), &query, variables); err != nil { 93 | return nil, err 94 | } 95 | 96 | for _, edge := range query.Organization.SamlIdentityProvider.ExternalIdentities.Edges { 97 | gitLogin := string(edge.Node.User.Login) 98 | ssoLogin := string(edge.Node.SamlIdentity.NameId) 99 | if gitLogin != "" && ssoLogin != "" { 100 | mapping[ssoLogin] = gitLogin 101 | } 102 | } 103 | 104 | pageInfo := query.Organization.SamlIdentityProvider.ExternalIdentities.PageInfo 105 | if !pageInfo.HasNextPage { 106 | break 107 | } 108 | variables["cursor"] = pageInfo.EndCursor 109 | } 110 | 111 | c.telemetry.Metrics.IncrCounter([]string{telemetry.GitHub, telemetry.Fetch, telemetry.Calls}, 1) 112 | c.telemetry.Metrics.SetGauge( 113 | []string{telemetry.GitHub, telemetry.Fetch, telemetry.Latency}, float32(time.Since(fetchStart).Milliseconds())) 114 | c.telemetry.Metrics.SetGauge([]string{telemetry.GitHub, telemetry.Fetch, telemetry.Count}, float32(len(mapping))) 115 | return mapping, nil 116 | } 117 | 118 | func (c *Api) updateUserMappings() { 119 | fetchStart := time.Now() 120 | mapping, err := c.fetchUserMappings() 121 | if err != nil { 122 | c.logger.Errorf("unable to retrieve github mapping: %s", err) 123 | return 124 | } 125 | 126 | if err := c.storage.RecordGitHubMapping(mapping); err != nil { 127 | c.logger.Errorf("unable to record github mapping: %s", err) 128 | } else { 129 | c.telemetry.Metrics.IncrCounter([]string{telemetry.GitHub, telemetry.SyncJob, telemetry.Success}, 1) 130 | } 131 | c.telemetry.Metrics.SetGauge( 132 | []string{telemetry.GitHub, telemetry.SyncJob, telemetry.Latency}, 133 | float32(time.Since(fetchStart).Milliseconds())) 134 | } 135 | 136 | func (c *Api) RetrieveGitHubUsername(ssoIdentity string) (string, error) { 137 | user, err := c.storage.QueryGitHubMapping(ssoIdentity) 138 | 139 | if err != nil { 140 | return "", fmt.Errorf("unable to find user %s in github mapping: %s", ssoIdentity, err) 141 | } 142 | 143 | return user, nil 144 | } 145 | 146 | func (c *Api) StartGitHubUserMappingSyncJob() error { 147 | c.logger.Printf("Starting GitHubUserMappingSyncJob to run every %s", c.conf.GitHub.SyncInterval) 148 | // The cron doesn't initially run, so we run it first before kick off the cron 149 | c.updateUserMappings() 150 | job := cron.New() 151 | if _, err := job.AddFunc(fmt.Sprintf("@every %s", c.conf.GitHub.SyncInterval), c.updateUserMappings); err != nil { 152 | return err 153 | } 154 | job.Start() 155 | 156 | return nil 157 | } 158 | -------------------------------------------------------------------------------- /pkg/server/storage/mysql.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "strings" 7 | 8 | "golang.org/x/crypto/ssh" 9 | 10 | "bitbucket.org/liamstask/goose/lib/goose" 11 | "github.com/go-sql-driver/mysql" 12 | "github.com/square/sharkey/pkg/server/config" 13 | ) 14 | 15 | // MysqlStorage implements the storage interface, using Mysql for storage. 16 | type MysqlStorage struct { 17 | *sql.DB 18 | } 19 | 20 | const ( 21 | mysqlHostCert = "host_cert" 22 | mysqlUserCert = "user_cert" 23 | ) 24 | 25 | var _ Storage = &MysqlStorage{} 26 | 27 | func (my *MysqlStorage) RecordIssuance(certType uint32, principal string, pubkey ssh.PublicKey) (uint64, error) { 28 | pkdata := ssh.MarshalAuthorizedKey(pubkey) 29 | 30 | typ, err := certTypeToMySQL(certType) 31 | if err != nil { 32 | return 0, err 33 | } 34 | 35 | result, err := my.DB.Exec( 36 | "INSERT INTO hostkeys (hostname, pubkey, cert_type) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE pubkey = ?", 37 | principal, pkdata, typ, pkdata) 38 | if err != nil { 39 | return 0, fmt.Errorf("error recording issuance: %s", err.Error()) 40 | 41 | } 42 | 43 | // TODO: This is broken! It doesn't work in the ON DUPLICATE KEY case. 44 | // Tracked in https://github.com/square/sharkey/issues/80 45 | id, err := result.LastInsertId() 46 | return uint64(id), err 47 | } 48 | 49 | func (my *MysqlStorage) QueryHostkeys() (ResultIterator, error) { 50 | rows, err := my.DB.Query("SELECT hostname, pubkey FROM hostkeys WHERE cert_type = ?", 51 | mysqlHostCert) 52 | if err != nil { 53 | return &SqlResultIterator{}, err 54 | } 55 | return &SqlResultIterator{Rows: rows}, nil 56 | } 57 | 58 | func (my *MysqlStorage) RecordGitHubMapping(mapping map[string]string) error { 59 | // Prepare for batch insert 60 | insertEntries := make([]string, 0, len(mapping)) 61 | insertValues := make([]interface{}, 0, len(mapping)*2) 62 | deleteEntries := make([]string, 0, len(mapping)) 63 | deleteValues := make([]interface{}, 0, len(mapping)) 64 | for ssoIdentity, githubUser := range mapping { 65 | // Create one set of values for each mapping 66 | insertEntries = append(insertEntries, "(?, ?)") 67 | // Append matching values for mapping 68 | insertValues = append(insertValues, ssoIdentity) 69 | insertValues = append(insertValues, githubUser) 70 | 71 | deleteEntries = append(deleteEntries, "?") 72 | deleteValues = append(deleteValues, ssoIdentity) 73 | } 74 | 75 | // Delete if not found in GitHub results 76 | deleteStmt := fmt.Sprintf( 77 | "DELETE FROM github_user_mappings WHERE sso_identity NOT IN (%s)", 78 | strings.Join(deleteEntries, ",")) 79 | _, err := my.DB.Exec(deleteStmt, deleteValues...) 80 | if err != nil { 81 | return fmt.Errorf("error deleting mappings: %s", err.Error()) 82 | } 83 | 84 | insertStmt := fmt.Sprintf( 85 | "REPLACE INTO github_user_mappings (sso_identity, github_username) VALUES %s", 86 | strings.Join(insertEntries, ",")) 87 | // Execute with blown up values that match into the (?, ?) blocks inserted into the statement 88 | if _, err := my.DB.Exec(insertStmt, insertValues...); err != nil { 89 | return fmt.Errorf("error recording mapping: %s", err.Error()) 90 | } 91 | 92 | return nil 93 | } 94 | 95 | func (my *MysqlStorage) QueryGitHubMapping(ssoIdentity string) (string, error) { 96 | row := my.DB.QueryRow("SELECT github_username FROM github_user_mappings WHERE sso_identity = ?", ssoIdentity) 97 | var githubUser string 98 | if err := row.Scan(&githubUser); err != nil { 99 | return "", err 100 | } 101 | 102 | return githubUser, nil 103 | } 104 | 105 | // Migrate runs any pending migrations 106 | func (my *MysqlStorage) Migrate(migrationsDir string) error { 107 | gooseConf := goose.DBConf{ 108 | MigrationsDir: migrationsDir, 109 | Env: "sharkey", 110 | Driver: goose.DBDriver{ 111 | Name: "mysql", 112 | Import: "github.com/go-sql-driver/mysql", 113 | Dialect: goose.MySqlDialect{}, 114 | }, 115 | } 116 | 117 | desiredVersion, err := goose.GetMostRecentDBVersion(migrationsDir) 118 | if err != nil { 119 | return fmt.Errorf("unable to run migrations: %s", err) 120 | } 121 | 122 | err = goose.RunMigrationsOnDb(&gooseConf, migrationsDir, desiredVersion, my.DB) 123 | if err != nil { 124 | return fmt.Errorf("unable to run migrations: %s", err) 125 | } 126 | 127 | return nil 128 | } 129 | 130 | func NewMysql(cfg config.Database) (*MysqlStorage, error) { 131 | url := cfg.Username 132 | if cfg.Password != "" { 133 | url += ":" + cfg.Password 134 | } 135 | url += "@tcp(" + cfg.Address + ")/" + cfg.Schema 136 | 137 | // Setup TLS (if configured) 138 | if cfg.TLS != nil { 139 | tlsConfig, err := config.BuildTLS(*cfg.TLS) 140 | if err != nil { 141 | return nil, err 142 | } 143 | err = mysql.RegisterTLSConfig("sharkey", tlsConfig) 144 | if err != nil { 145 | return nil, err 146 | } 147 | url += "?tls=sharkey" 148 | } 149 | 150 | db, err := sql.Open("mysql", url) 151 | return &MysqlStorage{DB: db}, err 152 | } 153 | 154 | // certTypeToMySQL converts the certType uint32 into a valid MySQL enum 155 | // or returns error 156 | func certTypeToMySQL(certType uint32) (string, error) { 157 | switch certType { 158 | case ssh.HostCert: 159 | return mysqlHostCert, nil 160 | case ssh.UserCert: 161 | return mysqlUserCert, nil 162 | default: 163 | return "", fmt.Errorf("storage: unknown ssh cert type: %d", certType) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /pkg/server/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "fmt" 7 | "os" 8 | "time" 9 | 10 | "github.com/spiffe/go-spiffe/v2/spiffeid" 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | func Load(file string) (conf Config, err error) { 15 | data, err := os.ReadFile(file) 16 | if err != nil { 17 | return 18 | } 19 | 20 | if err = yaml.Unmarshal(data, &conf); err != nil { 21 | // Ensure we can and should inspect the AuthenticatingProxy member 22 | if conf.AuthenticatingProxy != nil && len(conf.AuthenticatingProxy.AllowedSpiffeIds) > 0 { 23 | 24 | // Make sure the spiffe IDs we wanted were valid 25 | spiffeErr := conf.AuthenticatingProxy.validateSpiffeIds() 26 | 27 | // Report the invalid ones if they are invalid 28 | if spiffeErr != nil { 29 | err = fmt.Errorf("spiffe error: %w: %v", spiffeErr, err) 30 | } 31 | } 32 | return 33 | } 34 | 35 | return 36 | } 37 | 38 | type Config struct { 39 | Database Database `yaml:"db"` 40 | TLS TLS `yaml:"tls"` 41 | SigningKey string `yaml:"signing_key"` 42 | HostCertDuration string `yaml:"host_cert_duration"` 43 | UserCertDuration string `yaml:"user_cert_duration"` 44 | ListenAddr string `yaml:"listen_addr"` 45 | StripSuffix string `yaml:"strip_suffix"` 46 | Aliases map[string][]string `yaml:"aliases"` 47 | ExtraAuthorities []string `yaml:"extra_authorities"` 48 | ExtraKnownHosts []string `yaml:"extra_known_hosts"` 49 | AuthenticatingProxy *AuthenticatingProxy `yaml:"auth_proxy"` 50 | SSH SSH `yaml:"ssh"` 51 | GitHub GitHub `yaml:"github"` 52 | Telemetry Telemetry `yaml:"telemetry"` 53 | } 54 | 55 | type SSH struct { 56 | UserCertExtensions []string `yaml:"user_cert_extensions"` 57 | } 58 | 59 | type TLS struct { 60 | CA string 61 | Cert string 62 | Key string 63 | } 64 | 65 | type Database struct { 66 | Username string 67 | Password string 68 | Address string 69 | Schema string 70 | Type string 71 | TLS *TLS 72 | } 73 | 74 | // An AuthenticatingProxy represents a known entity that will perform 75 | // authentication of incoming requests to Sharkey. 76 | // 77 | // The authenticating proxy connection can be validated with either 78 | // a hostname in the TLS connection OR by a SPIFFE ID contained in 79 | // the certificate used for the TLS connection. 80 | type AuthenticatingProxy struct { 81 | Hostname string `yaml:"hostname"` // Expected hostname of the authenticating proxy 82 | UsernameHeader string `yaml:"username_header"` // Username header key the authenticating proxy will use 83 | AllowedSpiffeIds []spiffeid.ID `yaml:"allowed_spiffe_ids"` // A list of SPIFFE IDs that can be used for authentcation 84 | } 85 | 86 | type GitHub struct { 87 | IncludeUserIdentity bool `yaml:"include_user_identity"` 88 | AppId int64 `yaml:"app_id"` 89 | InstallationId int64 `yaml:"installation_id"` 90 | PrivateKeyPath string `yaml:"private_key_path"` 91 | OrganizationName string `yaml:"organization_name"` 92 | SyncInterval time.Duration `yaml:"sync_interval"` 93 | SyncEnabled bool `yaml:"sync_enabled"` 94 | } 95 | 96 | type Telemetry struct { 97 | Address string `yaml:"address"` 98 | } 99 | 100 | // buildConfig reads command-line options and builds a tls.Config 101 | func BuildTLS(opts TLS) (*tls.Config, error) { 102 | caBundleBytes, err := os.ReadFile(opts.CA) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | caBundle := x509.NewCertPool() 108 | caBundle.AppendCertsFromPEM(caBundleBytes) 109 | 110 | config := &tls.Config{ 111 | RootCAs: caBundle, 112 | ClientCAs: caBundle, 113 | ClientAuth: tls.VerifyClientCertIfGiven, 114 | MinVersion: tls.VersionTLS11, 115 | CipherSuites: []uint16{ 116 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 117 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 118 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 119 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 120 | tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, 121 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, 122 | tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, 123 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, 124 | tls.TLS_RSA_WITH_AES_128_CBC_SHA, 125 | tls.TLS_RSA_WITH_AES_256_CBC_SHA, 126 | }, 127 | CurvePreferences: []tls.CurveID{ 128 | // P-256 has an ASM implementation, others do not (as of 2016-12-19). 129 | tls.CurveP256, 130 | }, 131 | } 132 | 133 | if opts.Cert != "" { 134 | // Setup client certificates 135 | certs, err := tls.LoadX509KeyPair(opts.Cert, opts.Key) 136 | if err != nil { 137 | return nil, err 138 | } 139 | config.Certificates = []tls.Certificate{certs} 140 | } 141 | 142 | return config, nil 143 | } 144 | 145 | // Validate the configured SPIFFE IDs by index since they get 146 | // configured as empty by the parser. The "omitempty" flag 147 | // does not appear to work with spiffeid.ID.isZero() 148 | func (ap *AuthenticatingProxy) validateSpiffeIds() error { 149 | var failedSpiffeIds []int 150 | for index, value := range ap.AllowedSpiffeIds { 151 | if value.IsZero() { 152 | failedSpiffeIds = append(failedSpiffeIds, index) 153 | } 154 | } 155 | 156 | // If there was an error, report the indices. 157 | if len(failedSpiffeIds) > 0 { 158 | return fmt.Errorf("indices of spiffe ids that failed to parse: %v", failedSpiffeIds) 159 | } 160 | 161 | return nil 162 | } 163 | -------------------------------------------------------------------------------- /pkg/server/api/enroll.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2016 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package api 18 | 19 | import ( 20 | "encoding/base64" 21 | "errors" 22 | "fmt" 23 | "io" 24 | "net/http" 25 | "strings" 26 | 27 | "github.com/gorilla/mux" 28 | "github.com/sirupsen/logrus" 29 | "github.com/spiffe/go-spiffe/v2/spiffeid" 30 | "github.com/spiffe/go-spiffe/v2/svid/x509svid" 31 | "github.com/square/sharkey/pkg/server/cert" 32 | "github.com/square/sharkey/pkg/server/config" 33 | "golang.org/x/crypto/ssh" 34 | ) 35 | 36 | func logHttpError(r *http.Request, w http.ResponseWriter, err error, code int, logger *logrus.Logger) { 37 | // Log an error response: 38 | // POST /enroll/example.com: 404 some message 39 | logger.WithFields(logrus.Fields{ 40 | "method": r.Method, 41 | "url": r.URL, 42 | "code": code, 43 | }).WithError(err).Error("logHttpError") 44 | 45 | http.Error(w, err.Error(), code) 46 | } 47 | 48 | func (c *Api) Enroll(w http.ResponseWriter, r *http.Request) { 49 | vars := mux.Vars(r) 50 | hostname := vars["hostname"] 51 | 52 | if !clientAuthenticated(r) { 53 | http.Error(w, "no client certificate provided", http.StatusUnauthorized) 54 | return 55 | } 56 | 57 | hostnameMatches, err := clientHostnameMatches(hostname, r) 58 | if !hostnameMatches { 59 | if err != nil { 60 | c.logger.Error(err) 61 | } 62 | 63 | http.Error(w, "hostname does not match certificate", http.StatusForbidden) 64 | return 65 | } 66 | 67 | cert, err := c.EnrollHost(hostname, r) 68 | if err != nil { 69 | c.logger.Error("internal error") 70 | http.Error(w, err.Error(), http.StatusInternalServerError) 71 | return 72 | } 73 | 74 | _, _ = w.Write([]byte(cert)) 75 | } 76 | 77 | // Read a public key off the wire 78 | func readPubkey(r *http.Request) (ssh.PublicKey, error) { 79 | data, err := io.ReadAll(r.Body) 80 | if err != nil { 81 | return nil, err 82 | } 83 | pubkey, _, _, _, err := ssh.ParseAuthorizedKey(data) 84 | return pubkey, err 85 | } 86 | 87 | func (c *Api) EnrollHost(hostname string, r *http.Request) (string, error) { 88 | pubkey, err := readPubkey(r) 89 | if err != nil { 90 | return "", err 91 | } 92 | 93 | signedCert, err := c.signHost(hostname, pubkey) 94 | if err != nil { 95 | return "", err 96 | } 97 | 98 | return cert.EncodeCert(signedCert) 99 | } 100 | 101 | func clientAuthenticated(r *http.Request) bool { 102 | return len(r.TLS.VerifiedChains) > 0 103 | } 104 | 105 | func clientHostnameMatches(hostname string, r *http.Request) (bool, error) { 106 | conn := r.TLS 107 | if len(conn.VerifiedChains) == 0 { 108 | return false, fmt.Errorf("length of TLS chain is zero") 109 | } 110 | cert := conn.VerifiedChains[0][0] 111 | 112 | err := cert.VerifyHostname(hostname) 113 | if err != nil { 114 | return false, fmt.Errorf("hostname failed to verify: %w", err) 115 | } 116 | 117 | return true, nil 118 | } 119 | 120 | func clientSpiffeIdMatches(expected []spiffeid.ID, r *http.Request) (bool, error) { 121 | conn := r.TLS 122 | if len(conn.VerifiedChains) == 0 { 123 | return false, fmt.Errorf("length of TLS chain is zero") 124 | } 125 | 126 | cert := conn.VerifiedChains[0][0] 127 | 128 | // Get the SPIFFE ID from the presented certificate 129 | actualId, err := x509svid.IDFromCert(cert) 130 | if err != nil { 131 | return false, fmt.Errorf("bad spiffe ID from cert: %w", err) 132 | } 133 | 134 | matcher := spiffeid.MatchOneOf(expected...) 135 | 136 | validationFailed := matcher(actualId) 137 | if validationFailed != nil { 138 | return false, fmt.Errorf("failed validation of spiffe ID presented from cert: %w", validationFailed) 139 | } 140 | 141 | return true, nil 142 | } 143 | 144 | func (c *Api) signHost(hostname string, pubkey ssh.PublicKey) (*ssh.Certificate, error) { 145 | principals := []string{hostname} 146 | if c.conf.StripSuffix != "" && strings.HasSuffix(hostname, c.conf.StripSuffix) { 147 | principals = append(principals, strings.TrimSuffix(hostname, c.conf.StripSuffix)) 148 | } 149 | if aliases, ok := c.conf.Aliases[hostname]; ok { 150 | principals = append(principals, aliases...) 151 | } 152 | 153 | return c.signer.Sign(hostname, principals, ssh.HostCert, pubkey, map[string]string{}) 154 | } 155 | 156 | // This assumes there's an authenticating proxy which provides the user in a header, configurable. 157 | // We identify the proxy with its TLS client cert 158 | func proxyAuthenticated(ap *config.AuthenticatingProxy, w http.ResponseWriter, r *http.Request, logger *logrus.Logger) (string, bool) { 159 | if ap == nil { 160 | // Client certificates are not configured 161 | logHttpError(r, w, errors.New("client certificates are unavailable"), http.StatusNotFound, logger) 162 | return "", false 163 | } 164 | 165 | // Host name matching 166 | hostnameMatches, hostnameErr := clientHostnameMatches(ap.Hostname, r) 167 | 168 | // SPIFFE ID matching 169 | spiffeIdMatches, spiffeIdErr := clientSpiffeIdMatches(ap.AllowedSpiffeIds, r) 170 | 171 | // Matching fails case 172 | if !(hostnameMatches || spiffeIdMatches) { 173 | logger.WithError(fmt.Errorf("hostname error: %v. spiffe id error: %v", hostnameErr, spiffeIdErr)) 174 | logHttpError(r, w, fmt.Errorf("request didn't come from proxy"), http.StatusUnauthorized, logger) 175 | return "", false 176 | } 177 | 178 | user := r.Header.Get(ap.UsernameHeader) 179 | if user == "" { // Shouldn't happen 180 | logHttpError(r, w, errors.New("no username supplied"), http.StatusUnauthorized, logger) 181 | return "", false 182 | } 183 | 184 | // We've got a valid connection from the authenticating proxy. 185 | return user, true 186 | } 187 | 188 | func (c *Api) EnrollUser(w http.ResponseWriter, r *http.Request) { 189 | user, ok := proxyAuthenticated(c.conf.AuthenticatingProxy, w, r, c.logger) 190 | if !ok { 191 | // proxyAuthenticated sets http status & logs message 192 | return 193 | } 194 | 195 | pk, err := readPubkey(r) 196 | if err != nil { 197 | logHttpError(r, w, err, http.StatusBadRequest, c.logger) 198 | return 199 | } 200 | 201 | extensions := map[string]string{} 202 | if c.conf.GitHub.IncludeUserIdentity { 203 | username, err := c.RetrieveGitHubUsername(user) 204 | if err != nil { 205 | c.logger.Error(err) 206 | } else if username != "" { 207 | // If no error in retrieval and username not empty string then add github extension 208 | extensions["login@github.com"] = username 209 | } 210 | } 211 | 212 | certificate, err := c.signer.Sign(user, []string{user}, ssh.UserCert, pk, extensions) 213 | if err != nil { 214 | logHttpError(r, w, err, http.StatusInternalServerError, c.logger) 215 | return 216 | } 217 | 218 | certString, err := cert.EncodeCert(certificate) 219 | if err != nil { 220 | logHttpError(r, w, err, http.StatusInternalServerError, c.logger) 221 | return 222 | } 223 | 224 | _, _ = w.Write([]byte(certString)) 225 | 226 | encodedPublicKey := base64.StdEncoding.EncodeToString(pk.Marshal()) 227 | c.logger.WithFields(logrus.Fields{ 228 | "Type": pk.Type(), 229 | "Public Key": encodedPublicKey, 230 | "user": user, 231 | }).Println("call EnrollUser") 232 | } 233 | -------------------------------------------------------------------------------- /pkg/server/api/github_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "net/http/httptest" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/armon/go-metrics" 13 | "github.com/shurcooL/githubv4" 14 | "github.com/sirupsen/logrus" 15 | "github.com/sirupsen/logrus/hooks/test" 16 | "github.com/square/sharkey/pkg/server/config" 17 | "github.com/square/sharkey/pkg/server/storage" 18 | "github.com/square/sharkey/pkg/server/telemetry" 19 | "github.com/stretchr/testify/assert" 20 | "github.com/stretchr/testify/require" 21 | "golang.org/x/crypto/ssh" 22 | ) 23 | 24 | type localRoundTripper struct { 25 | handler http.Handler 26 | } 27 | 28 | func (l localRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 29 | w := httptest.NewRecorder() 30 | l.handler.ServeHTTP(w, req) 31 | return w.Result(), nil 32 | } 33 | 34 | const sampleGitHubApiResult = ` 35 | { 36 | "data": { 37 | "organization": { 38 | "samlIdentityProvider": { 39 | "externalIdentities": { 40 | "edges": [ 41 | { 42 | "node": { 43 | "guid": "1234567890", 44 | "samlIdentity": { 45 | "nameId": "alice" 46 | }, 47 | "user": { 48 | "login": "alice_git" 49 | } 50 | } 51 | }, 52 | { 53 | "node": { 54 | "guid": "1234567891", 55 | "samlIdentity": { 56 | "nameId": "bob" 57 | }, 58 | "user": { 59 | "login": "bob_git" 60 | } 61 | } 62 | }, 63 | { 64 | "node": { 65 | "guid": "1234567892", 66 | "samlIdentity": { 67 | "nameId": "carol" 68 | }, 69 | "user": { 70 | "login": "carol_git" 71 | } 72 | } 73 | } 74 | ], 75 | "pageInfo": { 76 | "hasNextPage": false, 77 | "endCursor": "" 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | ` 85 | 86 | var ( 87 | githubFetchPrefix = strings.Join([]string{telemetry.Service, telemetry.GitHub, telemetry.Fetch}, ".") 88 | gitHubFetchCalls = strings.Join([]string{githubFetchPrefix, telemetry.Calls}, ".") 89 | gitHubFetchCount = strings.Join([]string{githubFetchPrefix, telemetry.Count}, ".") 90 | gitHubFetchLatency = strings.Join([]string{githubFetchPrefix, telemetry.Latency}, ".") 91 | ) 92 | 93 | func TestEmptyGitHubUser(t *testing.T) { 94 | hostname := "proxy" 95 | header := "X-Forwarded-User" 96 | c, err := generateContext(t) 97 | require.NoError(t, err) 98 | 99 | // set auth proxy 100 | c.conf.AuthenticatingProxy = &config.AuthenticatingProxy{ 101 | Hostname: hostname, 102 | UsernameHeader: header, 103 | } 104 | c.conf.GitHub.IncludeUserIdentity = true 105 | 106 | hook := test.NewLocal(c.logger) 107 | 108 | for i := 0; i < 5; i++ { 109 | request, err := generateUserRequest(hostname) 110 | request.Header.Set(header, "alice") 111 | require.NoError(t, err, "Error reading test ssh key") 112 | 113 | rr := httptest.NewRecorder() 114 | c.EnrollUser(rr, request) 115 | 116 | assert.Equal(t, 2, len(hook.Entries)) 117 | assert.Equal(t, logrus.ErrorLevel, hook.Entries[0].Level) 118 | assert.Equal(t, logrus.InfoLevel, hook.LastEntry().Level) 119 | assert.Equal(t, "call EnrollUser", hook.LastEntry().Message) 120 | assert.Contains(t, hook.LastEntry().Data, "Type") 121 | assert.Contains(t, hook.LastEntry().Data, "Public Key") 122 | assert.Contains(t, hook.LastEntry().Data, "user") 123 | assert.Contains(t, hook.Entries[0].Message, "no rows in result set") 124 | 125 | res := rr.Result() 126 | body, err := io.ReadAll(res.Body) 127 | fmt.Println(string(body)) 128 | require.NoError(t, err, "unexpected error reading body") 129 | require.Equal(t, 200, res.StatusCode, "failed to enroll user") 130 | 131 | pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(body)) 132 | require.NoError(t, err, "unexpected error parsing public key") 133 | _, ok := pubKey.(*ssh.Certificate).Extensions["login@github.com"] 134 | assert.Equal(t, ok, false) 135 | 136 | hook.Reset() 137 | } 138 | } 139 | 140 | func TestGitHubUser(t *testing.T) { 141 | hostname := "proxy" 142 | header := "X-Forwarded-User" 143 | c, err := generateContext(t) 144 | require.NoError(t, err) 145 | 146 | // set auth proxy 147 | c.conf.AuthenticatingProxy = &config.AuthenticatingProxy{ 148 | Hostname: hostname, 149 | UsernameHeader: header, 150 | } 151 | c.conf.GitHub.IncludeUserIdentity = true 152 | 153 | sqlite, err := storage.NewSqlite(config.Database{Address: ":memory:"}) 154 | require.NoError(t, err) 155 | err = sqlite.Migrate("../../../db/sqlite/migrations") 156 | require.NoError(t, err) 157 | err = sqlite.RecordGitHubMapping(map[string]string{"alice": "alice_git"}) 158 | require.NoError(t, err) 159 | c.storage = sqlite 160 | 161 | hook := test.NewLocal(c.logger) 162 | 163 | for i := 0; i < 5; i++ { 164 | request, err := generateUserRequest(hostname) 165 | request.Header.Set(header, "alice") 166 | require.NoError(t, err, "Error reading test ssh key") 167 | 168 | rr := httptest.NewRecorder() 169 | c.EnrollUser(rr, request) 170 | 171 | assert.Equal(t, 1, len(hook.Entries)) 172 | assert.Equal(t, logrus.InfoLevel, hook.LastEntry().Level) 173 | assert.Equal(t, "call EnrollUser", hook.LastEntry().Message) 174 | assert.Contains(t, hook.LastEntry().Data, "Type") 175 | assert.Contains(t, hook.LastEntry().Data, "Public Key") 176 | assert.Contains(t, hook.LastEntry().Data, "user") 177 | 178 | res := rr.Result() 179 | body, err := io.ReadAll(res.Body) 180 | fmt.Println(string(body)) 181 | require.NoError(t, err, "unexpected error reading body") 182 | require.Equal(t, 200, res.StatusCode, "failed to enroll user") 183 | 184 | pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(body)) 185 | require.NoError(t, err, "unexpected error parsing public key") 186 | assert.Equal(t, pubKey.(*ssh.Certificate).Extensions["login@github.com"], "alice_git") 187 | 188 | hook.Reset() 189 | } 190 | } 191 | 192 | func TestGitHubFetchMapping(t *testing.T) { 193 | c, err := generateContext(t) 194 | require.NoError(t, err) 195 | inMemSink := metrics.NewInmemSink(10*time.Second, time.Minute) 196 | metricsImpl, err := metrics.New(metrics.DefaultConfig(telemetry.Service), inMemSink) 197 | metricsImpl.EnableHostname = false 198 | require.NoError(t, err) 199 | c.telemetry = &telemetry.Telemetry{ 200 | Metrics: metricsImpl, 201 | } 202 | c.gitHubClient = mockGitHubClient(t) 203 | 204 | mapping, err := c.fetchUserMappings() 205 | require.NoError(t, err, "error fetching github user mappings") 206 | assert.Equal(t, len(mapping), 3) 207 | assert.Equal(t, mapping["alice"], "alice_git") 208 | 209 | assert.Equal(t, len(inMemSink.Data()), 1) 210 | assert.Equal(t, len(inMemSink.Data()[0].Gauges), 2) 211 | assert.Equal(t, inMemSink.Data()[0].Gauges[gitHubFetchCount].Value, float32(3)) 212 | assert.GreaterOrEqual(t, inMemSink.Data()[0].Gauges[gitHubFetchLatency].Value, float32(100)) 213 | assert.Equal(t, len(inMemSink.Data()[0].Counters), 1) 214 | assert.Equal(t, inMemSink.Data()[0].Counters[gitHubFetchCalls].Count, 1) 215 | } 216 | 217 | func mockGitHubClient(t *testing.T) *githubv4.Client { 218 | mux := http.NewServeMux() 219 | mux.HandleFunc("/graphql", func(w http.ResponseWriter, req *http.Request) { 220 | time.Sleep(100 * time.Millisecond) 221 | assert.Equal(t, req.Method, http.MethodPost) 222 | w.Header().Set("Content-Type", "application/json") 223 | _, err := io.WriteString(w, sampleGitHubApiResult) 224 | require.NoError(t, err) 225 | }) 226 | return githubv4.NewClient(&http.Client{Transport: localRoundTripper{handler: mux}}) 227 | } 228 | -------------------------------------------------------------------------------- /pkg/client/sharkey_client.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2016 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package client 18 | 19 | import ( 20 | "bytes" 21 | "crypto/tls" 22 | "crypto/x509" 23 | "io" 24 | "net/http" 25 | "os" 26 | "os/exec" 27 | "strings" 28 | "time" 29 | 30 | "github.com/sirupsen/logrus" 31 | ) 32 | 33 | type tlsConfig struct { 34 | Ca, Cert, Key string 35 | } 36 | 37 | type hostKey struct { 38 | HostKey string `yaml:"plain"` 39 | SignedCert string `yaml:"signed"` 40 | } 41 | 42 | type Config struct { 43 | TLS tlsConfig `yaml:"tls"` 44 | RequestAddr string `yaml:"request_addr"` 45 | HostKey string `yaml:"host_key"` // deprecated 46 | SignedCert string `yaml:"signed_cert"` // deprecated 47 | HostKeys []hostKey `yaml:"host_keys"` 48 | KnownHosts string `yaml:"known_hosts"` 49 | KnownHostsAuthoritiesOnly bool `yaml:"known_hosts_authorities_only"` 50 | Sleep string `yaml:"sleep"` 51 | Sudo string `yaml:"sudo"` 52 | SSHReload []string `yaml:"ssh_reload"` 53 | } 54 | 55 | type Client struct { 56 | conf *Config 57 | client *http.Client 58 | logger *logrus.Logger 59 | } 60 | 61 | func Run(conf *Config, logger *logrus.Logger) { 62 | c := &Client{ 63 | conf: conf, 64 | logger: logger, 65 | } 66 | 67 | if len(c.conf.HostKeys) == 0 { 68 | // Support old host_key/signed_cert options 69 | c.conf.HostKeys = []hostKey{ 70 | {c.conf.HostKey, c.conf.SignedCert}, 71 | } 72 | } else if c.conf.HostKey != "" || c.conf.SignedCert != "" { 73 | c.logger.Fatal("Options host_key/signed_cert and host_keys are mutually exclusive") 74 | } 75 | 76 | if err := c.GenerateClient(); err != nil { 77 | c.logger.WithError(err).Fatalln("Error generating http client") 78 | } 79 | 80 | c.logger.Println("Fetching updated SSH certificate from server") 81 | for _, entry := range c.conf.HostKeys { 82 | c.enroll(entry.HostKey, entry.SignedCert) 83 | } 84 | c.makeKnownHosts() 85 | c.reloadSSH() 86 | 87 | if c.conf.Sleep != "" { 88 | sleep, err := time.ParseDuration(c.conf.Sleep) 89 | if err != nil { 90 | logger.WithError(err).Fatalln("Error parsing sleep duration") 91 | } 92 | ticker := time.NewTicker(sleep) 93 | for range ticker.C { 94 | if err = c.GenerateClient(); err != nil { 95 | logger.WithError(err).Fatalln("Error generating http client") 96 | } 97 | 98 | logger.Println("Fetching updated SSH certificate from server") 99 | for _, entry := range c.conf.HostKeys { 100 | c.enroll(entry.HostKey, entry.SignedCert) 101 | } 102 | c.makeKnownHosts() 103 | c.reloadSSH() 104 | } 105 | } 106 | } 107 | 108 | func (c *Client) enroll(hostKey string, signedCert string) { 109 | hostname, err := os.Hostname() 110 | if err != nil { 111 | // Should be impossible 112 | panic(err) 113 | } 114 | url := c.conf.RequestAddr + "/enroll/" + hostname // host name of machine running on 115 | hostkey, err := os.ReadFile(hostKey) // path to host key 116 | if err != nil { 117 | c.logger.WithFields(logrus.Fields{ 118 | "hostkey": hostKey, 119 | "error": err, 120 | }).Print("Error reading host key") 121 | return 122 | } 123 | resp, err := c.client.Post(url, "text/plain", bytes.NewReader(hostkey)) 124 | if err != nil { 125 | c.logger.WithError(err).Println("Error talking to backend") 126 | return 127 | } 128 | defer resp.Body.Close() 129 | body, err := io.ReadAll(resp.Body) 130 | if err != nil { 131 | c.logger.WithError(err).Errorln("Error reading response from server") 132 | return 133 | } 134 | if resp.StatusCode != 200 { 135 | c.logger.WithField("body", string(body)).Errorln("Error retrieving signed cert from server") 136 | return 137 | } 138 | tmp, err := os.CreateTemp("", "sharkey-signed-cert") 139 | if err != nil { 140 | c.logger.WithError(err).Errorln("Error creating temp file") 141 | return 142 | } 143 | defer os.Remove(tmp.Name()) 144 | err = os.Chmod(tmp.Name(), 0644) 145 | if err != nil { 146 | c.logger.WithError(err).WithField("tmpName", tmp.Name()).Errorln("Error calling chmod") 147 | return 148 | } 149 | err = os.WriteFile(tmp.Name(), body, 0644) 150 | if err != nil { 151 | c.logger.WithError(err).WithField("tmpName", tmp.Name()).Errorln("Error writing file") 152 | return 153 | } 154 | 155 | c.logger.WithField("signedCert", signedCert).Println("Installing updated SSH certificate") 156 | c.shellOut([]string{"/bin/mv", tmp.Name(), signedCert}) 157 | } 158 | 159 | func (c *Client) reloadSSH() { 160 | c.logger.Println("Restarting SSH daemon to make it pick up new certificate") 161 | c.shellOut(c.conf.SSHReload) 162 | } 163 | 164 | func (c *Client) makeKnownHosts() { 165 | var knownHosts string 166 | if c.conf.KnownHostsAuthoritiesOnly { 167 | knownHosts = "/authority" 168 | } else { 169 | knownHosts = "/known_hosts" 170 | } 171 | url := c.conf.RequestAddr + knownHosts 172 | resp, err := c.client.Get(url) 173 | if err != nil { 174 | c.logger.WithError(err).Errorln("Error talking to backend") 175 | return 176 | } 177 | defer resp.Body.Close() 178 | str, err := io.ReadAll(resp.Body) 179 | if err != nil { 180 | c.logger.WithError(err).Errorln("Error reading response body") 181 | return 182 | } 183 | if resp.StatusCode != 200 { 184 | c.logger.WithField("StatusCode", resp.StatusCode).Errorln("Error retrieving known hosts file from server") 185 | return 186 | } 187 | tmp, err := os.CreateTemp("", "sharkey-known-hosts") 188 | if err != nil { 189 | c.logger.WithError(err).Errorln("Error creating temp file") 190 | return 191 | } 192 | defer os.Remove(tmp.Name()) 193 | err = os.Chmod(tmp.Name(), 0644) 194 | if err != nil { 195 | c.logger.WithError(err).WithField("tmpName", tmp.Name()).Errorln("Error calling chmod") 196 | return 197 | } 198 | err = os.WriteFile(tmp.Name(), str, 0644) 199 | if err != nil { 200 | c.logger.WithError(err).WithField("tmpName", tmp.Name()).Errorln("Error writing file") 201 | return 202 | } 203 | 204 | c.logger.WithField("KnownHosts", c.conf.KnownHosts).Println("Installing known_hosts file") 205 | c.shellOut([]string{"/bin/mv", tmp.Name(), c.conf.KnownHosts}) 206 | } 207 | 208 | func (c *Client) GenerateClient() error { 209 | if c.client != nil { 210 | c.client.CloseIdleConnections() 211 | } 212 | 213 | tlsConfig, err := buildConfig(c.conf.TLS.Ca) 214 | if err != nil { 215 | return err 216 | } 217 | cert, err := tls.LoadX509KeyPair(c.conf.TLS.Cert, c.conf.TLS.Key) 218 | if err != nil { 219 | return err 220 | } 221 | tlsConfig.Certificates = []tls.Certificate{cert} 222 | tr := &http.Transport{TLSClientConfig: tlsConfig} 223 | c.client = &http.Client{Transport: tr} 224 | return nil 225 | } 226 | 227 | // buildConfig reads command-line options and builds a tls.Config 228 | func buildConfig(caBundlePath string) (*tls.Config, error) { 229 | caBundleBytes, err := os.ReadFile(caBundlePath) 230 | if err != nil { 231 | return nil, err 232 | } 233 | 234 | caBundle := x509.NewCertPool() 235 | caBundle.AppendCertsFromPEM(caBundleBytes) 236 | 237 | return &tls.Config{ 238 | // Certificates 239 | RootCAs: caBundle, 240 | ClientCAs: caBundle, 241 | ClientAuth: tls.RequireAndVerifyClientCert, 242 | MinVersion: tls.VersionTLS12, 243 | CipherSuites: []uint16{ 244 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 245 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 246 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 247 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 248 | }, 249 | CurvePreferences: []tls.CurveID{ 250 | // P-256 has an ASM implementation, others do not (as of 2016-12-19). 251 | tls.CurveP256, 252 | }, 253 | }, nil 254 | } 255 | 256 | func (c *Client) shellOut(command []string) { 257 | if len(command) == 0 { 258 | return 259 | } 260 | if c.conf.Sudo != "" { 261 | command = append([]string{c.conf.Sudo}, command...) 262 | } 263 | cmd := exec.Cmd{ 264 | Path: command[0], 265 | Args: command, 266 | } 267 | var stdout bytes.Buffer 268 | var stderr bytes.Buffer 269 | cmd.Stdout = &stdout 270 | cmd.Stderr = &stderr 271 | 272 | c.logger.WithField("commands", strings.Join(command, " ")).Println("calling exec on commands") 273 | 274 | err := cmd.Run() 275 | if err != nil { 276 | c.logger.WithError(err).WithField("command", command).Errorln("Failed to execute command") 277 | if len(stdout.Bytes()) > 0 { 278 | c.logger.WithField("stdout", stdout.Bytes()).Println("Printing Stdout") 279 | } 280 | if len(stderr.Bytes()) > 0 { 281 | c.logger.WithField("stderr", stderr.Bytes()).Errorln("Printing Stderr") 282 | } 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![say no to TOFU](sharkey.png) 2 | 3 | # sharkey 4 | 5 | [![license](http://img.shields.io/badge/license-apache_2.0-blue.svg?style=flat)](https://raw.githubusercontent.com/square/certigo/master/LICENSE) 6 | ![development status](https://img.shields.io/badge/status-alpha-orange.svg) 7 | [![tests](https://github.com/square/sharkey/actions/workflows/tests.yml/badge.svg)](https://github.com/square/sharkey/actions/workflows/tests.yml) 8 | [![report](https://goreportcard.com/badge/github.com/square/sharkey)](https://goreportcard.com/report/github.com/square/sharkey) 9 | 10 | Sharkey is a service for managing certificates for use by OpenSSH. 11 | 12 | ![sharks](dancing-sharks.png) 13 | 14 | Sharkey has a client component and a server component. The server is 15 | responsible for issuing signed host certificates, the client is responsible for 16 | installing host certificates on machines. Sharkey builds on the trust relationships 17 | of your existing X.509 PKI to manage trusted SSH certificates. Existing X.509 18 | certificates can be minted into SSH certificates, so you don't have to maintain 19 | two separate PKI hierarchies. 20 | 21 | ### Build 22 | 23 | Check out the repository, and build client/server: 24 | 25 | go build -o sharkey-client ./client 26 | go build -o sharkey-server ./server 27 | 28 | ### Server 29 | 30 | The server component accepts requests and issues short lived host certificates. 31 | 32 | Clients send their public key to the server (via TLS with mutual 33 | authentication) periodically. The server authenticates the client by checking 34 | that its certificate is valid for the requested hostname. If everything looks 35 | good, the server will take the public key in the request and issue an OpenSSH 36 | host certificate for the requested hostname. 37 | 38 | A log of all issued certificates is stored in a database. The server can 39 | generate a `known_hosts` file from the issuance log if required. 40 | 41 | Usage: 42 | 43 | usage: sharkey-server --config=CONFIG [] [ ...] 44 | 45 | Certificate issuer of the ssh-ca system. 46 | 47 | Flags: 48 | --help Show context-sensitive help (also try --help-long and --help-man). 49 | --config=CONFIG Path to config file for server. 50 | --version Show application version. 51 | 52 | Commands: 53 | help [...] 54 | Show help. 55 | 56 | start 57 | Run the sharkey server. 58 | 59 | migrate [] 60 | Set up database/run migrations. 61 | 62 | Configuration (example): 63 | 64 | # SQLite database 65 | # --- 66 | db: 67 | address: /path/to/sharkey.db 68 | type: sqlite 69 | 70 | # MySQL database 71 | # --- 72 | # db: 73 | # username: root 74 | # password: password 75 | # address: hostname:port 76 | # schema: ssh_ca 77 | # type: mysql 78 | # tls: # MySQL TLS config (optional) 79 | # ca: /path/to/mysql-ca-bundle.pem 80 | # cert: /path/to/mysql-client-cert.pem # MySQL client cert 81 | # key: /path/to/mysql-client-cert-key.pem # MySQL client cert key 82 | 83 | # Server listening address 84 | listen_addr: "0.0.0.0:8080" 85 | 86 | # TLS config for serving requests 87 | # --- 88 | tls: 89 | ca: /path/to/ca-bundle.pem 90 | cert: /path/to/server-certificate.pem 91 | key: /path/to/server-certificate-key.pem 92 | 93 | # Signing key (from ssh-keygen) 94 | signing_key: /path/to/ca-signing-key 95 | 96 | # Lifetime/validity duration for generated host certificates 97 | host_cert_duration: 168h 98 | 99 | # Lifetime/validity duration for generated user certificates 100 | user_cert_duration: 24h 101 | 102 | # Optional suffix to strip from client hostnames when generating certificates. 103 | # This is useful if all your machines have a common TLD/domain, and you want to 104 | # include an alias in the generated certificate that doesn't include that suffix. 105 | # Leave empty to disable 106 | strip_suffix: ".example.com" 107 | 108 | # Optional set of aliases for hosts. If a hostname matches an alias entry, the 109 | # listed principals will be added to its certificate. This is useful if you have 110 | # special hosts that are accessed via CNAME records. 111 | aliases: 112 | "host.example.com": 113 | - "alias1.example.com" 114 | - "alias2.example.com" 115 | 116 | # Optional set of extra entries to provide to clients when they fetch a known_hosts 117 | # file. This is useful if you have externally-managed servers in your infrastructure 118 | # that you want to tell clients about, of if you want to add CA entries to the 119 | # known_hosts file. 120 | extra_known_hosts: 121 | - "@cert-authority *.example.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBwhA8rKPESjDy4iqTlkBqUlBU2xjwtmFUHY6cutA9TYbB5H/mjxzUpnSNw/HyFWNpysjTSQtHWWBdJdJGU/0aDgFUwbduHeDFxviGVSkOxm2AYn7XJopzITZRqmAmsYXHUBa75RQb+UgIG7EpCoi8hF4ItJV+TT777j1irkXwlMmeDiJEaA+7bPNdUdGw8zRbk0CyeotYVD0griRtkXdfgnQAu+DvBwOuW/uiZaPz/rAVjt4b9fmp6pcFKI3RsBqqn5tQVhKCPVuSwqvIQ7CTVkMClYovlH1/zGe8PG1DHbM9irP98S5j3mVD9W5v3QILpsg24RIS14M8pLarlD6t root@authority" 122 | 123 | # User certs are issued to users who connect through an authenticating proxy 124 | # That user should connect with a user certificate and set the username 125 | # in a header. 126 | auth_proxy: 127 | # Hostname is validated against the incoming user certificate 128 | hostname: proxy.example.com 129 | # The HTTP header containing the username 130 | username_header: X-Forwarded-User 131 | 132 | # Optional settings related to SSH 133 | ssh: 134 | # List of extensions that should be set on the user certificate (default is no extensions) 135 | user_cert_extensions: 136 | - "permit-X11-forwarding" 137 | - "permit-agent-forwarding" 138 | - "permit-port-forwarding" 139 | - "permit-pty" 140 | - "permit-user-rc" 141 | 142 | A signing key for generating host certificates can be generated with `ssh-keygen`. 143 | 144 | #### Database 145 | 146 | Sharkey supports both SQLite and MySQL. There is a built-in command in the 147 | server binary to manage migrations (based on [goose][goose]). 148 | 149 | To run migrations on a configured database: 150 | 151 | # SQLite 152 | ./sharkey-server --config=[CONFIG] migrate --migrations=db/sqlite 153 | 154 | # MySQL 155 | ./sharkey-server --config=[CONFIG] migrate --migrations=db/mysql 156 | 157 | You can also manage migrations using the [goose][goose] command-line utility. 158 | See the [goose][goose] documentation for more info. 159 | 160 | [goose]: https://bitbucket.org/liamstask/goose 161 | 162 | ### Client 163 | 164 | The client component periodically requests a new host certificate from the 165 | server and installs it on the machine. 166 | 167 | The client will use a TLS client certificate to make a connection to the server 168 | and authenticate itself. This assumes that there is a long-lived certificate 169 | and key installed on each machine that uses the client. We then periodically 170 | read the host key for the locally running OpenSSH (`host_key`), send it to the 171 | server, and retrieve a signed host certificate based on that key. The signed 172 | host certificate is then installed on the machine (`signed_cert`). 173 | 174 | Usage: 175 | 176 | usage: sharkey-client --config=CONFIG [] 177 | 178 | Flags: 179 | --help Show context-sensitive help (also try --help-long and --help-man). 180 | --config=CONFIG Path to yaml config file for setup 181 | --version Show application version. 182 | 183 | Configuration (example): 184 | 185 | # Server address 186 | request_addr: "https://sharkey-server.example:8080" 187 | 188 | # TLS config for making requests 189 | # --- 190 | tls: 191 | ca: /path/to/ca-bundle.pem 192 | cert: /path/to/client-certificate.pem 193 | key: /path/to/client-certificate-key.pem 194 | 195 | # List of host keys for OpenSSH server 196 | host_keys: 197 | # Here, 'key' is the public key, and 'cert' is where to install the signed cert 198 | - plain: "/etc/ssh/ssh_host_rsa_key.pub" 199 | signed: "/etc/ssh/ssh_host_rsa_key-cert.pub" 200 | # You can specify multiple host keys (e.g. if you have both RSA, ED25519 keys) 201 | - plain: "/etc/ssh/ssh_host_ed25519_key.pub" 202 | signed: "/etc/ssh/ssh_host_ed25519_key-cert.pub" 203 | 204 | # Where to install the known_hosts file 205 | known_hosts: /etc/ssh/known_hosts 206 | 207 | # If set to true, only install authorities in known_hosts file (ignore other machine's host keys). 208 | known_hosts_authorities_only: false 209 | 210 | # How often to refresh/request new certificate 211 | sleep: "24h" 212 | 213 | # Path to sudo binary on client host 214 | # Uses sudo to write known_hosts and signed_cert.pub if this field specified 215 | sudo: "/usr/bin/sudo" 216 | 217 | # Command to restart ssh daemon for the host 218 | # If sudo is set as well, this command will be prefixed with 'sudo' 219 | ssh_reload: ["/usr/sbin/service", "ssh", "restart"] 220 | 221 | OpenSSH will have to be configured to read the signed host certificate (this is 222 | with the `HostCertificate` config option in `sshd_config`). If the signed host 223 | certificate is missing from disk, OpenSSH will fall back to TOFU with the 224 | default host key. Therefore, it should always be safe to configure a host 225 | certificate; even if the Sharkey client fails you can still SSH into your 226 | machine. 227 | 228 | ### User Certificates 229 | 230 | For a user to SSH into an openssh server, they can present a certificate, which 231 | should have a principal matching their username. 232 | Sharkey outsources identifying users to an SSO proxy. That proxy needs to 233 | connect to sharkey over mTLS. You can configure the DNS SAN that should appear 234 | on the server's client cert (eg, proxy.example.com) and the HTTP header it sets 235 | the username to (eg, X-Forwarded-User). See example configs. 236 | 237 | No client helper is included with Sharkey at this time, so you have to set up 238 | a script yourself at this time to enroll the user. 239 | 240 | Testing looks something like this: 241 | `curl --cert proxy.crt --key proxy.key https://localhost:8080/enroll_user -H "X-Forwarded-User: bob" -d @~/.ssh/bob.pub` 242 | 243 | But in production use you'd expect it more like 244 | `curl https://ssoproxy.example.com/enroll_user -d @~/.ssh/bob.pub` 245 | 246 | ### GitHub SSH CA Support 247 | 248 | Sharkey supports issuing user certificates that are compatible with GitHub SSH CA format by: 249 | 250 | - Mapping a GitHub username to a SAML identity 251 | - Including appropriate GitHub username in each certificate 252 | 253 | GitHub supports authentication using SSH certificates for Enterprise Cloud accounts. The only requirement is that certificates include GitHub usernames, so that they can be matched to a particular user. 254 | 255 | Sharkey already requires SSO proxy for the user certificate feature. Additionally, the GitHub integration requires that the GitHub organization is configured with SSO (i.e. non-GitHub) access. 256 | 257 | An example config with GitHub SSH CA Support enabled can be found in `test/git_server_config.yaml`. 258 | A GitHub App with read/write access to `Organization:members` is required. 259 | 260 | Sharkey will periodically query GitHub for a mapping of SAML identities to GitHub usernames and store it in Sharkey's DB. 261 | When issuing a certificate, Sharkey will check the DB and if a mapping exists, attaches it to the certificate as an extension. 262 | 263 | An example cert is shown below: 264 | ``` 265 | Type: ssh-rsa-cert-v01@openssh.com user certificate 266 | Public key: RSA-CERT SHA256:Eabuov2aAPLhN1FscJ6P3Lle85N6Txhj4sy4ALTkG6M 267 | Signing CA: ED25519 SHA256:HYgRf1dHbVtWY/e3jjfnAlwvAPPBKYxdXz8SDfhlAws (using ssh-ed25519) 268 | Key ID: "alice" 269 | Serial: 1 270 | Valid: from 2020-07-31T16:10:25 to 2020-08-01T16:10:25 271 | Principals: 272 | alice 273 | Critical Options: (none) 274 | Extensions: 275 | login@github.com UNKNOWN OPTION (len 5) 276 | permit-X11-forwarding 277 | permit-agent-forwarding 278 | permit-port-forwarding 279 | permit-pty 280 | permit-user-rc 281 | ``` 282 | 283 | ### Telemetry 284 | 285 | Sharkey supports sending DogStatsD metrics. Currently only metrics regarding GitHub SSH CA are being emitted. 286 | Adding the following block to the server configuration will enable metrics: 287 | ``` 288 | telemetry: 289 | address: "127.0.0.1:8200" 290 | ``` 291 | Unix sockets are also supported. 292 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c h1:bkb2NMGo3/Du52wvYj9Whth5KZfMV6d3O0Vbr3nz/UE= 2 | bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c/go.mod h1:hSVuE3qU7grINVSwrmzHfpg9k87ALBk+XaualNyUzI4= 3 | cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= 4 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 5 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 6 | github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= 7 | github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= 8 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= 9 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= 10 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 11 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 12 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 13 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 14 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4 h1:Hs82Z41s6SdL1CELW+XaDYmOH4hkBN4/N9og/AsOv7E= 15 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 16 | github.com/armon/go-metrics v0.3.5 h1:uq4txK6NAUvLQ60rotN+K+JuTnf3XP4TdQmcs9ma5mk= 17 | github.com/armon/go-metrics v0.3.5/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= 18 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 19 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 20 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 21 | github.com/bradleyfalzon/ghinstallation/v2 v2.4.0 h1:zYSzkoIwekCQAr6GT6KxISLt4YRS6kd4/ixfzMN+7yc= 22 | github.com/bradleyfalzon/ghinstallation/v2 v2.4.0/go.mod h1:4MwZLSgBJJgg4i3nJwZJ95AMooSqN8fJDmegLVn9Q2U= 23 | github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 24 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 25 | github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= 26 | github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= 27 | github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= 28 | github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= 29 | github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 30 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 31 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 32 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 33 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 34 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 35 | github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= 36 | github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= 37 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 38 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 39 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 40 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 41 | github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= 42 | github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= 43 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 44 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 45 | github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 46 | github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= 47 | github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 48 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 49 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 50 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 51 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 52 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 53 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 54 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 55 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 56 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 57 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 58 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 59 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 60 | github.com/google/go-github/v52 v52.0.0 h1:uyGWOY+jMQ8GVGSX8dkSwCzlehU3WfdxQ7GweO/JP7M= 61 | github.com/google/go-github/v52 v52.0.0/go.mod h1:WJV6VEEUPuMo5pXqqa2ZCZEdbQqua4zAk2MZTIo+m+4= 62 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 63 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 64 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 65 | github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= 66 | github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= 67 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 68 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 69 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 70 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 71 | github.com/hashicorp/go-immutable-radix v1.2.0 h1:l6UW37iCXwZkZoAbEYnptSHVE/cQ5bOTPYG5W3vf9+8= 72 | github.com/hashicorp/go-immutable-radix v1.2.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 73 | github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= 74 | github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= 75 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 76 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 77 | github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= 78 | github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 79 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 80 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 81 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 82 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 83 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 84 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 85 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 86 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 87 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 88 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 89 | github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28 h1:mkl3tvPHIuPaWsLtmHTybJeoVEW7cbePK73Ir8VtruA= 90 | github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c= 91 | github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= 92 | github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 93 | github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= 94 | github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 95 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 96 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 97 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 98 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 99 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 100 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 101 | github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= 102 | github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 103 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 104 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 105 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 106 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 107 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 108 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 109 | github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= 110 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 111 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 112 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 113 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 114 | github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= 115 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 116 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 117 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 118 | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= 119 | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 120 | github.com/shurcooL/githubv4 v0.0.0-20200627185320-e003124d66e4 h1:cjmR6xY0f89IwBYMSwUrkFs4/1+KKw30Df3SqT7nZ6Q= 121 | github.com/shurcooL/githubv4 v0.0.0-20200627185320-e003124d66e4/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= 122 | github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk= 123 | github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= 124 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 125 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 126 | github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= 127 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 128 | github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE= 129 | github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= 130 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 131 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 132 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 133 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 134 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 135 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 136 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 137 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 138 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 139 | github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= 140 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 141 | github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= 142 | github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= 143 | github.com/ziutek/mymysql v0.0.0-20160623123511-8787d5581eb6 h1:e+GLcI9ToenNBlc57xXLXg9vBKcF1UZJ4+qrdQsxQzo= 144 | github.com/ziutek/mymysql v0.0.0-20160623123511-8787d5581eb6/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= 145 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 146 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 147 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 148 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 149 | golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= 150 | golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= 151 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 152 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 153 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 154 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 155 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 156 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 157 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 158 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 159 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 160 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 161 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 162 | golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= 163 | golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= 164 | golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= 165 | golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= 166 | golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 167 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 168 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 169 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 170 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 171 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 172 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 173 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 174 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 175 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 176 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 177 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 178 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 179 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 180 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 181 | golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 182 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 183 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 184 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 185 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 186 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 187 | golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= 188 | golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 189 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 190 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 191 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 192 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 193 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 194 | golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= 195 | golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= 196 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 197 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 198 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 199 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 200 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 201 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 202 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 203 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 204 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 205 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 206 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 207 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 208 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 209 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 210 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 211 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 212 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 213 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 214 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 215 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 216 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 217 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 218 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 219 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 220 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 221 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 222 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 223 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 224 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 225 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 226 | -------------------------------------------------------------------------------- /pkg/server/api/server_test.go: -------------------------------------------------------------------------------- 1 | /*- 2 | * Copyright 2016 Square Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package api 18 | 19 | import ( 20 | "crypto/tls" 21 | "crypto/x509" 22 | "encoding/pem" 23 | "errors" 24 | "fmt" 25 | "io" 26 | "net/http" 27 | "net/http/httptest" 28 | "os" 29 | "strings" 30 | "testing" 31 | 32 | _ "github.com/mattn/go-sqlite3" 33 | "github.com/sirupsen/logrus" 34 | "github.com/sirupsen/logrus/hooks/test" 35 | "github.com/spiffe/go-spiffe/v2/spiffeid" 36 | "github.com/square/sharkey/pkg/server/cert" 37 | "github.com/square/sharkey/pkg/server/config" 38 | "github.com/square/sharkey/pkg/server/storage" 39 | "github.com/stretchr/testify/assert" 40 | "github.com/stretchr/testify/require" 41 | "golang.org/x/crypto/ssh" 42 | ) 43 | 44 | const ( 45 | testKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsfClUt72oaV+J4mAe3XK1nPqXn9ISTxRNj" + 46 | "giXNYhmVvluwrtS5o0Fwc144c1pqW38QilcvCNmaiXvPxdaSyzTnVCg8UGlNsa/Fwz5Lc/hojAoQCitiRxBna81VSGZI" + 47 | "Ob79JD4lVxGxDOfVykfvjo4KzfDE4stMPixW6grDlpUsb6MVELUB1jcyx+j6RVctPYuRtZKLI/5SX6NGWK3H6P68IhY+" + 48 | "2MKYIc6+TItabryI0cNTIcjkPyetAo2T1BOl8sPeukIvX3zG2NrxxinXrEWScYpsuoewvuCYdc/+fY2o498PwM+asCpQ" + 49 | "i+3IRj7siWEDLwK0kga+aYrwyO2/TiB" 50 | defaultProxyPath = "testdata/proxy.crt" 51 | proxy2Path = "testdata/proxy2.crt" 52 | goodName = "goodname" 53 | badName = "badname" 54 | ) 55 | 56 | func TestValidClient(t *testing.T) { 57 | request, err := generateHostRequest(goodName) 58 | require.NoError(t, err, "error reading test ssh key") 59 | 60 | hostnameMatches, err := clientHostnameMatches(badName, request) 61 | require.Error(t, err, "thought a bad client was valid") 62 | require.False(t, hostnameMatches, "thought a bad client was valid") 63 | 64 | hostnameMatches, err = clientHostnameMatches(goodName, request) 65 | require.NoError(t, err, "thought a good client was invalid") 66 | require.True(t, hostnameMatches, "thought a good client was invalid") 67 | } 68 | 69 | func TestSignHost(t *testing.T) { 70 | c, err := generateContext(t) 71 | require.NoError(t, err) 72 | 73 | data, err := os.ReadFile("testdata/ssh_host_rsa_key.pub") 74 | require.NoError(t, err) 75 | 76 | pubkey, _, _, _, err := ssh.ParseAuthorizedKey(data) 77 | require.NoError(t, err) 78 | 79 | cert, err := c.signHost("hostname.square", pubkey) 80 | require.NoError(t, err, "SignHost method returned an error") 81 | require.Equal(t, uint64(1), cert.Serial, "Incorrect cert serial number") 82 | require.Equal(t, "hostname.square", cert.KeyId, "Cert pubkey doesn't match") 83 | require.Equal(t, pubkey, cert.Key, "Cert pubkey doesn't match") 84 | require.Contains(t, cert.ValidPrincipals, "hostname.square") 85 | require.Contains(t, cert.ValidPrincipals, "alias.square") 86 | } 87 | 88 | func TestEnrollHost(t *testing.T) { 89 | c, err := generateContext(t) 90 | require.NoError(t, err) 91 | 92 | for i := 0; i < 5; i++ { 93 | request, err := generateHostRequest(goodName) 94 | require.NoError(t, err, "Error reading test ssh key") 95 | 96 | _, err = c.EnrollHost("goodname", request) 97 | require.NoError(t, err, "Error enrolling host") 98 | } 99 | } 100 | 101 | func TestClientHostNameMatchesEmpty(t *testing.T) { 102 | _, err := generateContext(t) 103 | require.NoError(t, err) 104 | 105 | for i := 0; i < 5; i++ { 106 | request, err := generateHostRequest("") 107 | require.NoError(t, err, "Error reading test ssh key") 108 | 109 | hostnameMatches, err := clientHostnameMatches("", request) 110 | require.False(t, hostnameMatches, "Accepted unknown host") 111 | require.Error(t, err, "Accepted unknown host") 112 | } 113 | } 114 | 115 | func TestEnrollUserNoSpiffeId(t *testing.T) { 116 | hostname := "proxy" 117 | header := "X-Forwarded-User" 118 | c, err := generateContext(t) 119 | require.NoError(t, err) 120 | 121 | // set auth proxy 122 | c.conf.AuthenticatingProxy = &config.AuthenticatingProxy{ 123 | Hostname: hostname, 124 | UsernameHeader: header, 125 | } 126 | 127 | hook := test.NewLocal(c.logger) 128 | 129 | for i := 0; i < 5; i++ { 130 | request, err := generateUserRequest(hostname) 131 | request.Header.Set(header, "alice") 132 | require.NoError(t, err, "Error reading test ssh key") 133 | 134 | rr := httptest.NewRecorder() 135 | c.EnrollUser(rr, request) 136 | 137 | assert.Equal(t, 1, len(hook.Entries)) 138 | assert.Equal(t, logrus.InfoLevel, hook.LastEntry().Level) 139 | assert.Equal(t, "call EnrollUser", hook.LastEntry().Message) 140 | assert.Contains(t, hook.LastEntry().Data, "Type") 141 | assert.Contains(t, hook.LastEntry().Data, "Public Key") 142 | assert.Contains(t, hook.LastEntry().Data, "user") 143 | 144 | res := rr.Result() 145 | body, err := io.ReadAll(res.Body) 146 | fmt.Println(string(body)) 147 | require.NoError(t, err, "unexpected error reading body") 148 | require.Equal(t, 200, res.StatusCode, "failed to enroll user") 149 | hook.Reset() 150 | } 151 | } 152 | 153 | func TestEnrollUserSpiffeIdEmptyHostname(t *testing.T) { 154 | hostname := "proxy" 155 | header := "X-Forwarded-User" 156 | c, err := generateContext(t) 157 | require.NoError(t, err) 158 | 159 | // set auth proxy 160 | c.conf.AuthenticatingProxy = &config.AuthenticatingProxy{ 161 | Hostname: hostname, 162 | UsernameHeader: header, 163 | AllowedSpiffeIds: []spiffeid.ID{spiffeid.RequireFromString("spiffe://proxy.com")}, 164 | } 165 | 166 | hook := test.NewLocal(c.logger) 167 | 168 | for i := 0; i < 5; i++ { 169 | request, err := generateSpiffeUserRequest("", defaultProxyPath) 170 | require.NoError(t, err, "Error reading test ssh key or parsing X.509 Certificate") 171 | request.Header.Set(header, "alice") 172 | 173 | rr := httptest.NewRecorder() 174 | c.EnrollUser(rr, request) 175 | 176 | assert.Equal(t, 1, len(hook.Entries)) 177 | assert.Equal(t, logrus.InfoLevel, hook.LastEntry().Level) 178 | assert.Equal(t, "call EnrollUser", hook.LastEntry().Message) 179 | assert.Contains(t, hook.LastEntry().Data, "Type") 180 | assert.Contains(t, hook.LastEntry().Data, "Public Key") 181 | assert.Contains(t, hook.LastEntry().Data, "user") 182 | 183 | res := rr.Result() 184 | body, err := io.ReadAll(res.Body) 185 | fmt.Println(string(body)) 186 | require.NoError(t, err, "unexpected error reading body") 187 | require.Equal(t, 200, res.StatusCode, "failed to enroll user") 188 | hook.Reset() 189 | } 190 | } 191 | 192 | func TestEnrollUserSpiffeIdWrongHostname(t *testing.T) { 193 | hostname := "proxy" 194 | header := "X-Forwarded-User" 195 | c, err := generateContext(t) 196 | require.NoError(t, err) 197 | 198 | // set auth proxy 199 | c.conf.AuthenticatingProxy = &config.AuthenticatingProxy{ 200 | Hostname: hostname, 201 | UsernameHeader: header, 202 | AllowedSpiffeIds: []spiffeid.ID{spiffeid.RequireFromString("spiffe://proxy.com")}, 203 | } 204 | 205 | hook := test.NewLocal(c.logger) 206 | 207 | for i := 0; i < 5; i++ { 208 | request, err := generateSpiffeUserRequest(badName, defaultProxyPath) 209 | require.NoError(t, err, "Error reading test ssh key or parsing X.509 Certificate") 210 | request.Header.Set(header, "alice") 211 | 212 | rr := httptest.NewRecorder() 213 | c.EnrollUser(rr, request) 214 | 215 | assert.Equal(t, 1, len(hook.Entries)) 216 | assert.Equal(t, logrus.InfoLevel, hook.LastEntry().Level) 217 | assert.Equal(t, "call EnrollUser", hook.LastEntry().Message) 218 | assert.Contains(t, hook.LastEntry().Data, "Type") 219 | assert.Contains(t, hook.LastEntry().Data, "Public Key") 220 | assert.Contains(t, hook.LastEntry().Data, "user") 221 | 222 | res := rr.Result() 223 | body, err := io.ReadAll(res.Body) 224 | fmt.Println(string(body)) 225 | require.NoError(t, err, "unexpected error reading body") 226 | require.Equal(t, 200, res.StatusCode, "failed to enroll user") 227 | hook.Reset() 228 | } 229 | } 230 | 231 | func TestEnrollUserSpiffeIdNoConfiguredHostname(t *testing.T) { 232 | hostname := "proxy" 233 | header := "X-Forwarded-User" 234 | c, err := generateContext(t) 235 | require.NoError(t, err) 236 | 237 | // set auth proxy 238 | c.conf.AuthenticatingProxy = &config.AuthenticatingProxy{ 239 | UsernameHeader: header, 240 | AllowedSpiffeIds: []spiffeid.ID{spiffeid.RequireFromString("spiffe://proxy.com")}, 241 | } 242 | 243 | hook := test.NewLocal(c.logger) 244 | 245 | for i := 0; i < 5; i++ { 246 | request, err := generateSpiffeUserRequest(hostname, defaultProxyPath) 247 | require.NoError(t, err, "Error reading test ssh key or parsing X.509 Certificate") 248 | request.Header.Set(header, "alice") 249 | 250 | rr := httptest.NewRecorder() 251 | c.EnrollUser(rr, request) 252 | 253 | assert.Equal(t, 1, len(hook.Entries)) 254 | assert.Equal(t, logrus.InfoLevel, hook.LastEntry().Level) 255 | assert.Equal(t, "call EnrollUser", hook.LastEntry().Message) 256 | assert.Contains(t, hook.LastEntry().Data, "Type") 257 | assert.Contains(t, hook.LastEntry().Data, "Public Key") 258 | assert.Contains(t, hook.LastEntry().Data, "user") 259 | 260 | res := rr.Result() 261 | body, err := io.ReadAll(res.Body) 262 | fmt.Println(string(body)) 263 | require.NoError(t, err, "unexpected error reading body") 264 | require.Equal(t, 200, res.StatusCode, "failed to enroll user") 265 | hook.Reset() 266 | } 267 | } 268 | 269 | func TestEnrollUserSpiffeIdWithHostname(t *testing.T) { 270 | hostname := "proxy" 271 | header := "X-Forwarded-User" 272 | c, err := generateContext(t) 273 | require.NoError(t, err) 274 | 275 | // set auth proxy 276 | c.conf.AuthenticatingProxy = &config.AuthenticatingProxy{ 277 | Hostname: hostname, 278 | UsernameHeader: header, 279 | AllowedSpiffeIds: []spiffeid.ID{spiffeid.RequireFromString("spiffe://proxy.com")}, 280 | } 281 | 282 | hook := test.NewLocal(c.logger) 283 | 284 | for i := 0; i < 5; i++ { 285 | request, err := generateSpiffeUserRequest("proxy", defaultProxyPath) 286 | require.NoError(t, err, "Error reading test ssh key or parsing X.509 Certificate") 287 | request.Header.Set(header, "alice") 288 | 289 | rr := httptest.NewRecorder() 290 | c.EnrollUser(rr, request) 291 | 292 | assert.Equal(t, 1, len(hook.Entries)) 293 | assert.Equal(t, logrus.InfoLevel, hook.LastEntry().Level) 294 | assert.Equal(t, "call EnrollUser", hook.LastEntry().Message) 295 | assert.Contains(t, hook.LastEntry().Data, "Type") 296 | assert.Contains(t, hook.LastEntry().Data, "Public Key") 297 | assert.Contains(t, hook.LastEntry().Data, "user") 298 | 299 | res := rr.Result() 300 | body, err := io.ReadAll(res.Body) 301 | fmt.Println(string(body)) 302 | require.NoError(t, err, "unexpected error reading body") 303 | require.Equal(t, 200, res.StatusCode, "failed to enroll user") 304 | hook.Reset() 305 | } 306 | } 307 | 308 | func TestEnrollUserSpiffeIdSecondId(t *testing.T) { 309 | header := "X-Forwarded-User" 310 | c, err := generateContext(t) 311 | require.NoError(t, err) 312 | 313 | // set auth proxy 314 | c.conf.AuthenticatingProxy = &config.AuthenticatingProxy{ 315 | UsernameHeader: header, 316 | AllowedSpiffeIds: []spiffeid.ID{spiffeid.RequireFromString("spiffe://proxy.com"), spiffeid.RequireFromString("spiffe://proxy2.com")}, 317 | } 318 | 319 | hook := test.NewLocal(c.logger) 320 | 321 | for i := 0; i < 5; i++ { 322 | request, err := generateSpiffeUserRequest("", proxy2Path) 323 | require.NoError(t, err, "Error reading test ssh key or parsing X.509 Certificate") 324 | request.Header.Set(header, "alice") 325 | 326 | rr := httptest.NewRecorder() 327 | c.EnrollUser(rr, request) 328 | 329 | assert.Equal(t, 1, len(hook.Entries)) 330 | assert.Equal(t, logrus.InfoLevel, hook.LastEntry().Level) 331 | assert.Equal(t, "call EnrollUser", hook.LastEntry().Message) 332 | assert.Contains(t, hook.LastEntry().Data, "Type") 333 | assert.Contains(t, hook.LastEntry().Data, "Public Key") 334 | assert.Contains(t, hook.LastEntry().Data, "user") 335 | 336 | res := rr.Result() 337 | body, err := io.ReadAll(res.Body) 338 | fmt.Println(string(body)) 339 | require.NoError(t, err, "unexpected error reading body") 340 | require.Equal(t, 200, res.StatusCode, "failed to enroll user") 341 | hook.Reset() 342 | } 343 | } 344 | 345 | func TestEnrollUserNoProxyConfigured(t *testing.T) { 346 | c, err := generateContext(t) 347 | require.NoError(t, err) 348 | hook := test.NewLocal(c.logger) 349 | 350 | request, err := generateUserRequest("proxy") 351 | require.NoError(t, err) 352 | 353 | rr := httptest.NewRecorder() 354 | c.EnrollUser(rr, request) 355 | 356 | assert.Equal(t, 1, len(hook.Entries)) 357 | assert.Equal(t, logrus.ErrorLevel, hook.LastEntry().Level) 358 | assert.Equal(t, "logHttpError", hook.LastEntry().Message) 359 | assert.Equal(t, errors.New("client certificates are unavailable"), hook.LastEntry().Data["error"]) 360 | assert.Contains(t, hook.LastEntry().Data, "method") 361 | assert.Contains(t, hook.LastEntry().Data, "url") 362 | assert.Contains(t, hook.LastEntry().Data, "code") 363 | 364 | res := rr.Result() 365 | _, err = io.ReadAll(res.Body) 366 | require.NoError(t, err, "unexpected error reading body") 367 | require.Equal(t, 404, res.StatusCode, "expected 404 for unconfigured proxy") 368 | } 369 | 370 | func TestEnrollNoAuthedUser(t *testing.T) { 371 | hostname := "proxy" 372 | c, err := generateContext(t) 373 | require.NoError(t, err) 374 | hook := test.NewLocal(c.logger) 375 | 376 | // set auth proxy 377 | c.conf.AuthenticatingProxy = &config.AuthenticatingProxy{ 378 | Hostname: hostname, 379 | } 380 | 381 | request, err := generateUserRequest(hostname) 382 | require.NoError(t, err) 383 | 384 | rr := httptest.NewRecorder() 385 | c.EnrollUser(rr, request) 386 | 387 | assert.Equal(t, "logHttpError", hook.LastEntry().Message) 388 | assert.Equal(t, errors.New("no username supplied"), hook.LastEntry().Data["error"]) 389 | assert.Contains(t, hook.LastEntry().Data, "method") 390 | assert.Contains(t, hook.LastEntry().Data, "url") 391 | assert.Contains(t, hook.LastEntry().Data, "code") 392 | 393 | res := rr.Result() 394 | _, err = io.ReadAll(res.Body) 395 | require.NoError(t, err, "unexpected error reading body") 396 | require.Equal(t, 401, res.StatusCode, "expected 401 for unauthed user") 397 | } 398 | 399 | func TestEnrollWrongProxyDomain(t *testing.T) { 400 | hostname := "proxy" 401 | header := "X-Forwarded-User" 402 | c, err := generateContext(t) 403 | require.NoError(t, err) 404 | hook := test.NewLocal(c.logger) 405 | 406 | // set auth proxy 407 | c.conf.AuthenticatingProxy = &config.AuthenticatingProxy{ 408 | Hostname: hostname, 409 | UsernameHeader: header, 410 | } 411 | 412 | request, err := generateUserRequest("notproxy.com") 413 | request.Header.Set(header, "alice") 414 | require.NoError(t, err) 415 | 416 | rr := httptest.NewRecorder() 417 | c.EnrollUser(rr, request) 418 | 419 | assert.Equal(t, "logHttpError", hook.LastEntry().Message) 420 | assert.Equal(t, errors.New("request didn't come from proxy"), hook.LastEntry().Data["error"]) 421 | assert.Contains(t, hook.LastEntry().Data, "method") 422 | assert.Contains(t, hook.LastEntry().Data, "url") 423 | assert.Contains(t, hook.LastEntry().Data, "code") 424 | 425 | res := rr.Result() 426 | _, err = io.ReadAll(res.Body) 427 | require.NoError(t, err, "unexpected error reading body") 428 | require.Equal(t, 401, res.StatusCode, "expected 401 for requets not from proxy") 429 | } 430 | 431 | func TestGetAuthority(t *testing.T) { 432 | c, err := generateContext(t) 433 | require.NoError(t, err) 434 | 435 | req, err := generateHostRequest(goodName) 436 | require.NoError(t, err, "Error reading test ssh key") 437 | 438 | rec := httptest.NewRecorder() 439 | c.Authority(rec, req) 440 | 441 | rec.Flush() 442 | require.Equalf(t, 200, rec.Code, "Request to /authority failed with %d", rec.Code) 443 | 444 | body, _ := io.ReadAll(rec.Body) 445 | expected, err := os.ReadFile("testdata/server_ca.pub") 446 | require.NoError(t, err, "Error reading testdata") 447 | 448 | require.Equalf(t, []byte(fmt.Sprintf("@cert-authority * %s", expected)), body, 449 | "Request body from /authority unexpectedly returned '%s'", string(body)) 450 | } 451 | 452 | func TestGetAuthorityWithExtraCAs(t *testing.T) { 453 | c, err := generateContext(t) 454 | require.NoError(t, err) 455 | c = addExtraAuthorities(t, c) 456 | 457 | req, err := generateHostRequest(goodName) 458 | require.NoError(t, err, "Error reading test ssh key") 459 | 460 | rec := httptest.NewRecorder() 461 | c.Authority(rec, req) 462 | 463 | rec.Flush() 464 | require.Equalf(t, 200, rec.Code, "Request to /authority failed with %d", rec.Code) 465 | 466 | body, _ := io.ReadAll(rec.Body) 467 | receivedCAs := strings.Split(string(body), "\n") 468 | expectedCA1Bytes, err := os.ReadFile("testdata/server_ca.pub") 469 | require.NoError(t, err, "Error reading testdata") 470 | expectedCA1 := fmt.Sprintf("@cert-authority * %s", strings.Trim(string(expectedCA1Bytes), "\n")) 471 | expectedCA2Bytes, err := os.ReadFile("testdata/next_server_ca.pub") 472 | require.NoError(t, err, "Error reading testdata") 473 | expectedCA2 := fmt.Sprintf("@cert-authority * %s", strings.Trim(string(expectedCA2Bytes), "\n")) 474 | 475 | require.Containsf(t, receivedCAs, string(expectedCA1), "Request body from /authority unexpectedly returned '%s', which didn't contain '%s'", string(body), expectedCA1) 476 | require.Containsf(t, receivedCAs, string(expectedCA2), "Request body from /authority unexpectedly returned '%s', which didn't contain '%s'", string(body), expectedCA2) 477 | } 478 | 479 | func TestGetKnownHosts(t *testing.T) { 480 | c, err := generateContext(t) 481 | require.NoError(t, err) 482 | 483 | pubkey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(testKey)) 484 | require.NoError(t, err) 485 | 486 | _, err = c.storage.RecordIssuance(ssh.HostCert, "hostname", pubkey) 487 | require.NoError(t, err) 488 | 489 | result, err := c.GetKnownHosts() 490 | require.NoError(t, err) 491 | 492 | results := strings.Split(result, "\n") 493 | assert.EqualValues(t, 3, len(results)) 494 | assert.Equal(t, "@certificate-authority * pubkey", results[0]) 495 | assert.Equal(t, "hostname "+testKey, results[1]) 496 | assert.Equal(t, "", results[2]) 497 | } 498 | 499 | func TestStatus(t *testing.T) { 500 | c, err := generateContext(t) 501 | require.NoError(t, err) 502 | 503 | req, err := generateHostRequest(goodName) 504 | require.NoError(t, err) 505 | 506 | rec := httptest.NewRecorder() 507 | c.Status(rec, req) 508 | 509 | rec.Flush() 510 | require.Equalf(t, 200, rec.Code, "Request to /_status failed with %d", rec.Code) 511 | 512 | body, _ := io.ReadAll(rec.Body) 513 | expected := []byte(`{"ok":true,"status":"ok","messages":[]}`) 514 | 515 | require.Equalf(t, expected, body, 516 | "Request body from /_status unexpectedly returned '%s'", string(body)) 517 | 518 | require.Equalf(t, "application/json", rec.Header().Get("Content-Type"), 519 | "Expected Content-Type to be set to 'application/json', but instead got %s", rec.Header().Get("Content-Type")) 520 | } 521 | 522 | func generateContext(t *testing.T) (*Api, error) { 523 | conf := &config.Config{ 524 | SigningKey: "testdata/server_ca", 525 | HostCertDuration: "160h", 526 | UserCertDuration: "8h", 527 | Aliases: map[string][]string{ 528 | "hostname.square": []string{"alias.square"}, 529 | }, 530 | ExtraKnownHosts: []string{ 531 | "@certificate-authority * pubkey", 532 | }, 533 | } 534 | 535 | key, err := os.ReadFile(conf.SigningKey) 536 | require.NoError(t, err) 537 | 538 | logger := logrus.New() 539 | 540 | // in-memory sqlite: see https://github.com/mattn/go-sqlite3 for address docs 541 | sqlite, err := storage.NewSqlite(config.Database{Address: ":memory:"}) 542 | require.NoError(t, err) 543 | err = sqlite.Migrate("../../../db/sqlite/migrations") 544 | require.NoError(t, err) 545 | 546 | sshSigner, err := ssh.ParsePrivateKey(key) 547 | require.NoError(t, err) 548 | 549 | signer := cert.NewSigner(sshSigner, conf, sqlite) 550 | 551 | c := &Api{ 552 | signer: signer, 553 | storage: sqlite, 554 | conf: conf, 555 | logger: logger, 556 | } 557 | 558 | return c, nil 559 | } 560 | 561 | func addExtraAuthorities(t *testing.T, a *Api) *Api { 562 | extraCABytes, err := os.ReadFile("testdata/next_server_ca.pub") 563 | require.NoError(t, err, "Error reading testdata") 564 | a.conf.ExtraAuthorities = []string{(string(extraCABytes))} 565 | return a 566 | } 567 | 568 | func generateRequest(hostname string, body io.ReadCloser) (*http.Request, error) { 569 | cert := x509.Certificate{ 570 | DNSNames: []string{hostname}, 571 | } 572 | chain := [][]*x509.Certificate{{&cert}} 573 | conn := tls.ConnectionState{ 574 | VerifiedChains: chain, 575 | } 576 | 577 | request := http.Request{ 578 | TLS: &conn, 579 | Body: body, 580 | Header: http.Header{}, 581 | } 582 | return &request, nil 583 | } 584 | 585 | func parseDERFromPEM(pemDataId string, blockType string) (*pem.Block, error) { 586 | bytes, err := os.ReadFile(pemDataId) 587 | if err != nil { 588 | return nil, err 589 | } 590 | 591 | var block *pem.Block 592 | for len(bytes) > 0 { 593 | block, bytes = pem.Decode(bytes) 594 | if block == nil { 595 | return nil, errors.New("unable to parse PEM data") 596 | } 597 | if block.Type == blockType { 598 | return block, nil 599 | } 600 | } 601 | return nil, errors.New("requested block type could not be found") 602 | } 603 | 604 | func generateSpiffeRequest(hostname string, proxyPath string, body io.ReadCloser) (*http.Request, error) { 605 | block, err := parseDERFromPEM(proxyPath, "CERTIFICATE") 606 | if err != nil { 607 | return nil, err 608 | } 609 | 610 | cert, err := x509.ParseCertificate(block.Bytes) 611 | if err != nil { 612 | return nil, err 613 | } 614 | 615 | if len(hostname) > 0 { 616 | cert.DNSNames = []string{hostname} 617 | } 618 | 619 | chain := [][]*x509.Certificate{{cert}} 620 | conn := tls.ConnectionState{ 621 | VerifiedChains: chain, 622 | } 623 | 624 | request := http.Request{ 625 | TLS: &conn, 626 | Body: body, 627 | Header: http.Header{}, 628 | } 629 | return &request, nil 630 | } 631 | 632 | func generateUserRequest(commonName string) (*http.Request, error) { 633 | key, err := os.Open("testdata/ssh_alice_rsa.pub") 634 | if err != nil { 635 | return nil, err 636 | } 637 | return generateRequest(commonName, io.NopCloser(key)) 638 | } 639 | 640 | func generateSpiffeUserRequest(commonName string, proxyPath string) (*http.Request, error) { 641 | key, err := os.Open("testdata/ssh_alice_rsa.pub") 642 | if err != nil { 643 | return nil, err 644 | } 645 | return generateSpiffeRequest(commonName, proxyPath, io.NopCloser(key)) 646 | } 647 | 648 | func generateHostRequest(hostname string) (*http.Request, error) { 649 | key, err := os.Open("testdata/ssh_host_rsa_key.pub") 650 | if err != nil { 651 | return nil, err 652 | } 653 | return generateRequest(hostname, io.NopCloser(key)) 654 | } 655 | --------------------------------------------------------------------------------