Why can git clone work anonymously over HTTPS without an account but not over SSH on GitHub

intro

There are mainly two ways to clone a repo from github: over HTTPS and ssh. A subtle difference can be noticed that the ssh address is not available if you are not logged in github. Also, when ssh doesn't work out, HTTPS seems to be a more robust option.

HTTPS

Configure the apache http server to enable anonymous read access but authenticated write access.

When the client try to do a git push without authentication, the https server will respond with a 401 error code.

Git will get this error code from libcurl which is used to faciliate the http protocol.

///@file:http.c
static int handle_curl_result(struct slot_results *results)
{
///...
	else if (results->http_code == 401) {
		if ((http_auth.username && http_auth.password) ||\
		    (http_auth.authtype && http_auth.credential)) {
			if (http_auth.multistage) {
				credential_clear_secrets(&http_auth);
				return HTTP_REAUTH;
			}
			credential_reject(the_repository, &http_auth);
			if (always_auth_proactively())
				http_proactive_auth = PROACTIVE_AUTH_NONE;
			return HTTP_NOAUTH;
		} else {
			http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE;
			if (results->auth_avail) {
				http_auth_methods &= results->auth_avail;
				http_auth_methods_restricted = 1;
			}
			return HTTP_REAUTH;
		}
	} 
///...
}

The http error code is checked constently to handle the authentication issue in http_request_reauth.

///@file: http.c
static int http_request_reauth(const char *url,
			       void *result, int target,
			       struct http_get_options *options)
{
	int i = 3;
	int ret;

	if (always_auth_proactively())
		credential_fill(the_repository, &http_auth, 1);

	ret = http_request(url, result, target, options);

	if (ret != HTTP_OK && ret != HTTP_REAUTH)
		return ret;

	if (options && options->effective_url && options->base_url) {
		if (update_url_from_redirect(options->base_url,
					     url, options->effective_url)) {
			credential_from_url(&http_auth, options->base_url->buf);
			url = options->effective_url->buf;
		}
	}

	while (ret == HTTP_REAUTH && --i) {
		/*
		 * The previous request may have put cruft into our output stream; we
		 * should clear it out before making our next request.
		 */
		switch (target) {
		case HTTP_REQUEST_STRBUF:
			strbuf_reset(result);
			break;
		case HTTP_REQUEST_FILE: {
			FILE *f = result;
			if (fflush(f)) {
				error_errno("unable to flush a file");
				return HTTP_START_FAILED;
			}
			rewind(f);
			if (ftruncate(fileno(f), 0) < 0) {
				error_errno("unable to truncate a file");
				return HTTP_START_FAILED;
			}
			break;
		}
		default:
			BUG("Unknown http_request target");
		}

		credential_fill(the_repository, &http_auth, 1);

		ret = http_request(url, result, target, options);
	}
	return ret;
}

The "Username for 'https://github.com':" will be prompted to git user to input username and password. After that, the authentication will go on as authentication.

ssh

The RFC for ssh authentication protocol is rfc4252. The authentication takes place right after the key exechange. At this point, the ssh server has not any clue about what the command is going to be laucnhed on ther server, so it cann't determine which access level is required for this connection.

There are two options: be optimisitic or pessimistic. If we assume it's a public repo clone request, so we can allow the "none" authentication request, which is absolutely very risky. Besides, if the unauthenticated user trigger a denied push request due to permission deny, there is no way in ssh protocol to notify the client this issue, which will make the client has no chance to retry even it wants to.

///@file:sshconnect2.c
static int
input_userauth_service_accept(int type, u_int32_t seq, struct ssh *ssh)
{
	int r;

	if (ssh_packet_remaining(ssh) > 0) {
		char *reply;

		if ((r = sshpkt_get_cstring(ssh, &reply, NULL)) != 0)
			goto out;
		debug2("service_accept: %s", reply);
		free(reply);
	} else {
		debug2("buggy server: service_accept w/o service");
	}
	if ((r = sshpkt_get_end(ssh)) != 0)
		goto out;
	debug("SSH2_MSG_SERVICE_ACCEPT received");

	/* initial userauth request */
	userauth_none(ssh);

	/* accept EXT_INFO at any time during userauth */
	ssh_dispatch_set(ssh, SSH2_MSG_EXT_INFO, ssh->kex->ext_info_s ?
	    &kex_input_ext_info : &input_userauth_error);
	ssh_dispatch_set(ssh, SSH2_MSG_USERAUTH_SUCCESS, &input_userauth_success);
	ssh_dispatch_set(ssh, SSH2_MSG_USERAUTH_FAILURE, &input_userauth_failure);
	ssh_dispatch_set(ssh, SSH2_MSG_USERAUTH_BANNER, &input_userauth_banner);
	r = 0;
 out:
	return r;
}
///...

static int
userauth_none(struct ssh *ssh)
{
	Authctxt *authctxt = (Authctxt *)ssh->authctxt;
	int r;

	/* initial userauth request */
	if ((r = sshpkt_start(ssh, SSH2_MSG_USERAUTH_REQUEST)) != 0 ||
	    (r = sshpkt_put_cstring(ssh, authctxt->server_user)) != 0 ||
	    (r = sshpkt_put_cstring(ssh, authctxt->service)) != 0 ||
	    (r = sshpkt_put_cstring(ssh, authctxt->method->name)) != 0 ||
	    (r = sshpkt_send(ssh)) != 0)
		fatal_fr(r, "send packet");
	return 1;
}

outro

From my perspective, the two protocols are designed for different intention: the ssh is supposed to be able to fully interact with the remote server, typically used for remote administration with standard shell.

On the contratry, HTTPS is based on HTTP, it typically deals with static pages or specific CGIs. its behavior can be easily restricted.

"With great power comes great responsibility". Also, "With great power needs strict limination" 😃.

posted on 2026-01-27 22:26  tsecer  阅读(6)  评论(0)    收藏  举报

导航