diff --git a/config/custom-environment-variables.yml b/config/custom-environment-variables.yml
index 098d3fe0a86fa17ab294d98b5ab1f05541a4f211..468c4bf5de7a17d35ab74727df36925d88c117e7 100644
--- a/config/custom-environment-variables.yml
+++ b/config/custom-environment-variables.yml
@@ -25,11 +25,27 @@ public:
     dir: 'PERSISTENCE_DIR'
 
   services:
+    bitbucketServer:
+      authorizedOrigins: 'BITBUCKET_SERVER_ORIGINS'
+    drone:
+      authorizedOrigins: 'DRONE_ORIGINS'
     github:
       baseUri: 'GITHUB_URL'
       debug:
         enabled: 'GITHUB_DEBUG_ENABLED'
         intervalSeconds: 'GITHUB_DEBUG_INTERVAL_SECONDS'
+    jenkins:
+      authorizedOrigins: 'JENKINS_ORIGINS'
+    jira:
+      authorizedOrigins: 'JIRA_ORIGINS'
+    nexus:
+      authorizedOrigins: 'NEXUS_ORIGINS'
+    npm:
+      authorizedOrigins: 'NPM_ORIGINS'
+    sonar:
+      authorizedOrigins: 'SONAR_ORIGINS'
+    teamcity:
+      authorizedOrigins: 'TEAMCITY_ORIGINS'
     trace: 'TRACE_SERVICES'
 
   profiling:
@@ -46,6 +62,10 @@ private:
   azure_devops_token: 'AZURE_DEVOPS_TOKEN'
   bintray_user: 'BINTRAY_USER'
   bintray_apikey: 'BINTRAY_API_KEY'
+  bitbucket_username: 'BITBUCKET_USER'
+  bitbucket_password: 'BITBUCKET_PASS'
+  bitbucket_server_username: 'BITBUCKET_SERVER_USER'
+  bitbucket_server_password: 'BITBUCKET_SERVER_PASS'
   drone_token: 'DRONE_TOKEN'
   gh_client_id: 'GH_CLIENT_ID'
   gh_client_secret: 'GH_CLIENT_SECRET'
@@ -63,6 +83,8 @@ private:
   sl_insight_userUuid: 'SL_INSIGHT_USER_UUID'
   sl_insight_apiToken: 'SL_INSIGHT_API_TOKEN'
   sonarqube_token: 'SONARQUBE_TOKEN'
+  teamcity_user: 'TEAMCITY_USER'
+  teamcity_pass: 'TEAMCITY_PASS'
   twitch_client_id: 'TWITCH_CLIENT_ID'
   twitch_client_secret: 'TWITCH_CLIENT_SECRET'
   wheelmap_token: 'WHEELMAP_TOKEN'
diff --git a/core/base-service/auth-helper.js b/core/base-service/auth-helper.js
index bb61cd01d544ed69a25692efdeb71588371bebdf..6dedf1ba9e11e3bb864af4c7cd44cc52aa9fc72f 100644
--- a/core/base-service/auth-helper.js
+++ b/core/base-service/auth-helper.js
@@ -1,34 +1,68 @@
 'use strict'
 
+const { URL } = require('url')
+const { InvalidParameter } = require('./errors')
+
 class AuthHelper {
   constructor(
     {
       userKey,
       passKey,
+      authorizedOrigins,
+      serviceKey,
       isRequired = false,
       defaultToEmptyStringForUser = false,
     },
-    privateConfig
+    config
   ) {
     if (!userKey && !passKey) {
       throw Error('Expected userKey or passKey to be set')
     }
 
+    if (!authorizedOrigins && !serviceKey) {
+      throw Error('Expected authorizedOrigins or serviceKey to be set')
+    }
+
     this._userKey = userKey
     this._passKey = passKey
     if (userKey) {
-      this.user = privateConfig[userKey]
+      this._user = config.private[userKey]
     } else {
-      this.user = defaultToEmptyStringForUser ? '' : undefined
+      this._user = defaultToEmptyStringForUser ? '' : undefined
     }
-    this.pass = passKey ? privateConfig[passKey] : undefined
+    this._pass = passKey ? config.private[passKey] : undefined
     this.isRequired = isRequired
+
+    if (serviceKey !== undefined && !(serviceKey in config.public.services)) {
+      // Keep this as its own error, as it's useful to the programmer as they're
+      // getting auth set up.
+      throw Error(`Service key ${serviceKey} was missing from config schema`)
+    }
+
+    let requireStrictSsl, requireStrictSslToAuthenticate
+    if (serviceKey === undefined) {
+      requireStrictSsl = true
+      requireStrictSslToAuthenticate = true
+    } else {
+      ;({
+        authorizedOrigins,
+        requireStrictSsl = true,
+        requireStrictSslToAuthenticate = true,
+      } = config.public.services[serviceKey])
+    }
+    if (!Array.isArray(authorizedOrigins)) {
+      throw Error('Expected authorizedOrigins to be an array of origins')
+    }
+    this._authorizedOrigins = authorizedOrigins
+    this._requireStrictSsl = requireStrictSsl
+    this._requireStrictSslToAuthenticate = requireStrictSslToAuthenticate
   }
 
   get isConfigured() {
     return (
-      (this._userKey ? Boolean(this.user) : true) &&
-      (this._passKey ? Boolean(this.pass) : true)
+      this._authorizedOrigins.length > 0 &&
+      (this._userKey ? Boolean(this._user) : true) &&
+      (this._passKey ? Boolean(this._pass) : true)
     )
   }
 
@@ -36,20 +70,135 @@ class AuthHelper {
     if (this.isRequired) {
       return this.isConfigured
     } else {
-      const configIsEmpty = !this.user && !this.pass
+      const configIsEmpty = !this._user && !this._pass
       return this.isConfigured || configIsEmpty
     }
   }
 
-  get basicAuth() {
-    const { user, pass } = this
+  static _isInsecureSslRequest({ options = {} }) {
+    const { strictSSL = true } = options
+    return strictSSL !== true
+  }
+
+  enforceStrictSsl({ options = {} }) {
+    if (
+      this._requireStrictSsl &&
+      this.constructor._isInsecureSslRequest({ options })
+    ) {
+      throw new InvalidParameter({ prettyMessage: 'strict ssl is required' })
+    }
+  }
+
+  shouldAuthenticateRequest({ url, options = {} }) {
+    let parsed
+    try {
+      parsed = new URL(url)
+    } catch (e) {
+      throw new InvalidParameter({ prettyMessage: 'invalid url parameter' })
+    }
+
+    const { protocol, host } = parsed
+    const origin = `${protocol}//${host}`
+    const originViolation = !this._authorizedOrigins.includes(origin)
+
+    const strictSslCheckViolation =
+      this._requireStrictSslToAuthenticate &&
+      this.constructor._isInsecureSslRequest({ options })
+
+    return this.isConfigured && !originViolation && !strictSslCheckViolation
+  }
+
+  get _basicAuth() {
+    const { _user: user, _pass: pass } = this
     return this.isConfigured ? { user, pass } : undefined
   }
 
-  get bearerAuthHeader() {
-    const { pass } = this
+  /*
+   * Helper function for `withBasicAuth()` and friends.
+   */
+  _withAnyAuth(requestParams, mergeAuthFn) {
+    this.enforceStrictSsl(requestParams)
+
+    const shouldAuthenticate = this.shouldAuthenticateRequest(requestParams)
+    if (this.isRequired && !shouldAuthenticate) {
+      throw new InvalidParameter({
+        prettyMessage: 'requested origin not authorized',
+      })
+    }
+
+    return shouldAuthenticate ? mergeAuthFn(requestParams) : requestParams
+  }
+
+  static _mergeAuth(requestParams, auth) {
+    const { options, ...rest } = requestParams
+    return {
+      options: {
+        auth,
+        ...options,
+      },
+      ...rest,
+    }
+  }
+
+  withBasicAuth(requestParams) {
+    return this._withAnyAuth(requestParams, requestParams =>
+      this.constructor._mergeAuth(requestParams, this._basicAuth)
+    )
+  }
+
+  get _bearerAuthHeader() {
+    const { _pass: pass } = this
     return this.isConfigured ? { Authorization: `Bearer ${pass}` } : undefined
   }
+
+  static _mergeHeaders(requestParams, headers) {
+    const {
+      options: { headers: existingHeaders, ...restOptions } = {},
+      ...rest
+    } = requestParams
+    return {
+      options: {
+        headers: {
+          ...existingHeaders,
+          ...headers,
+        },
+        ...restOptions,
+      },
+      ...rest,
+    }
+  }
+
+  withBearerAuthHeader(requestParams) {
+    return this._withAnyAuth(requestParams, requestParams =>
+      this.constructor._mergeHeaders(requestParams, this._bearerAuthHeader)
+    )
+  }
+
+  static _mergeQueryParams(requestParams, query) {
+    const {
+      options: { qs: existingQuery, ...restOptions } = {},
+      ...rest
+    } = requestParams
+    return {
+      options: {
+        qs: {
+          ...existingQuery,
+          ...query,
+        },
+        ...restOptions,
+      },
+      ...rest,
+    }
+  }
+
+  withQueryStringAuth({ userKey, passKey }, requestParams) {
+    return this._withAnyAuth(requestParams, requestParams =>
+      this.constructor._mergeQueryParams(requestParams, {
+        ...(userKey ? { [userKey]: this._user } : undefined),
+        ...(passKey ? { [passKey]: this._pass } : undefined),
+      })
+    )
+  }
 }
 
 module.exports = { AuthHelper }
diff --git a/core/base-service/auth-helper.spec.js b/core/base-service/auth-helper.spec.js
index cd66066bcf0feba0e3a3476cb320bebd712a7b67..bec321dde1cf0c52445e02f0c19d3955bba1cf34 100644
--- a/core/base-service/auth-helper.spec.js
+++ b/core/base-service/auth-helper.spec.js
@@ -3,18 +3,42 @@
 const { expect } = require('chai')
 const { test, given, forCases } = require('sazerac')
 const { AuthHelper } = require('./auth-helper')
+const { InvalidParameter } = require('./errors')
 
 describe('AuthHelper', function() {
-  it('throws without userKey or passKey', function() {
-    expect(() => new AuthHelper({}, {})).to.throw(
-      Error,
-      'Expected userKey or passKey to be set'
-    )
+  describe('constructor checks', function() {
+    it('throws without userKey or passKey', function() {
+      expect(() => new AuthHelper({}, {})).to.throw(
+        Error,
+        'Expected userKey or passKey to be set'
+      )
+    })
+    it('throws without serviceKey or authorizedOrigins', function() {
+      expect(
+        () => new AuthHelper({ userKey: 'myci_user', passKey: 'myci_pass' }, {})
+      ).to.throw(Error, 'Expected authorizedOrigins or serviceKey to be set')
+    })
+    it('throws when authorizedOrigins is not an array', function() {
+      expect(
+        () =>
+          new AuthHelper(
+            {
+              userKey: 'myci_user',
+              passKey: 'myci_pass',
+              authorizedOrigins: true,
+            },
+            { private: {} }
+          )
+      ).to.throw(Error, 'Expected authorizedOrigins to be an array of origins')
+    })
   })
 
   describe('isValid', function() {
     function validate(config, privateConfig) {
-      return new AuthHelper(config, privateConfig).isValid
+      return new AuthHelper(
+        { authorizedOrigins: ['https://example.test'], ...config },
+        { private: privateConfig }
+      ).isValid
     }
     test(validate, () => {
       forCases([
@@ -65,9 +89,12 @@ describe('AuthHelper', function() {
     })
   })
 
-  describe('basicAuth', function() {
+  describe('_basicAuth', function() {
     function validate(config, privateConfig) {
-      return new AuthHelper(config, privateConfig).basicAuth
+      return new AuthHelper(
+        { authorizedOrigins: ['https://example.test'], ...config },
+        { private: privateConfig }
+      )._basicAuth
     }
     test(validate, () => {
       forCases([
@@ -100,4 +127,250 @@ describe('AuthHelper', function() {
       })
     })
   })
+
+  describe('_isInsecureSslRequest', function() {
+    test(AuthHelper._isInsecureSslRequest, () => {
+      forCases([
+        given({ url: 'http://example.test' }),
+        given({ url: 'http://example.test', options: {} }),
+        given({ url: 'http://example.test', options: { strictSSL: true } }),
+        given({
+          url: 'http://example.test',
+          options: { strictSSL: undefined },
+        }),
+      ]).expect(false)
+      given({
+        url: 'http://example.test',
+        options: { strictSSL: false },
+      }).expect(true)
+    })
+  })
+
+  describe('enforceStrictSsl', function() {
+    const authConfig = {
+      userKey: 'myci_user',
+      passKey: 'myci_pass',
+      serviceKey: 'myci',
+    }
+
+    context('by default', function() {
+      const authHelper = new AuthHelper(authConfig, {
+        public: {
+          services: { myci: { authorizedOrigins: ['http://myci.test'] } },
+        },
+        private: { myci_user: 'admin', myci_pass: 'abc123' },
+      })
+      it('does not throw for secure requests', function() {
+        expect(() => authHelper.enforceStrictSsl({})).not.to.throw()
+      })
+      it('throws for insecure requests', function() {
+        expect(() =>
+          authHelper.enforceStrictSsl({ options: { strictSSL: false } })
+        ).to.throw(InvalidParameter)
+      })
+    })
+
+    context("when strict SSL isn't required", function() {
+      const authHelper = new AuthHelper(authConfig, {
+        public: {
+          services: {
+            myci: {
+              authorizedOrigins: ['http://myci.test'],
+              requireStrictSsl: false,
+            },
+          },
+        },
+        private: { myci_user: 'admin', myci_pass: 'abc123' },
+      })
+      it('does not throw for secure requests', function() {
+        expect(() => authHelper.enforceStrictSsl({})).not.to.throw()
+      })
+      it('does not throw for insecure requests', function() {
+        expect(() =>
+          authHelper.enforceStrictSsl({ options: { strictSSL: false } })
+        ).not.to.throw()
+      })
+    })
+  })
+
+  describe('shouldAuthenticateRequest', function() {
+    const authConfig = {
+      userKey: 'myci_user',
+      passKey: 'myci_pass',
+      serviceKey: 'myci',
+    }
+
+    context('by default', function() {
+      const authHelper = new AuthHelper(authConfig, {
+        public: {
+          services: {
+            myci: {
+              authorizedOrigins: ['https://myci.test'],
+            },
+          },
+        },
+        private: { myci_user: 'admin', myci_pass: 'abc123' },
+      })
+      const shouldAuthenticateRequest = requestOptions =>
+        authHelper.shouldAuthenticateRequest(requestOptions)
+      describe('a secure request to an authorized origin', function() {
+        test(shouldAuthenticateRequest, () => {
+          given({ url: 'https://myci.test/api' }).expect(true)
+        })
+      })
+      describe('an insecure request', function() {
+        test(shouldAuthenticateRequest, () => {
+          given({
+            url: 'https://myci.test/api',
+            options: { strictSSL: false },
+          }).expect(false)
+        })
+      })
+      describe('a request to an unauthorized origin', function() {
+        test(shouldAuthenticateRequest, () => {
+          forCases([
+            given({ url: 'http://myci.test/api' }),
+            given({ url: 'https://myci.test:12345/api' }),
+            given({ url: 'https://other.test/api' }),
+          ]).expect(false)
+        })
+      })
+    })
+
+    context('when auth over insecure SSL is allowed', function() {
+      const authHelper = new AuthHelper(authConfig, {
+        public: {
+          services: {
+            myci: {
+              authorizedOrigins: ['https://myci.test'],
+              requireStrictSslToAuthenticate: false,
+            },
+          },
+        },
+        private: { myci_user: 'admin', myci_pass: 'abc123' },
+      })
+      const shouldAuthenticateRequest = requestOptions =>
+        authHelper.shouldAuthenticateRequest(requestOptions)
+      describe('a secure request to an authorized origin', function() {
+        test(shouldAuthenticateRequest, () => {
+          given({ url: 'https://myci.test' }).expect(true)
+        })
+      })
+      describe('an insecure request', function() {
+        test(shouldAuthenticateRequest, () => {
+          given({
+            url: 'https://myci.test',
+            options: { strictSSL: false },
+          }).expect(true)
+        })
+      })
+      describe('a request to an unauthorized origin', function() {
+        test(shouldAuthenticateRequest, () => {
+          forCases([
+            given({ url: 'http://myci.test' }),
+            given({ url: 'https://myci.test:12345/' }),
+            given({ url: 'https://other.test' }),
+          ]).expect(false)
+        })
+      })
+    })
+
+    context('when the service is partly configured', function() {
+      const authHelper = new AuthHelper(authConfig, {
+        public: {
+          services: {
+            myci: {
+              authorizedOrigins: ['https://myci.test'],
+              requireStrictSslToAuthenticate: false,
+            },
+          },
+        },
+        private: { myci_user: 'admin' },
+      })
+      const shouldAuthenticateRequest = requestOptions =>
+        authHelper.shouldAuthenticateRequest(requestOptions)
+      describe('a secure request to an authorized origin', function() {
+        test(shouldAuthenticateRequest, () => {
+          given({ url: 'https://myci.test' }).expect(false)
+        })
+      })
+    })
+  })
+
+  describe('withBasicAuth', function() {
+    const authHelper = new AuthHelper(
+      {
+        userKey: 'myci_user',
+        passKey: 'myci_pass',
+        serviceKey: 'myci',
+      },
+      {
+        public: {
+          services: {
+            myci: {
+              authorizedOrigins: ['https://myci.test'],
+            },
+          },
+        },
+        private: { myci_user: 'admin', myci_pass: 'abc123' },
+      }
+    )
+    const withBasicAuth = requestOptions =>
+      authHelper.withBasicAuth(requestOptions)
+
+    describe('authenticates a secure request to an authorized origin', function() {
+      test(withBasicAuth, () => {
+        given({
+          url: 'https://myci.test/api',
+        }).expect({
+          url: 'https://myci.test/api',
+          options: {
+            auth: { user: 'admin', pass: 'abc123' },
+          },
+        })
+        given({
+          url: 'https://myci.test/api',
+          options: {
+            headers: { Accept: 'application/json' },
+          },
+        }).expect({
+          url: 'https://myci.test/api',
+          options: {
+            headers: { Accept: 'application/json' },
+            auth: { user: 'admin', pass: 'abc123' },
+          },
+        })
+      })
+    })
+
+    describe('does not authenticate a request to an unauthorized origin', function() {
+      test(withBasicAuth, () => {
+        given({
+          url: 'https://other.test/api',
+        }).expect({
+          url: 'https://other.test/api',
+        })
+        given({
+          url: 'https://other.test/api',
+          options: {
+            headers: { Accept: 'application/json' },
+          },
+        }).expect({
+          url: 'https://other.test/api',
+          options: {
+            headers: { Accept: 'application/json' },
+          },
+        })
+      })
+    })
+
+    describe('throws on an insecure SSL request', function() {
+      expect(() =>
+        withBasicAuth({
+          url: 'https://myci.test/api',
+          options: { strictSSL: false },
+        })
+      ).to.throw(InvalidParameter)
+    })
+  })
 })
diff --git a/core/base-service/base.js b/core/base-service/base.js
index 2d4dfdb644571acd29bee0f9af52a8060c876717..db671e1c1dd6405e407a0991e5440d576a7d2494 100644
--- a/core/base-service/base.js
+++ b/core/base-service/base.js
@@ -359,9 +359,7 @@ class BaseService {
     // Like the service instance, the auth helper could be reused for each request.
     // However, moving its instantiation to `register()` makes `invoke()` harder
     // to test.
-    const authHelper = this.auth
-      ? new AuthHelper(this.auth, config.private)
-      : undefined
+    const authHelper = this.auth ? new AuthHelper(this.auth, config) : undefined
 
     const serviceInstance = new this({ ...context, authHelper }, config)
 
diff --git a/core/base-service/base.spec.js b/core/base-service/base.spec.js
index 06d9cfe0d3f66e4a53ef3d6974fe7777b9faa2f7..ae4879630819c355f2abb8591aeaecc6905032ca 100644
--- a/core/base-service/base.spec.js
+++ b/core/base-service/base.spec.js
@@ -74,7 +74,13 @@ class DummyServiceWithServiceResponseSizeMetricEnabled extends DummyService {
 }
 
 describe('BaseService', function() {
-  const defaultConfig = { handleInternalErrors: false, private: {} }
+  const defaultConfig = {
+    public: {
+      handleInternalErrors: false,
+      services: {},
+    },
+    private: {},
+  }
 
   it('Invokes the handler as expected', async function() {
     expect(
@@ -564,13 +570,14 @@ describe('BaseService', function() {
       static get auth() {
         return {
           passKey: 'myci_pass',
+          serviceKey: 'myci',
           isRequired: true,
         }
       }
 
       async handle() {
         return {
-          message: `The CI password is ${this.authHelper.pass}`,
+          message: `The CI password is ${this.authHelper._pass}`,
         }
       }
     }
@@ -579,7 +586,13 @@ describe('BaseService', function() {
       expect(
         await AuthService.invoke(
           {},
-          { defaultConfig, private: { myci_pass: 'abc123' } },
+          {
+            public: {
+              ...defaultConfig.public,
+              services: { myci: { authorizedOrigins: ['https://myci.test'] } },
+            },
+            private: { myci_pass: 'abc123' },
+          },
           { namedParamA: 'bar.bar.bar' }
         )
       ).to.deep.equal({ message: 'The CI password is abc123' })
@@ -587,9 +600,19 @@ describe('BaseService', function() {
 
     it('when auth is not configured properly, invoke() returns inacessible', async function() {
       expect(
-        await AuthService.invoke({}, defaultConfig, {
-          namedParamA: 'bar.bar.bar',
-        })
+        await AuthService.invoke(
+          {},
+          {
+            public: {
+              ...defaultConfig.public,
+              services: { myci: { authorizedOrigins: ['https://myci.test'] } },
+            },
+            private: {},
+          },
+          {
+            namedParamA: 'bar.bar.bar',
+          }
+        )
       ).to.deep.equal({
         color: 'lightgray',
         isError: true,
diff --git a/core/base-service/legacy-request-handler.js b/core/base-service/legacy-request-handler.js
index 91e2d394d774f805495c195534194ea48a735f61..f14ff236a72e2a6ef6be45d3634706d45237e26a 100644
--- a/core/base-service/legacy-request-handler.js
+++ b/core/base-service/legacy-request-handler.js
@@ -13,6 +13,8 @@ const { makeSend } = require('./legacy-result-sender')
 const LruCache = require('./lru-cache')
 const coalesceBadge = require('./coalesce-badge')
 
+const userAgent = 'Shields.io/2003a'
+
 // We avoid calling the vendor's server for computation of the information in a
 // number of badges.
 const minAccuracy = 0.75
@@ -204,8 +206,7 @@ function handleRequest(cacheHeaderConfig, handlerOptions) {
         options = uri
       }
       options.headers = options.headers || {}
-      options.headers['User-Agent'] =
-        options.headers['User-Agent'] || 'Shields.io'
+      options.headers['User-Agent'] = userAgent
 
       let bufferLength = 0
       const r = request(options, (err, res, body) => {
@@ -294,4 +295,5 @@ module.exports = {
   clearRequestCache,
   // Expose for testing.
   _requestCache: requestCache,
+  userAgent,
 }
diff --git a/core/server/server.js b/core/server/server.js
index b87dbd6ddb11b88a488a1e2e4b64d7fd672f8f89..b4843e420bd2bea0d610903759dbeb14779362f3 100644
--- a/core/server/server.js
+++ b/core/server/server.js
@@ -5,9 +5,10 @@
 
 const path = require('path')
 const url = require('url')
+const { URL } = url
 const bytes = require('bytes')
-const Joi = require('@hapi/joi')
 const Camp = require('camp')
+const originalJoi = require('@hapi/joi')
 const makeBadge = require('../../gh-badges/lib/make-badge')
 const GithubConstellation = require('../../services/github/github-constellation')
 const suggest = require('../../services/suggest')
@@ -23,8 +24,44 @@ const log = require('./log')
 const sysMonitor = require('./monitor')
 const PrometheusMetrics = require('./prometheus-metrics')
 
+const Joi = originalJoi
+  .extend(base => ({
+    type: 'arrayFromString',
+    base: base.array(),
+    coerce: (value, state, options) => ({
+      value: typeof value === 'string' ? value.split(' ') : value,
+    }),
+  }))
+  .extend(base => ({
+    type: 'string',
+    base: base.string(),
+    messages: {
+      'string.origin':
+        'needs to be an origin string, e.g. https://host.domain with optional port and no trailing slash',
+    },
+    rules: {
+      origin: {
+        validate(value, helpers) {
+          let origin
+          try {
+            ;({ origin } = new URL(value))
+          } catch (e) {}
+          if (origin !== undefined && origin === value) {
+            return value
+          } else {
+            return helpers.error('string.origin')
+          }
+        },
+      },
+    },
+  }))
+
 const optionalUrl = Joi.string().uri({ scheme: ['http', 'https'] })
 const requiredUrl = optionalUrl.required()
+const origins = Joi.arrayFromString().items(Joi.string().origin())
+const defaultService = Joi.object({ authorizedOrigins: origins }).default({
+  authorizedOrigins: [],
+})
 
 const publicConfigSchema = Joi.object({
   bind: {
@@ -61,7 +98,9 @@ const publicConfigSchema = Joi.object({
   persistence: {
     dir: Joi.string().required(),
   },
-  services: {
+  services: Joi.object({
+    bitbucketServer: defaultService,
+    drone: defaultService,
     github: {
       baseUri: requiredUrl,
       debug: {
@@ -72,8 +111,18 @@ const publicConfigSchema = Joi.object({
           .required(),
       },
     },
+    jira: defaultService,
+    jenkins: Joi.object({
+      authorizedOrigins: origins,
+      requireStrictSsl: Joi.boolean(),
+      requireStrictSslToAuthenticate: Joi.boolean(),
+    }).default({ authorizedOrigins: [] }),
+    nexus: defaultService,
+    npm: defaultService,
+    sonar: defaultService,
+    teamcity: defaultService,
     trace: Joi.boolean().required(),
-  },
+  }).required(),
   profiling: {
     makeBadge: Joi.boolean().required(),
   },
@@ -296,6 +345,7 @@ class Server {
           fetchLimitBytes: bytes(config.public.fetchLimit),
           rasterUrl: config.public.rasterUrl,
           private: config.private,
+          public: config.public,
         }
       )
     )
diff --git a/core/server/server.spec.js b/core/server/server.spec.js
index 7150b7e36d8e50fa2f6900312eed3d3e678fcb29..177b12f5c7ee229c815bb65ed720584aad83f415 100644
--- a/core/server/server.spec.js
+++ b/core/server/server.spec.js
@@ -65,7 +65,7 @@ describe('The server', function() {
 
   it('should produce json badges', async function() {
     const { statusCode, body, headers } = await got(
-      `${baseUrl}npm/v/express.json`
+      `${baseUrl}twitter/follow/_Pyves.json`
     )
     expect(statusCode).to.equal(200)
     expect(headers['content-type']).to.equal('application/json')
diff --git a/doc/server-secrets.md b/doc/server-secrets.md
index aeffa08f79ef43e8f9a48d33699830c5eccf74c7..1f75a12d027de4e13db7d2e0f175a96a0be91077 100644
--- a/doc/server-secrets.md
+++ b/doc/server-secrets.md
@@ -10,14 +10,19 @@ There are two ways of setting secrets:
    environment.
 
 ```sh
-GH_TOKEN=...
+DRONE_TOKEN=...
+DRONE_ORIGINS="https://drone.example.com"
 ```
 
 2. Via checked-in `config/local.yml`:
 
 ```yml
+public:
+  services:
+    drone:
+      authorizedOrigins: ['https://drone.example.com']
 private:
-  gh_token: '...'
+  drone_token: '...'
 ```
 
 For more complex scenarios, configuration files can cascade. See the [node-config documentation][]
@@ -25,9 +30,43 @@ for details.
 
 [node-config documentation]: https://github.com/lorenwest/node-config/wiki/Configuration-Files
 
-## Azure DevOps
+## Authorized origins
 
-- `AZURE_DEVOPS_TOKEN` (yml: `azure_devops_token`)
+Several of the badges provided by Shields allow users to specify the target
+URL/server of the upstream instance to use via a query parameter in the badge URL
+(e.g. https://img.shields.io/nexus/s/com.google.guava/guava?server=https%3A%2F%2Foss.sonatype.org).
+This supports scenarios where your users may need badges from multiple upstream
+targets, for example if you have more than one Nexus server.
+
+Accordingly, if you configure credentials for one of these services with your
+self-hosted Shields instance, you must also specifically authorize the hosts
+to which the credentials are allowed to be sent. If your self-hosted Shields
+instance then receives a badge request for a target that does not match any
+of the authorized origins, one of two things will happen:
+
+- if credentials are required for the targeted service, Shields will render
+  an error badge.
+- if credentials are optional for the targeted service, Shields will attempt
+  the request, but without sending any credentials.
+
+When setting authorized origins through an environment variable, use a space
+to separate multiple origins. Note that failing to define authorized origins
+for a service will default to an empty list, i.e. no authorized origins.
+
+It is highly recommended to use `https` origins with valid SSL, to avoid the
+possibility of exposing your credentials, for example through DNS-based attacks.
+
+It is also recommended to use tokens for a service account having
+[the fewest privileges needed][polp] for fetching the relevant status
+information.
+
+[polp]: https://en.wikipedia.org/wiki/Principle_of_least_privilege
+
+## Services
+
+### Azure DevOps
+
+- `AZURE_DEVOPS_TOKEN` (yml: `private.azure_devops_token`)
 
 An Azure DevOps Token (PAT) is required for accessing [private Azure DevOps projects][ado project visibility].
 
@@ -41,24 +80,45 @@ An Azure DevOps Token (PAT) is required for accessing [private Azure DevOps proj
 [ado personal access tokens]: https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=vsts#create-personal-access-tokens-to-authenticate-access
 [ado token scopes]: https://docs.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/oauth?view=vsts#scopes
 
-## Bintray
+### Bintray
 
-- `BINTRAY_USER` (yml: `bintray_user`)
-- `BINTRAY_API_KEY` (yml: `bintray_apikey`)
+- `BINTRAY_USER` (yml: `private.bintray_user`)
+- `BINTRAY_API_KEY` (yml: `private.bintray_apikey`)
 
 The bintray API [requires authentication](https://bintray.com/docs/api/#_authentication)
 Create an account and obtain a token from the user profile page.
 
-## Drone
+### Bitbucket (Cloud)
+
+- `BITBUCKET_USER` (yml: `private.bitbucket_username`)
+- `BITBUCKET_PASS` (yml: `private.bitbucket_password`)
+
+Bitbucket badges use basic auth. Provide a username and password to give your
+self-hosted Shields installation access to private repositories hosted on bitbucket.org.
+
+### Bitbucket Server
+
+- `BITBUCKET_SERVER_ORIGINS` (yml: `public.services.bitbucketServer.authorizedOrigins`)
+- `BITBUCKET_SERVER_USER` (yml: `private.bitbucket_server_username`)
+- `BITBUCKET_SERVER_PASS` (yml: `private.bitbucket_server_password`)
+
+Bitbucket badges use basic auth. Provide a username and password to give your
+self-hosted Shields installation access to a private Bitbucket Server instance.
+
+### Drone
 
-- `DRONE_TOKEN` (yml: `drone_token`)
+- `DRONE_ORIGINS` (yml: `public.services.drone.authorizedOrigins`)
+- `DRONE_TOKEN` (yml: `private.drone_token`)
 
-The self-hosted Drone API [requires authentication](https://0-8-0.docs.drone.io/api-authentication/)
-Login to your Drone instance and obtain a token from the user profile page.
+The self-hosted Drone API [requires authentication][drone auth]. Log in to your
+Drone instance and obtain a token from the user profile page.
 
-## GitHub
+[drone auth]: https://0-8-0.docs.drone.io/api-authentication/
 
-- `GH_TOKEN` (yml: `gh_token`)
+### GitHub
+
+- `GITHUB_URL` (yml: `public.services.github.baseUri`)
+- `GH_TOKEN` (yml: `private.gh_token`)
 
 Because of Github rate limits, you will need to provide a token, or else badges
 will stop working once you hit 60 requests per hour, the
@@ -72,76 +132,85 @@ will have access to your private repositories.
 When a `gh_token` is specified, it is used in place of the Shields token
 rotation logic.
 
+`GITHUB_URL` can be used to optionally send all the GitHub requests to a
+GitHub Enterprise server. This can be done in conjunction with setting a
+token, though it's not required.
+
 [github rate limit]: https://developer.github.com/v3/#rate-limiting
 [personal access tokens]: https://github.com/settings/tokens
 
-- `GH_CLIENT_ID` (yml: `gh_client_id`)
-- `GH_CLIENT_SECRET` (yml: `gh_client_secret`)
+- `GH_CLIENT_ID` (yml: `private.gh_client_id`)
+- `GH_CLIENT_SECRET` (yml: `private.gh_client_secret`)
 
 These settings are used by shields.io for GitHub OAuth app authorization
 but will not be necessary for most self-hosted installations. See
 [production-hosting.md](./production-hosting.md).
 
-## Jenkins CI
+### Jenkins CI
 
-- `JENKINS_USER` (yml: `jenkins_user`)
-- `JENKINS_PASS` (yml: `jenkins_pass`)
+- `JENKINS_ORIGINS` (yml: `public.services.jenkins.authorizedOrigins`)
+- `JENKINS_USER` (yml: `private.jenkins_user`)
+- `JENKINS_PASS` (yml: `private.jenkins_pass`)
 
 Provide a username and password to give your self-hosted Shields installation
 access to a private Jenkins CI instance.
 
-## JIRA
+### Jira
 
-- `JIRA_USER` (yml: `jira_user`)
-- `JIRA_PASS` (yml: `jira_pass`)
+- `JIRA_ORIGINS` (yml: `public.services.jira.authorizedOrigins`)
+- `JIRA_USER` (yml: `private.jira_user`)
+- `JIRA_PASS` (yml: `private.jira_pass`)
 
 Provide a username and password to give your self-hosted Shields installation
 access to a private JIRA instance.
 
-## Nexus
+### Nexus
 
-- `NEXUS_USER` (yml: `nexus_user`)
-- `NEXUS_PASS` (yml: `nexus_pass`)
+- `NEXUS_ORIGINS` (yml: `public.services.nexus.authorizedOrigins`)
+- `NEXUS_USER` (yml: `private.nexus_user`)
+- `NEXUS_PASS` (yml: `private.nexus_pass`)
 
 Provide a username and password to give your self-hosted Shields installation
 access to your private nexus repositories.
 
-## NPM
+### npm
 
-- `NPM_TOKEN` (yml: `npm_token`)
+- `NPM_ORIGINS` (yml: `public.services.npm.authorizedOrigins`)
+- `NPM_TOKEN` (yml: `private.npm_token`)
 
 [Generate an npm token][npm token] to give your self-hosted Shields
 installation access to private npm packages
 
 [npm token]: https://docs.npmjs.com/getting-started/working_with_tokens
 
-## Sentry
-
-- `SENTRY_DSN` (yml: `sentry_dsn`)
-
-A [Sentry DSN](https://docs.sentry.io/error-reporting/quickstart/?platform=javascript#configure-the-dsn)
-may be used to send error reports from your installation to
-[Sentry.io](http://sentry.io/). For more info, see the
-[self hosting docs](https://github.com/badges/shields/blob/master/doc/self-hosting.md#sentry).
+### SymfonyInsight (formerly Sensiolabs)
 
-## SymfonyInsight (formerly Sensiolabs)
-
-- `SL_INSIGHT_USER_UUID` (yml: `sl_insight_userUuid`)
-- `SL_INSIGHT_API_TOKEN` (yml: `sl_insight_apiToken`)
+- `SL_INSIGHT_USER_UUID` (yml: `private.sl_insight_userUuid`)
+- `SL_INSIGHT_API_TOKEN` (yml: `private.sl_insight_apiToken`)
 
 The SymfonyInsight API requires authentication. To obtain a token,
 Create an account, sign in and obtain a uuid and token from your
 [account page](https://insight.sensiolabs.com/account).
 
-## SonarQube
+### SonarQube
 
-- `SONARQUBE_TOKEN` (yml: `sonarqube_token`)
+- `SONAR_ORIGINS` (yml: `public.services.sonar.authorizedOrigins`)
+- `SONARQUBE_TOKEN` (yml: `private.sonarqube_token`)
 
 [Generate a token](https://docs.sonarqube.org/latest/user-guide/user-token/)
 to give your self-hosted Shields installation access to a
 private SonarQube instance or private project on a public instance.
 
-## Twitch
+### TeamCity
+
+- `TEAMCITY_ORIGINS` (yml: `public.services.teamcity.authorizedOrigins`)
+- `TEAMCITY_USER` (yml: `private.teamcity_user`)
+- `TEAMCITY_PASS` (yml: `private.teamcity_pass`)
+
+Provide a username and password to give your self-hosted Shields installation
+access to your private nexus repositories.
+
+### Twitch
 
 - `TWITCH_CLIENT_ID` (yml: `twitch_client_id`)
 - `TWITCH_CLIENT_SECRET` (yml: `twitch_client_secret`)
@@ -149,12 +218,23 @@ private SonarQube instance or private project on a public instance.
 Register an application in the [Twitch developer console](https://dev.twitch.tv/console)
 in order to obtain a client id and a client secret for making Twitch API calls.
 
-## Wheelmap
+### Wheelmap
 
-- `WHEELMAP_TOKEN` (yml: `wheelmap_token`)
+- `WHEELMAP_TOKEN` (yml: `private.wheelmap_token`)
 
 The wheelmap API requires authentication. To obtain a token,
 Create an account, [sign in][wheelmap token] and use the _Authentication Token_
 displayed on your profile page.
 
 [wheelmap token]: http://classic.wheelmap.org/en/users/sign_in
+
+## Error reporting
+
+- `SENTRY_DSN` (yml: `private.sentry_dsn`)
+
+A [Sentry DSN][] may be used to send error reports from your installation to
+[Sentry.io][]. For more info, see the [self hosting docs][].
+
+[sentry dsn]: https://docs.sentry.io/error-reporting/quickstart/?platform=javascript#configure-the-dsn
+[sentry.io]: http://sentry.io/
+[self hosting docs]: https://github.com/badges/shields/blob/master/doc/self-hosting.md#sentry
diff --git a/services/azure-devops/azure-devops-base.js b/services/azure-devops/azure-devops-base.js
index 396698181f1e5a745100619cdf71cb6effd6590e..904db4c28bea74963138d5838bd02e070affda14 100644
--- a/services/azure-devops/azure-devops-base.js
+++ b/services/azure-devops/azure-devops-base.js
@@ -18,17 +18,20 @@ module.exports = class AzureDevOpsBase extends BaseJsonService {
   static get auth() {
     return {
       passKey: 'azure_devops_token',
+      authorizedOrigins: ['https://dev.azure.com'],
       defaultToEmptyStringForUser: true,
     }
   }
 
   async fetch({ url, options, schema, errorMessages }) {
-    return this._requestJson({
-      schema,
-      url,
-      options,
-      errorMessages,
-    })
+    return this._requestJson(
+      this.authHelper.withBasicAuth({
+        schema,
+        url,
+        options,
+        errorMessages,
+      })
+    )
   }
 
   async getLatestCompletedBuildId(
@@ -36,7 +39,6 @@ module.exports = class AzureDevOpsBase extends BaseJsonService {
     project,
     definitionId,
     branch,
-    auth,
     errorMessages
   ) {
     // Microsoft documentation: https://docs.microsoft.com/en-us/rest/api/azure/devops/build/builds/list?view=azure-devops-rest-5.0
@@ -48,7 +50,6 @@ module.exports = class AzureDevOpsBase extends BaseJsonService {
         statusFilter: 'completed',
         'api-version': '5.0-preview.4',
       },
-      auth,
     }
 
     if (branch) {
diff --git a/services/azure-devops/azure-devops-coverage.service.js b/services/azure-devops/azure-devops-coverage.service.js
index c084b5c6fe2323428b30cc871840811be52615a8..97cc0596bec6ac1cb6a45c1960b6fb882f038a1b 100644
--- a/services/azure-devops/azure-devops-coverage.service.js
+++ b/services/azure-devops/azure-devops-coverage.service.js
@@ -100,7 +100,6 @@ module.exports = class AzureDevOpsCoverage extends AzureDevOpsBase {
   }
 
   async handle({ organization, project, definitionId, branch }) {
-    const auth = this.authHelper.basicAuth
     const errorMessages = {
       404: 'build pipeline or coverage not found',
     }
@@ -109,7 +108,6 @@ module.exports = class AzureDevOpsCoverage extends AzureDevOpsBase {
       project,
       definitionId,
       branch,
-      auth,
       errorMessages
     )
     // Microsoft documentation: https://docs.microsoft.com/en-us/rest/api/azure/devops/test/code%20coverage/get%20build%20code%20coverage?view=azure-devops-rest-5.0
@@ -119,7 +117,6 @@ module.exports = class AzureDevOpsCoverage extends AzureDevOpsBase {
         buildId,
         'api-version': '5.0-preview.1',
       },
-      auth,
     }
     const json = await this.fetch({
       url,
diff --git a/services/azure-devops/azure-devops-tests.service.js b/services/azure-devops/azure-devops-tests.service.js
index e0ee802cb296e060e3ba63a7c077d65196aa8782..6a4471b9b1d5c97c22e1ca110b122f4ea19d53b8 100644
--- a/services/azure-devops/azure-devops-tests.service.js
+++ b/services/azure-devops/azure-devops-tests.service.js
@@ -191,7 +191,6 @@ module.exports = class AzureDevOpsTests extends AzureDevOpsBase {
       skipped_label: skippedLabel,
     }
   ) {
-    const auth = this.authHelper.basicAuth
     const errorMessages = {
       404: 'build pipeline or test result summary not found',
     }
@@ -200,7 +199,6 @@ module.exports = class AzureDevOpsTests extends AzureDevOpsBase {
       project,
       definitionId,
       branch,
-      auth,
       errorMessages
     )
 
@@ -210,7 +208,6 @@ module.exports = class AzureDevOpsTests extends AzureDevOpsBase {
       url: `https://dev.azure.com/${organization}/${project}/_apis/test/ResultSummaryByBuild`,
       options: {
         qs: { buildId },
-        auth,
       },
       schema: buildTestResultSummarySchema,
       errorMessages,
diff --git a/services/bintray/bintray.service.js b/services/bintray/bintray.service.js
index a32cc34de40d3a24c050a50fa286625551f8da09..fc5c97b5cbd06d178ff019b33c308358c2b4155a 100644
--- a/services/bintray/bintray.service.js
+++ b/services/bintray/bintray.service.js
@@ -23,7 +23,11 @@ module.exports = class Bintray extends BaseJsonService {
   }
 
   static get auth() {
-    return { userKey: 'bintray_user', passKey: 'bintray_apikey' }
+    return {
+      userKey: 'bintray_user',
+      passKey: 'bintray_apikey',
+      authorizedOrigins: ['https://bintray.com'],
+    }
   }
 
   static get examples() {
@@ -46,11 +50,12 @@ module.exports = class Bintray extends BaseJsonService {
 
   async fetch({ subject, repo, packageName }) {
     // https://bintray.com/docs/api/#_get_version
-    return this._requestJson({
-      schema,
-      url: `https://bintray.com/api/v1/packages/${subject}/${repo}/${packageName}/versions/_latest`,
-      options: { auth: this.authHelper.basicAuth },
-    })
+    return this._requestJson(
+      this.authHelper.withBasicAuth({
+        schema,
+        url: `https://bintray.com/api/v1/packages/${subject}/${repo}/${packageName}/versions/_latest`,
+      })
+    )
   }
 
   async handle({ subject, repo, packageName }) {
diff --git a/services/bintray/bintray.spec.js b/services/bintray/bintray.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..2bb79cd71a84fea8b503d6fcf5d59492fefcff9d
--- /dev/null
+++ b/services/bintray/bintray.spec.js
@@ -0,0 +1,46 @@
+'use strict'
+
+const { expect } = require('chai')
+const nock = require('nock')
+const { cleanUpNockAfterEach, defaultContext } = require('../test-helpers')
+const Bintray = require('./bintray.service')
+
+describe('Bintray', function() {
+  describe('auth', function() {
+    cleanUpNockAfterEach()
+
+    const user = 'admin'
+    const pass = 'password'
+    const config = {
+      private: {
+        bintray_user: user,
+        bintray_apikey: pass,
+      },
+    }
+
+    it('sends the auth information as configured', async function() {
+      const scope = nock('https://bintray.com')
+        .get('/api/v1/packages/asciidoctor/maven/asciidoctorj/versions/_latest')
+        // This ensures that the expected credentials are actually being sent with the HTTP request.
+        // Without this the request wouldn't match and the test would fail.
+        .basicAuth({ user, pass })
+        .reply(200, {
+          name: '1.5.7',
+        })
+
+      expect(
+        await Bintray.invoke(defaultContext, config, {
+          subject: 'asciidoctor',
+          repo: 'maven',
+          packageName: 'asciidoctorj',
+        })
+      ).to.deep.equal({
+        label: undefined,
+        message: 'v1.5.7',
+        color: 'blue',
+      })
+
+      scope.done()
+    })
+  })
+})
diff --git a/services/bitbucket/bitbucket-pull-request.service.js b/services/bitbucket/bitbucket-pull-request.service.js
index 1061984ac67cec194d6dc2b9de6b12042b2e16c6..9f932aeb6edfc70fe242e9b1e11cf065d9e4091d 100644
--- a/services/bitbucket/bitbucket-pull-request.service.js
+++ b/services/bitbucket/bitbucket-pull-request.service.js
@@ -81,46 +81,48 @@ function pullRequestClassGenerator(raw) {
         {
           userKey: 'bitbucket_username',
           passKey: 'bitbucket_password',
+          authorizedOrigins: ['https://bitbucket.org'],
         },
-        config.private
+        config
       )
       this.bitbucketServerAuthHelper = new AuthHelper(
         {
           userKey: 'bitbucket_server_username',
           passKey: 'bitbucket_server_password',
+          serviceKey: 'bitbucketServer',
         },
-        config.private
+        config
       )
     }
 
     async fetchCloud({ user, repo }) {
-      return this._requestJson({
-        url: `https://bitbucket.org/api/2.0/repositories/${user}/${repo}/pullrequests/`,
-        schema,
-        options: {
-          qs: { state: 'OPEN', limit: 0 },
-          auth: this.bitbucketAuthHelper.basicAuth,
-        },
-        errorMessages,
-      })
+      return this._requestJson(
+        this.bitbucketAuthHelper.withBasicAuth({
+          url: `https://bitbucket.org/api/2.0/repositories/${user}/${repo}/pullrequests/`,
+          schema,
+          options: { qs: { state: 'OPEN', limit: 0 } },
+          errorMessages,
+        })
+      )
     }
 
     // https://docs.atlassian.com/bitbucket-server/rest/5.16.0/bitbucket-rest.html#idm46229602363312
     async fetchServer({ server, user, repo }) {
-      return this._requestJson({
-        url: `${server}/rest/api/1.0/projects/${user}/repos/${repo}/pull-requests`,
-        schema,
-        options: {
-          qs: {
-            state: 'OPEN',
-            limit: 100,
-            withProperties: false,
-            withAttributes: false,
+      return this._requestJson(
+        this.bitbucketServerAuthHelper.withBasicAuth({
+          url: `${server}/rest/api/1.0/projects/${user}/repos/${repo}/pull-requests`,
+          schema,
+          options: {
+            qs: {
+              state: 'OPEN',
+              limit: 100,
+              withProperties: false,
+              withAttributes: false,
+            },
           },
-          auth: this.bitbucketServerAuthHelper.basicAuth,
-        },
-        errorMessages,
-      })
+          errorMessages,
+        })
+      )
     }
 
     async fetch({ server, user, repo }) {
diff --git a/services/bitbucket/bitbucket-pull-request.spec.js b/services/bitbucket/bitbucket-pull-request.spec.js
index 3cf8aecf05da6d34943f774ff780ea42bd49fdc7..fce1ed1546b0ff38473a47e01d50a74428678c23 100644
--- a/services/bitbucket/bitbucket-pull-request.spec.js
+++ b/services/bitbucket/bitbucket-pull-request.spec.js
@@ -21,6 +21,13 @@ describe('BitbucketPullRequest', function() {
       await BitbucketPullRequest.invoke(
         defaultContext,
         {
+          public: {
+            services: {
+              bitbucketServer: {
+                authorizedOrigins: [],
+              },
+            },
+          },
           private: { bitbucket_username: user, bitbucket_password: pass },
         },
         { user: 'atlassian', repo: 'python-bitbucket' }
@@ -43,6 +50,13 @@ describe('BitbucketPullRequest', function() {
       await BitbucketPullRequest.invoke(
         defaultContext,
         {
+          public: {
+            services: {
+              bitbucketServer: {
+                authorizedOrigins: ['https://bitbucket.example.test'],
+              },
+            },
+          },
           private: {
             bitbucket_server_username: user,
             bitbucket_server_password: pass,
diff --git a/services/drone/drone-build.service.js b/services/drone/drone-build.service.js
index 85b09f84fbf3e8814c5b55968384ec13705a5d97..b89fe14a50bff627caff56ca9e851ff5b712fac8 100644
--- a/services/drone/drone-build.service.js
+++ b/services/drone/drone-build.service.js
@@ -5,7 +5,7 @@ const { isBuildStatus, renderBuildStatusBadge } = require('../build-status')
 const { optionalUrl } = require('../validators')
 const { BaseJsonService } = require('..')
 
-const DroneBuildSchema = Joi.object({
+const schema = Joi.object({
   status: Joi.alternatives()
     .try(isBuildStatus, Joi.equal('none'), Joi.equal('killed'))
     .required(),
@@ -29,7 +29,7 @@ module.exports = class DroneBuild extends BaseJsonService {
   }
 
   static get auth() {
-    return { passKey: 'drone_token' }
+    return { passKey: 'drone_token', serviceKey: 'drone' }
   }
 
   static get examples() {
@@ -83,24 +83,19 @@ module.exports = class DroneBuild extends BaseJsonService {
     }
   }
 
-  async handle({ user, repo, branch }, { server }) {
-    const options = {
-      qs: {
-        ref: branch ? `refs/heads/${branch}` : undefined,
-      },
-      headers: this.authHelper.bearerAuthHeader,
-    }
-    if (!server) {
-      server = 'https://cloud.drone.io'
-    }
-    const json = await this._requestJson({
-      options,
-      schema: DroneBuildSchema,
-      url: `${server}/api/repos/${user}/${repo}/builds/latest`,
-      errorMessages: {
-        401: 'repo not found or not authorized',
-      },
-    })
+  async handle({ user, repo, branch }, { server = 'https://cloud.drone.io' }) {
+    const json = await this._requestJson(
+      this.authHelper.withBearerAuthHeader({
+        schema,
+        url: `${server}/api/repos/${user}/${repo}/builds/latest`,
+        options: {
+          qs: { ref: branch ? `refs/heads/${branch}` : undefined },
+        },
+        errorMessages: {
+          401: 'repo not found or not authorized',
+        },
+      })
+    )
     return renderBuildStatusBadge({ status: json.status })
   }
 }
diff --git a/services/drone/drone-build.spec.js b/services/drone/drone-build.spec.js
index 41537d050ab730e18e6627fa0f60ee6522636160..06837dbad1f287d7eeba14f08c57726d17da17c9 100644
--- a/services/drone/drone-build.spec.js
+++ b/services/drone/drone-build.spec.js
@@ -21,7 +21,16 @@ describe('DroneBuild', function() {
       await DroneBuild.invoke(
         defaultContext,
         {
-          private: { drone_token: token },
+          public: {
+            services: {
+              drone: {
+                authorizedOrigins: ['https://cloud.drone.io'],
+              },
+            },
+          },
+          private: {
+            drone_token: token,
+          },
         },
         { user: 'atlassian', repo: 'python-bitbucket' }
       )
diff --git a/services/github/auth/acceptor.js b/services/github/auth/acceptor.js
index b2a9d93e2add6c1d23609dc50c38026955eec84c..9b5ae42f3376244f95960d5bf4cc6f271a8d6c34 100644
--- a/services/github/auth/acceptor.js
+++ b/services/github/auth/acceptor.js
@@ -2,6 +2,9 @@
 
 const queryString = require('query-string')
 const request = require('request')
+const {
+  userAgent,
+} = require('../../../core/base-service/legacy-request-handler')
 const log = require('../../../core/server/log')
 const secretIsValid = require('../../../core/server/secret-is-valid')
 const serverSecrets = require('../../../lib/server-secrets')
@@ -50,7 +53,11 @@ function setRoutes({ server, authHelper, onTokenAccepted }) {
   server.route(/^\/github-auth$/, (data, match, end, ask) => {
     ask.res.statusCode = 302 // Found.
     const query = queryString.stringify({
-      client_id: authHelper.user,
+      // TODO The `_user` property bypasses security checks in AuthHelper.
+      // (e.g: enforceStrictSsl and shouldAuthenticateRequest).
+      // Do not use it elsewhere. It would be better to clean this up so
+      // it's not setting a bad example.
+      client_id: authHelper._user,
       redirect_uri: `${baseUrl}/github-auth/done`,
     })
     ask.res.setHeader(
@@ -71,11 +78,15 @@ function setRoutes({ server, authHelper, onTokenAccepted }) {
       method: 'POST',
       headers: {
         'Content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
-        'User-Agent': 'Shields.io',
+        'User-Agent': userAgent,
       },
       form: queryString.stringify({
-        client_id: authHelper.user,
-        client_secret: authHelper.pass,
+        // TODO The `_user` and `_pass` properties bypass security checks in
+        // AuthHelper (e.g: enforceStrictSsl and shouldAuthenticateRequest).
+        // Do not use them elsewhere. It would be better to clean
+        // this up so it's not setting a bad example.
+        client_id: authHelper._user,
+        client_secret: authHelper._pass,
         code: data.code,
       }),
     }
diff --git a/services/github/auth/acceptor.spec.js b/services/github/auth/acceptor.spec.js
index b9917e69874e8d1934b85c04ee8ac5ff5b250138..844f76eed7fe80301bbfebed07c6863128069035 100644
--- a/services/github/auth/acceptor.spec.js
+++ b/services/github/auth/acceptor.spec.js
@@ -16,7 +16,7 @@ const fakeShieldsSecret = 'letmeinplz'
 
 describe('Github token acceptor', function() {
   const oauthHelper = GithubConstellation._createOauthHelper({
-    gh_client_id: fakeClientId,
+    private: { gh_client_id: fakeClientId },
   })
   before(function() {
     // Make sure properties exist.
diff --git a/services/github/github-api-provider.js b/services/github/github-api-provider.js
index b4be59a634f1fb6376c2490861ad7413734df272..632965bddaf01cc60549363f8f07274e115ddf96 100644
--- a/services/github/github-api-provider.js
+++ b/services/github/github-api-provider.js
@@ -3,6 +3,7 @@
 const Joi = require('@hapi/joi')
 const log = require('../../core/server/log')
 const { TokenPool } = require('../../core/token-pooling/token-pool')
+const { userAgent } = require('../../core/base-service/legacy-request-handler')
 const { nonNegativeInteger } = require('../validators')
 
 const headerSchema = Joi.object({
@@ -184,7 +185,7 @@ class GithubApiProvider {
         url,
         baseUrl,
         headers: {
-          'User-Agent': 'Shields.io',
+          'User-Agent': userAgent,
           Accept: 'application/vnd.github.v3+json',
           Authorization: `token ${tokenString}`,
         },
diff --git a/services/github/github-constellation.js b/services/github/github-constellation.js
index f914ea02bb51966fa3086134354bb00b51272d78..a9754bc6bf4262db6b288244fd31a5555efae787 100644
--- a/services/github/github-constellation.js
+++ b/services/github/github-constellation.js
@@ -13,14 +13,15 @@ const { setRoutes: setAcceptorRoutes } = require('./auth/acceptor')
 // Convenience class with all the stuff related to the Github API and its
 // authorization tokens, to simplify server initialization.
 class GithubConstellation {
-  static _createOauthHelper(privateConfig) {
+  static _createOauthHelper(config) {
     return new AuthHelper(
       {
         userKey: 'gh_client_id',
         passKey: 'gh_client_secret',
+        authorizedOrigins: ['https://api.github.com'],
         isRequired: true,
       },
-      privateConfig
+      config
     )
   }
 
@@ -54,7 +55,7 @@ class GithubConstellation {
       onTokenInvalidated: tokenString => this.onTokenInvalidated(tokenString),
     })
 
-    this.oauthHelper = this.constructor._createOauthHelper(config.private)
+    this.oauthHelper = this.constructor._createOauthHelper(config)
   }
 
   scheduleDebugLogging() {
diff --git a/services/jenkins/jenkins-base.js b/services/jenkins/jenkins-base.js
index 58c9e682c987ae11d9df76e39a8cfbc38a747c3c..c2d1e5b3b0c36c92bd42011da5e4994b38f29d49 100644
--- a/services/jenkins/jenkins-base.js
+++ b/services/jenkins/jenkins-base.js
@@ -7,6 +7,7 @@ module.exports = class JenkinsBase extends BaseJsonService {
     return {
       userKey: 'jenkins_user',
       passKey: 'jenkins_pass',
+      serviceKey: 'jenkins',
     }
   }
 
@@ -17,15 +18,16 @@ module.exports = class JenkinsBase extends BaseJsonService {
     errorMessages = { 404: 'instance or job not found' },
     disableStrictSSL,
   }) {
-    return this._requestJson({
-      url,
-      options: {
-        qs,
-        strictSSL: disableStrictSSL === undefined,
-        auth: this.authHelper.basicAuth,
-      },
-      schema,
-      errorMessages,
-    })
+    return this._requestJson(
+      this.authHelper.withBasicAuth({
+        url,
+        options: {
+          qs,
+          strictSSL: disableStrictSSL === undefined,
+        },
+        schema,
+        errorMessages,
+      })
+    )
   }
 }
diff --git a/services/jenkins/jenkins-build.spec.js b/services/jenkins/jenkins-build.spec.js
index 225290c94b455c66c23a99d77b475c75c02b5426..411436a8a671c83f4196c3bd1cc980c52cb37cbb 100644
--- a/services/jenkins/jenkins-build.spec.js
+++ b/services/jenkins/jenkins-build.spec.js
@@ -63,7 +63,19 @@ describe('JenkinsBuild', function() {
 
     const user = 'admin'
     const pass = 'password'
-    const config = { private: { jenkins_user: user, jenkins_pass: pass } }
+    const config = {
+      public: {
+        services: {
+          jenkins: {
+            authorizedOrigins: ['https://jenkins.ubuntu.com'],
+          },
+        },
+      },
+      private: {
+        jenkins_user: user,
+        jenkins_pass: pass,
+      },
+    }
 
     it('sends the auth information as configured', async function() {
       const scope = nock('https://jenkins.ubuntu.com')
diff --git a/services/jira/jira-common.js b/services/jira/jira-common.js
index 9f51ce3fd683409015258ac6d34afe74886c8c98..e8fa67915915c78f875556a68163f20b3f658977 100644
--- a/services/jira/jira-common.js
+++ b/services/jira/jira-common.js
@@ -3,6 +3,7 @@
 const authConfig = {
   userKey: 'jira_user',
   passKey: 'jira_pass',
+  serviceKey: 'jira',
 }
 
 module.exports = { authConfig }
diff --git a/services/jira/jira-issue.service.js b/services/jira/jira-issue.service.js
index 1ba8b98ad6468c27fe3c413eaa07879ff07a578c..b1eeea4a5138404779bdea0d873d2dbabf07646f 100644
--- a/services/jira/jira-issue.service.js
+++ b/services/jira/jira-issue.service.js
@@ -83,12 +83,13 @@ module.exports = class JiraIssue extends BaseJsonService {
 
   async handle({ issueKey }, { baseUrl }) {
     // Atlassian Documentation: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-api-2-issue-issueIdOrKey-get
-    const json = await this._requestJson({
-      schema,
-      url: `${baseUrl}/rest/api/2/issue/${encodeURIComponent(issueKey)}`,
-      options: { auth: this.authHelper.basicAuth },
-      errorMessages: { 404: 'issue not found' },
-    })
+    const json = await this._requestJson(
+      this.authHelper.withBasicAuth({
+        schema,
+        url: `${baseUrl}/rest/api/2/issue/${encodeURIComponent(issueKey)}`,
+        errorMessages: { 404: 'issue not found' },
+      })
+    )
 
     const issueStatus = json.fields.status
     const statusName = issueStatus.name
diff --git a/services/jira/jira-issue.spec.js b/services/jira/jira-issue.spec.js
index 26ecbc1d9b5f2ece47d38eea135712be7c51bc9a..df5c5e7c86b48c329964fb61cd0ef5d47343f218 100644
--- a/services/jira/jira-issue.spec.js
+++ b/services/jira/jira-issue.spec.js
@@ -4,13 +4,13 @@ const { expect } = require('chai')
 const nock = require('nock')
 const { cleanUpNockAfterEach, defaultContext } = require('../test-helpers')
 const JiraIssue = require('./jira-issue.service')
-const { user, pass, config } = require('./jira-test-helpers')
+const { user, pass, host, config } = require('./jira-test-helpers')
 
 describe('JiraIssue', function() {
   cleanUpNockAfterEach()
 
   it('sends the auth information as configured', async function() {
-    const scope = nock('https://myprivatejira.test')
+    const scope = nock(`https://${host}`)
       .get(`/rest/api/2/issue/${encodeURIComponent('secure-234')}`)
       // This ensures that the expected credentials are actually being sent with the HTTP request.
       // Without this the request wouldn't match and the test would fail.
@@ -24,7 +24,7 @@ describe('JiraIssue', function() {
         {
           issueKey: 'secure-234',
         },
-        { baseUrl: 'https://myprivatejira.test' }
+        { baseUrl: `https://${host}` }
       )
     ).to.deep.equal({
       label: 'secure-234',
diff --git a/services/jira/jira-sprint.service.js b/services/jira/jira-sprint.service.js
index 5fe1f48bc1c832be6fefa119314f9a3929f36f91..e7863b24b1f08dfe132e0caae47b43338174ccea 100644
--- a/services/jira/jira-sprint.service.js
+++ b/services/jira/jira-sprint.service.js
@@ -93,22 +93,23 @@ module.exports = class JiraSprint extends BaseJsonService {
     // Atlassian Documentation: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-group-Search
     // There are other sprint-specific APIs but those require authentication. The search API
     // allows us to get the needed data without being forced to authenticate.
-    const json = await this._requestJson({
-      url: `${baseUrl}/rest/api/2/search`,
-      schema,
-      options: {
-        qs: {
-          jql: `sprint=${sprintId} AND type IN (Bug,Improvement,Story,"Technical task")`,
-          fields: 'resolution',
-          maxResults: 500,
+    const json = await this._requestJson(
+      this.authHelper.withBasicAuth({
+        url: `${baseUrl}/rest/api/2/search`,
+        schema,
+        options: {
+          qs: {
+            jql: `sprint=${sprintId} AND type IN (Bug,Improvement,Story,"Technical task")`,
+            fields: 'resolution',
+            maxResults: 500,
+          },
         },
-        auth: this.authHelper.basicAuth,
-      },
-      errorMessages: {
-        400: 'sprint not found',
-        404: 'sprint not found',
-      },
-    })
+        errorMessages: {
+          400: 'sprint not found',
+          404: 'sprint not found',
+        },
+      })
+    )
 
     const numTotalIssues = json.total
     const numCompletedIssues = json.issues.filter(issue => {
diff --git a/services/jira/jira-sprint.spec.js b/services/jira/jira-sprint.spec.js
index 080a305f6c35590fad213f7e1f65be4c1bc5e8cf..d9696f3e8b780031a08da1f35e4f8e441e6d939c 100644
--- a/services/jira/jira-sprint.spec.js
+++ b/services/jira/jira-sprint.spec.js
@@ -7,6 +7,7 @@ const JiraSprint = require('./jira-sprint.service')
 const {
   user,
   pass,
+  host,
   config,
   sprintId,
   sprintQueryString,
@@ -16,7 +17,7 @@ describe('JiraSprint', function() {
   cleanUpNockAfterEach()
 
   it('sends the auth information as configured', async function() {
-    const scope = nock('https://myprivatejira.test')
+    const scope = nock(`https://${host}`)
       .get('/jira/rest/api/2/search')
       .query(sprintQueryString)
       // This ensures that the expected credentials are actually being sent with the HTTP request.
@@ -37,7 +38,7 @@ describe('JiraSprint', function() {
         {
           sprintId,
         },
-        { baseUrl: 'https://myprivatejira.test/jira' }
+        { baseUrl: `https://${host}/jira` }
       )
     ).to.deep.equal({
       label: 'completion',
diff --git a/services/jira/jira-test-helpers.js b/services/jira/jira-test-helpers.js
index ad31e8d2286c3346f114c980714db6ca08696b41..11c01cff674c9dddebaed52303c986f1d59a03bb 100644
--- a/services/jira/jira-test-helpers.js
+++ b/services/jira/jira-test-helpers.js
@@ -9,12 +9,23 @@ const sprintQueryString = {
 
 const user = 'admin'
 const pass = 'password'
-const config = { private: { jira_user: user, jira_pass: pass } }
+const host = 'myprivatejira.test'
+const config = {
+  public: {
+    services: {
+      jira: {
+        authorizedOrigins: [`https://${host}`],
+      },
+    },
+  },
+  private: { jira_user: user, jira_pass: pass },
+}
 
 module.exports = {
   sprintId,
   sprintQueryString,
   user,
   pass,
+  host,
   config,
 }
diff --git a/services/nexus/nexus.service.js b/services/nexus/nexus.service.js
index 462bbb9c925437b4b19b13facb2bb73d0c9f215b..b95362d73fbab7aef657191add1332e052e6c5f8 100644
--- a/services/nexus/nexus.service.js
+++ b/services/nexus/nexus.service.js
@@ -67,7 +67,7 @@ module.exports = class Nexus extends BaseJsonService {
   }
 
   static get auth() {
-    return { userKey: 'nexus_user', passKey: 'nexus_pass' }
+    return { userKey: 'nexus_user', passKey: 'nexus_pass', serviceKey: 'nexus' }
   }
 
   static get examples() {
@@ -223,14 +223,16 @@ module.exports = class Nexus extends BaseJsonService {
       this.addQueryParamsToQueryString({ qs, queryOpt })
     }
 
-    const json = await this._requestJson({
-      schema,
-      url,
-      options: { qs, auth: this.authHelper.basicAuth },
-      errorMessages: {
-        404: 'artifact not found',
-      },
-    })
+    const json = await this._requestJson(
+      this.authHelper.withBasicAuth({
+        schema,
+        url,
+        options: { qs },
+        errorMessages: {
+          404: 'artifact not found',
+        },
+      })
+    )
 
     return { actualNexusVersion: '2', json }
   }
@@ -262,14 +264,16 @@ module.exports = class Nexus extends BaseJsonService {
       server.slice(-1) === '/' ? '' : '/'
     }service/rest/v1/search`
 
-    const json = await this._requestJson({
-      schema: nexus3SearchApiSchema,
-      url,
-      options: { qs, auth: this.authHelper.basicAuth },
-      errorMessages: {
-        404: 'artifact not found',
-      },
-    })
+    const json = await this._requestJson(
+      this.authHelper.withBasicAuth({
+        schema: nexus3SearchApiSchema,
+        url,
+        options: { qs },
+        errorMessages: {
+          404: 'artifact not found',
+        },
+      })
+    )
 
     return { actualNexusVersion: '3', json }
   }
diff --git a/services/nexus/nexus.spec.js b/services/nexus/nexus.spec.js
index 3b478495d2c90ef3f24978e0d5d7207f344756a7..64556b16b25b1dfb67d7f5575ab98cd1ae09dd0f 100644
--- a/services/nexus/nexus.spec.js
+++ b/services/nexus/nexus.spec.js
@@ -119,11 +119,23 @@ describe('Nexus', function() {
 
     const user = 'admin'
     const pass = 'password'
-    const config = { private: { nexus_user: user, nexus_pass: pass } }
+    const config = {
+      public: {
+        services: {
+          nexus: {
+            authorizedOrigins: ['https://repository.jboss.org'],
+          },
+        },
+      },
+      private: {
+        nexus_user: user,
+        nexus_pass: pass,
+      },
+    }
 
     it('sends the auth information as configured', async function() {
-      const scope = nock('https://repository.jboss.org/nexus')
-        .get('/service/local/lucene/search')
+      const scope = nock('https://repository.jboss.org')
+        .get('/nexus/service/local/lucene/search')
         .query({ g: 'jboss', a: 'jboss-client' })
         // This ensures that the expected credentials are actually being sent with the HTTP request.
         // Without this the request wouldn't match and the test would fail.
diff --git a/services/npm/npm-base.js b/services/npm/npm-base.js
index 8809b8bd2adde4d0ae92acf3cb7aed901a0134a3..bded61a3910e10f3899b1ade5ea3a3408646b887 100644
--- a/services/npm/npm-base.js
+++ b/services/npm/npm-base.js
@@ -42,7 +42,7 @@ const queryParamSchema = Joi.object({
 // of a package.
 module.exports = class NpmBase extends BaseJsonService {
   static get auth() {
-    return { passKey: 'npm_token' }
+    return { passKey: 'npm_token', serviceKey: 'npm' }
   }
 
   static buildRoute(base, { withTag } = {}) {
@@ -81,17 +81,18 @@ module.exports = class NpmBase extends BaseJsonService {
   }
 
   async _requestJson(data) {
-    return super._requestJson({
-      ...data,
-      options: {
-        headers: {
-          // Use a custom Accept header because of this bug:
-          // <https://github.com/npm/npmjs.org/issues/163>
-          Accept: '*/*',
-          ...this.authHelper.bearerAuthHeader,
+    return super._requestJson(
+      this.authHelper.withBearerAuthHeader({
+        ...data,
+        options: {
+          headers: {
+            // Use a custom Accept header because of this bug:
+            // <https://github.com/npm/npmjs.org/issues/163>
+            Accept: '*/*',
+          },
         },
-      },
-    })
+      })
+    )
   }
 
   async fetchPackageData({ registryUrl, scope, packageName, tag }) {
diff --git a/services/npm/npm-base.spec.js b/services/npm/npm-base.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..45a0e3c8e55d41e2bac8cf7c16485714cbf9539c
--- /dev/null
+++ b/services/npm/npm-base.spec.js
@@ -0,0 +1,46 @@
+'use strict'
+
+const { expect } = require('chai')
+const nock = require('nock')
+const { cleanUpNockAfterEach, defaultContext } = require('../test-helpers')
+// use NPM Version as an example implementation of NpmBase for this test
+const NpmVersion = require('./npm-version.service')
+
+describe('npm', function() {
+  describe('auth', function() {
+    it('sends the auth information as configured', async function() {
+      cleanUpNockAfterEach()
+
+      const token = 'abc123'
+
+      const scope = nock('https://registry.npmjs.org', {
+        reqheaders: { Accept: '*/*', Authorization: `Bearer ${token}` },
+      })
+        .get('/-/package/npm/dist-tags')
+        .reply(200, { latest: '0.1.0' })
+
+      const config = {
+        public: {
+          services: {
+            npm: {
+              authorizedOrigins: ['https://registry.npmjs.org'],
+            },
+          },
+        },
+        private: {
+          npm_token: token,
+        },
+      }
+
+      expect(
+        await NpmVersion.invoke(defaultContext, config, { packageName: 'npm' })
+      ).to.deep.equal({
+        color: 'orange',
+        label: undefined,
+        message: 'v0.1.0',
+      })
+
+      scope.done()
+    })
+  })
+})
diff --git a/services/sonar/sonar-base.js b/services/sonar/sonar-base.js
index 05d0a4ed6623958dc7b1f51deafb56e99fe729b9..9e9bceb476fe97b8d801c0fa158cbf2b76130ac4 100644
--- a/services/sonar/sonar-base.js
+++ b/services/sonar/sonar-base.js
@@ -53,7 +53,7 @@ const legacySchema = Joi.array()
 
 module.exports = class SonarBase extends BaseJsonService {
   static get auth() {
-    return { userKey: 'sonarqube_token' }
+    return { userKey: 'sonarqube_token', serviceKey: 'sonar' }
   }
 
   async fetch({ sonarVersion, server, component, metricName }) {
@@ -78,17 +78,16 @@ module.exports = class SonarBase extends BaseJsonService {
       }
     }
 
-    return this._requestJson({
-      schema,
-      url,
-      options: {
-        qs,
-        auth: this.authHelper.basicAuth,
-      },
-      errorMessages: {
-        404: 'component or metric not found, or legacy API not supported',
-      },
-    })
+    return this._requestJson(
+      this.authHelper.withBasicAuth({
+        schema,
+        url,
+        options: { qs },
+        errorMessages: {
+          404: 'component or metric not found, or legacy API not supported',
+        },
+      })
+    )
   }
 
   transform({ json, sonarVersion }) {
diff --git a/services/sonar/sonar-fortify-rating.spec.js b/services/sonar/sonar-fortify-rating.spec.js
index ac8c9b3296e24c659f741a66b6139a8e678a1687..90b5b11a80b50b9894da57d18a552d0c374f87fc 100644
--- a/services/sonar/sonar-fortify-rating.spec.js
+++ b/services/sonar/sonar-fortify-rating.spec.js
@@ -6,7 +6,16 @@ const { cleanUpNockAfterEach, defaultContext } = require('../test-helpers')
 const SonarFortifyRating = require('./sonar-fortify-rating.service')
 
 const token = 'abc123def456'
-const config = { private: { sonarqube_token: token } }
+const config = {
+  public: {
+    services: {
+      sonar: { authorizedOrigins: ['http://sonar.petalslink.com'] },
+    },
+  },
+  private: {
+    sonarqube_token: token,
+  },
+}
 
 describe('SonarFortifyRating', function() {
   cleanUpNockAfterEach()
diff --git a/services/symfony/symfony-insight-base.js b/services/symfony/symfony-insight-base.js
index a51f7fffe0afcaeb764aecbb17187098a73f31e7..91cb91b9f2d79d768558d9ebce53576e8d97cfdd 100644
--- a/services/symfony/symfony-insight-base.js
+++ b/services/symfony/symfony-insight-base.js
@@ -54,6 +54,7 @@ class SymfonyInsightBase extends BaseXmlService {
     return {
       userKey: 'sl_insight_userUuid',
       passKey: 'sl_insight_apiToken',
+      authorizedOrigins: ['https://insight.symfony.com'],
       isRequired: true,
     }
   }
@@ -65,24 +66,23 @@ class SymfonyInsightBase extends BaseXmlService {
   }
 
   async fetch({ projectUuid }) {
-    return this._requestXml({
-      schema,
-      url: `https://insight.symfony.com/api/projects/${projectUuid}`,
-      options: {
-        headers: {
-          Accept: 'application/vnd.com.sensiolabs.insight+xml',
+    return this._requestXml(
+      this.authHelper.withBasicAuth({
+        schema,
+        url: `https://insight.symfony.com/api/projects/${projectUuid}`,
+        options: {
+          headers: { Accept: 'application/vnd.com.sensiolabs.insight+xml' },
         },
-        auth: this.authHelper.basicAuth,
-      },
-      errorMessages: {
-        401: 'not authorized to access project',
-        404: 'project not found',
-      },
-      parserOptions: {
-        attributeNamePrefix: '',
-        ignoreAttributes: false,
-      },
-    })
+        errorMessages: {
+          401: 'not authorized to access project',
+          404: 'project not found',
+        },
+        parserOptions: {
+          attributeNamePrefix: '',
+          ignoreAttributes: false,
+        },
+      })
+    )
   }
 
   transform({ data }) {
diff --git a/services/symfony/symfony-test-helpers.js b/services/symfony/symfony-test-helpers.js
index 9b2c09baf44e1effc3c135adaeb552546482a572..1f9bd0f670e2d1f304ad8a3b645b966cb912cebe 100644
--- a/services/symfony/symfony-test-helpers.js
+++ b/services/symfony/symfony-test-helpers.js
@@ -81,6 +81,7 @@ const multipleViolations = createMockResponse({
 const user = 'admin'
 const token = 'password'
 const config = {
+  public: { services: {} },
   private: {
     sl_insight_userUuid: user,
     sl_insight_apiToken: token,
diff --git a/services/teamcity/teamcity-base.js b/services/teamcity/teamcity-base.js
index eab3a26bcc60f7abeacd3125857861c2fdb60f57..e280235d20da825f4670483a890c1b229838180b 100644
--- a/services/teamcity/teamcity-base.js
+++ b/services/teamcity/teamcity-base.js
@@ -4,24 +4,27 @@ const { BaseJsonService } = require('..')
 
 module.exports = class TeamCityBase extends BaseJsonService {
   static get auth() {
-    return { userKey: 'teamcity_user', passKey: 'teamcity_pass' }
+    return {
+      userKey: 'teamcity_user',
+      passKey: 'teamcity_pass',
+      serviceKey: 'teamcity',
+    }
   }
 
   async fetch({ url, schema, qs = {}, errorMessages = {} }) {
     // JetBrains API Auth Docs: https://confluence.jetbrains.com/display/TCD18/REST+API#RESTAPI-RESTAuthentication
     const options = { qs }
-    const auth = this.authHelper.basicAuth
-    if (auth) {
-      options.auth = auth
-    } else {
+    if (!this.authHelper.isConfigured) {
       qs.guest = 1
     }
 
-    return this._requestJson({
-      url,
-      schema,
-      options,
-      errorMessages: { 404: 'build not found', ...errorMessages },
-    })
+    return this._requestJson(
+      this.authHelper.withBasicAuth({
+        url,
+        schema,
+        options,
+        errorMessages: { 404: 'build not found', ...errorMessages },
+      })
+    )
   }
 }
diff --git a/services/teamcity/teamcity-build.spec.js b/services/teamcity/teamcity-build.spec.js
index 3c48470e4e4b4aa686eaad5ce03e870507eb84d2..06aee3668f8dbdb050774ebe3632d9bc050e8030 100644
--- a/services/teamcity/teamcity-build.spec.js
+++ b/services/teamcity/teamcity-build.spec.js
@@ -4,13 +4,13 @@ const { expect } = require('chai')
 const nock = require('nock')
 const { cleanUpNockAfterEach, defaultContext } = require('../test-helpers')
 const TeamCityBuild = require('./teamcity-build.service')
-const { user, pass, config } = require('./teamcity-test-helpers')
+const { user, pass, host, config } = require('./teamcity-test-helpers')
 
 describe('TeamCityBuild', function() {
   cleanUpNockAfterEach()
 
   it('sends the auth information as configured', async function() {
-    const scope = nock('https://mycompany.teamcity.com')
+    const scope = nock(`https://${host}`)
       .get(`/app/rest/builds/${encodeURIComponent('buildType:(id:bt678)')}`)
       // This ensures that the expected credentials are actually being sent with the HTTP request.
       // Without this the request wouldn't match and the test would fail.
@@ -29,7 +29,7 @@ describe('TeamCityBuild', function() {
           verbosity: 'e',
           buildId: 'bt678',
         },
-        { server: 'https://mycompany.teamcity.com' }
+        { server: `https://${host}` }
       )
     ).to.deep.equal({
       message: 'tests failed: 1 (1 new), passed: 50246, ignored: 1, muted: 12',
diff --git a/services/teamcity/teamcity-coverage.spec.js b/services/teamcity/teamcity-coverage.spec.js
index 05081ce81d51e077f15115396986400dd93cf09b..c193caede9c090b4fce358cea9f7e63348145533 100644
--- a/services/teamcity/teamcity-coverage.spec.js
+++ b/services/teamcity/teamcity-coverage.spec.js
@@ -4,13 +4,13 @@ const { expect } = require('chai')
 const nock = require('nock')
 const { cleanUpNockAfterEach, defaultContext } = require('../test-helpers')
 const TeamCityCoverage = require('./teamcity-coverage.service')
-const { user, pass, config } = require('./teamcity-test-helpers')
+const { user, pass, host, config } = require('./teamcity-test-helpers')
 
 describe('TeamCityCoverage', function() {
   cleanUpNockAfterEach()
 
   it('sends the auth information as configured', async function() {
-    const scope = nock('https://mycompany.teamcity.com')
+    const scope = nock(`https://${host}`)
       .get(
         `/app/rest/builds/${encodeURIComponent(
           'buildType:(id:bt678)'
diff --git a/services/teamcity/teamcity-test-helpers.js b/services/teamcity/teamcity-test-helpers.js
index 47cfe75a1df64e540048f6ee510444a4011cccac..21d7a8b3ad89c7f5b2fcb1d999022e1ca6c25414 100644
--- a/services/teamcity/teamcity-test-helpers.js
+++ b/services/teamcity/teamcity-test-helpers.js
@@ -2,10 +2,24 @@
 
 const user = 'admin'
 const pass = 'password'
-const config = { private: { teamcity_user: user, teamcity_pass: pass } }
+const host = 'mycompany.teamcity.com'
+const config = {
+  public: {
+    services: {
+      teamcity: {
+        authorizedOrigins: [`https://${host}`],
+      },
+    },
+  },
+  private: {
+    teamcity_user: user,
+    teamcity_pass: pass,
+  },
+}
 
 module.exports = {
   user,
   pass,
+  host,
   config,
 }
diff --git a/services/wheelmap/wheelmap.service.js b/services/wheelmap/wheelmap.service.js
index b3702f0a9aba87399c5f59fefcb6f0ca1e1fe631..e9158ad5d88de72b9d39afe65b6bfeb2d9883d5f 100644
--- a/services/wheelmap/wheelmap.service.js
+++ b/services/wheelmap/wheelmap.service.js
@@ -22,7 +22,11 @@ module.exports = class Wheelmap extends BaseJsonService {
   }
 
   static get auth() {
-    return { passKey: 'wheelmap_token', isRequired: true }
+    return {
+      passKey: 'wheelmap_token',
+      authorizedOrigins: ['https://wheelmap.org'],
+      isRequired: true,
+    }
   }
 
   static get examples() {
@@ -52,15 +56,19 @@ module.exports = class Wheelmap extends BaseJsonService {
   }
 
   async fetch({ nodeId }) {
-    return this._requestJson({
-      schema,
-      url: `https://wheelmap.org/api/nodes/${nodeId}`,
-      options: { qs: { api_key: this.authHelper.pass } },
-      errorMessages: {
-        401: 'invalid token',
-        404: 'node not found',
-      },
-    })
+    return this._requestJson(
+      this.authHelper.withQueryStringAuth(
+        { passKey: 'api_key' },
+        {
+          schema,
+          url: `https://wheelmap.org/api/nodes/${nodeId}`,
+          errorMessages: {
+            401: 'invalid token',
+            404: 'node not found',
+          },
+        }
+      )
+    )
   }
 
   async handle({ nodeId }) {