[BREAKING] Create a suite for kubernetes tests.

Authelia client uses hash router instead of browser router in order to work
with Kubernetes nginx-ingress-controller. This is also better for users having
old browsers.

This commit is breaking because it requires to change the configuration of the
proxy to include the # in the URL of the login portal.
pull/331/head
Clement Michaud 2019-03-03 23:51:52 +01:00
parent f8a12b8482
commit 76fa325f08
157 changed files with 1890 additions and 1762 deletions

1
.gitignore vendored
View File

@ -38,3 +38,4 @@ Configuration.schema.json
users_database.test.yml users_database.test.yml
.suite .suite
.kube

View File

@ -15,18 +15,16 @@ addons:
hosts: hosts:
- admin.example.com - admin.example.com
- login.example.com - login.example.com
- single_factor.example.com - singlefactor.example.com
- dev.example.com - dev.example.com
- home.example.com - home.example.com
- mx1.mail.example.com - mx1.mail.example.com
- mx2.mail.example.com - mx2.mail.example.com
- public.example.com - public.example.com
- secure.example.com
- authelia.example.com - authelia.example.com
- admin.example.com - admin.example.com
- mail.example.com
before_install:
- npm install -g npm@'>=2.13.5'
- pushd client && npm install && popd
before_script: before_script:
- export DISPLAY=:99.0 - export DISPLAY=:99.0

View File

@ -41,14 +41,18 @@ For more details about the features, follow [Features](./docs/features.md).
## Getting Started ## Getting Started
If you want to quickly test Authelia with Docker, we recommend you read You can start off with
[Getting Started](./docs/getting-started.md).
source bootstrap.sh
If you want to go further, please read [Getting Started](./docs/getting-started.md).
## Deployment ## Deployment
Now that you have tested **Authelia** and you want to try it out in your own infrastructure, you can learn how to deploy and use it with Now that you have tested **Authelia** and you want to try it out in your own infrastructure,
[Deployment](./docs/deployment-production.md). This guide will show you how to deploy you can learn how to deploy and use it with [Deployment](./docs/deployment-production.md).
it on bare metal as well as on Kubernetes. This guide will show you how to deploy it on bare metal as well as on
[Kubernetes](https://kubernetes.io/).
## Security ## Security

49
bootstrap.sh 100644
View File

@ -0,0 +1,49 @@
export PATH=$(pwd)/scripts:/tmp:$PATH
export PS1="(authelia) $PS1"
echo "[BOOTSTRAP] Installing npm packages..."
npm i
pushd client
npm i
popd
echo "[BOOTSTRAP] Checking if Docker is installed..."
if [ ! -x "$(command -v docker)" ];
then
echo "[ERROR] You must install docker on your machine.";
return
fi
echo "[BOOTSTRAP] Checking if docker-compose is installed..."
if [ ! -x "$(command -v docker-compose)" ];
then
echo "[ERROR] You must install docker-compose on your machine.";
return;
fi
echo "[BOOTSTRAP] Checking if example.com domain is forwarded to your machine..."
cat /etc/hosts | grep "login.example.com" > /dev/null
if [ $? -ne 0 ];
then
echo "[ERROR] Please add those lines to /etc/hosts:
127.0.0.1 home.example.com
127.0.0.1 public.example.com
127.0.0.1 secure.example.com
127.0.0.1 dev.example.com
127.0.0.1 admin.example.com
127.0.0.1 mx1.mail.example.com
127.0.0.1 mx2.mail.example.com
127.0.0.1 singlefactor.example.com
127.0.0.1 login.example.com"
return;
fi
echo "[BOOTSTRAP] Running additional bootstrap steps..."
authelia-scripts bootstrap
echo "[BOOTSTRAP] Run 'authelia-scripts suites start dockerhub' to start Authelia and visit https://home.example.com:8080."
echo "[BOOTSTRAP] More details at https://github.com/clems4ever/authelia/blob/master/docs/getting-started.md"

View File

@ -3,14 +3,14 @@ import './App.scss';
import { Route, Switch } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import { routes } from './routes/index'; import { routes } from './routes/index';
import { createBrowserHistory } from 'history'; import { createHashHistory } from 'history';
import { createStore, applyMiddleware, compose } from 'redux'; import { createStore, applyMiddleware, compose } from 'redux';
import reducer from './reducers'; import reducer from './reducers';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import { routerMiddleware, ConnectedRouter } from 'connected-react-router'; import { routerMiddleware, ConnectedRouter } from 'connected-react-router';
const history = createBrowserHistory(); const history = createHashHistory();
const store = createStore( const store = createStore(
reducer(history), reducer(history),
compose( compose(

View File

@ -1,15 +0,0 @@
import { Dispatch } from "redux";
import * as AutheliaService from '../services/AutheliaService';
export default async function(url: string) {
try {
// Check the url against the backend before redirecting.
await AutheliaService.checkRedirection(url);
window.location.href = url;
} catch (e) {
console.error(
'Cannot redirect since the URL is not in the protected domain.' +
'This behavior could be malicious so please the issue to an administrator.');
throw e;
}
}

View File

@ -7,7 +7,7 @@ import CircleLoader, { Status } from "../CircleLoader/CircleLoader";
export interface OwnProps { export interface OwnProps {
username: string; username: string;
redirectionUrl: string; redirectionUrl: string | null;
} }
export interface DispatchProps { export interface DispatchProps {
@ -27,7 +27,7 @@ class AlreadyAuthenticated extends Component<Props> {
</div> </div>
<div className={styles.statusIcon}><CircleLoader status={Status.SUCCESSFUL} /></div> <div className={styles.statusIcon}><CircleLoader status={Status.SUCCESSFUL} /></div>
</div> </div>
<a href={this.props.redirectionUrl}>{this.props.redirectionUrl}</a> {(this.props.redirectionUrl) ? <a href={this.props.redirectionUrl}>{this.props.redirectionUrl}</a> : null}
<div className={styles.logoutButtonContainer}> <div className={styles.logoutButtonContainer}>
<Button <Button
onClick={this.props.onLogoutClicked} onClick={this.props.onLogoutClicked}

View File

@ -20,7 +20,7 @@ export interface StateProps {
} }
export interface DispatchProps { export interface DispatchProps {
onAuthenticationRequested(username: string, password: string, rememberMe: boolean): void; onAuthenticationRequested(username: string, password: string, rememberMe: boolean): Promise<void>;
} }
export type Props = OwnProps & StateProps & DispatchProps; export type Props = OwnProps & StateProps & DispatchProps;
@ -136,7 +136,11 @@ class FirstFactorForm extends Component<Props, State> {
this.props.onAuthenticationRequested( this.props.onAuthenticationRequested(
this.state.username, this.state.username,
this.state.password, this.state.password,
this.state.rememberMe); this.state.rememberMe)
.catch((err: Error) => console.error(err))
.finally(() => {
this.setState({username: '', password: ''});
})
} }
} }

View File

@ -6,7 +6,6 @@ import { RootState } from '../../../reducers';
import * as AutheliaService from '../../../services/AutheliaService'; import * as AutheliaService from '../../../services/AutheliaService';
import to from 'await-to-js'; import to from 'await-to-js';
import FetchStateBehavior from '../../../behaviors/FetchStateBehavior'; import FetchStateBehavior from '../../../behaviors/FetchStateBehavior';
import SafelyRedirectBehavior from '../../../behaviors/SafelyRedirectBehavior';
const mapStateToProps = (state: RootState): StateProps => { const mapStateToProps = (state: RootState): StateProps => {
return { return {
@ -16,7 +15,7 @@ const mapStateToProps = (state: RootState): StateProps => {
} }
function onAuthenticationRequested(dispatch: Dispatch, redirectionUrl: string | null) { function onAuthenticationRequested(dispatch: Dispatch, redirectionUrl: string | null) {
return async (username: string, password: string, rememberMe: boolean) => { return async (username: string, password: string, rememberMe: boolean): Promise<void> => {
let err, res; let err, res;
// Validate first factor // Validate first factor
@ -26,32 +25,37 @@ function onAuthenticationRequested(dispatch: Dispatch, redirectionUrl: string |
if (err) { if (err) {
await dispatch(authenticateFailure(err.message)); await dispatch(authenticateFailure(err.message));
return; throw new Error(err.message);
} }
if (!res) { if (!res) {
await dispatch(authenticateFailure('No response')); await dispatch(authenticateFailure('No response'));
return; throw new Error('No response');
} }
if (res.status === 200) { if (res.status === 200) {
const json = await res.json(); const json = await res.json();
if ('error' in json) { if ('error' in json) {
await dispatch(authenticateFailure(json['error'])); await dispatch(authenticateFailure(json['error']));
return; throw new Error(json['error']);
} }
dispatch(authenticateSuccess());
if ('redirect' in json) { if ('redirect' in json) {
window.location.href = json['redirect']; window.location.href = json['redirect'];
return; return;
} }
// fetch state to move to next stage in case redirect is not possible
await FetchStateBehavior(dispatch);
} else if (res.status === 204) { } else if (res.status === 204) {
dispatch(authenticateSuccess()); dispatch(authenticateSuccess());
// fetch state to move to next stage // fetch state to move to next stage
FetchStateBehavior(dispatch); await FetchStateBehavior(dispatch);
} else { } else {
dispatch(authenticateFailure('Unknown error')); dispatch(authenticateFailure('Unknown error'));
throw new Error('Unknown error... (' + res.status + ')');
} }
} }
} }

View File

@ -3,13 +3,20 @@ import { RootState } from '../../../reducers';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import u2fApi from 'u2f-api'; import u2fApi from 'u2f-api';
import to from 'await-to-js'; import to from 'await-to-js';
import { securityKeySignSuccess, securityKeySign, securityKeySignFailure, setSecurityKeySupported, oneTimePasswordVerification, oneTimePasswordVerificationFailure, oneTimePasswordVerificationSuccess } from '../../../reducers/Portal/SecondFactor/actions'; import {
securityKeySignSuccess,
securityKeySign,
securityKeySignFailure,
setSecurityKeySupported,
oneTimePasswordVerification,
oneTimePasswordVerificationFailure,
oneTimePasswordVerificationSuccess
} from '../../../reducers/Portal/SecondFactor/actions';
import SecondFactorForm, { OwnProps, StateProps } from '../../../components/SecondFactorForm/SecondFactorForm'; import SecondFactorForm, { OwnProps, StateProps } from '../../../components/SecondFactorForm/SecondFactorForm';
import * as AutheliaService from '../../../services/AutheliaService'; import * as AutheliaService from '../../../services/AutheliaService';
import { push } from 'connected-react-router'; import { push } from 'connected-react-router';
import fetchState from '../../../behaviors/FetchStateBehavior'; import fetchState from '../../../behaviors/FetchStateBehavior';
import LogoutBehavior from '../../../behaviors/LogoutBehavior'; import LogoutBehavior from '../../../behaviors/LogoutBehavior';
import SafelyRedirectBehavior from '../../../behaviors/SafelyRedirectBehavior';
const mapStateToProps = (state: RootState): StateProps => ({ const mapStateToProps = (state: RootState): StateProps => ({
securityKeySupported: state.secondFactor.securityKeySupported, securityKeySupported: state.secondFactor.securityKeySupported,
@ -20,7 +27,7 @@ const mapStateToProps = (state: RootState): StateProps => ({
oneTimePasswordVerificationError: state.secondFactor.oneTimePasswordVerificationError, oneTimePasswordVerificationError: state.secondFactor.oneTimePasswordVerificationError,
}); });
async function triggerSecurityKeySigning(dispatch: Dispatch) { async function triggerSecurityKeySigning(dispatch: Dispatch, redirectionUrl: string | null) {
let err, result; let err, result;
dispatch(securityKeySign()); dispatch(securityKeySign());
[err, result] = await to(AutheliaService.requestSigning()); [err, result] = await to(AutheliaService.requestSigning());
@ -45,26 +52,40 @@ async function triggerSecurityKeySigning(dispatch: Dispatch) {
throw 'No response'; throw 'No response';
} }
[err, result] = await to(AutheliaService.completeSecurityKeySigning(result)); [err, result] = await to(AutheliaService.completeSecurityKeySigning(result, redirectionUrl));
if (err) { if (err) {
await dispatch(securityKeySignFailure(err.message)); await dispatch(securityKeySignFailure(err.message));
throw err; throw err;
} }
await dispatch(securityKeySignSuccess());
try {
await redirectIfPossible(dispatch, result as Response);
dispatch(securityKeySignSuccess());
await handleSuccess(dispatch, 1000);
} catch (err) {
dispatch(securityKeySignFailure(err.message));
}
} }
async function handleSuccess(dispatch: Dispatch, ownProps: OwnProps, duration?: number) { async function redirectIfPossible(dispatch: Dispatch, res: Response) {
if (res.status === 204) return;
const body = await res.json();
if ('error' in body) {
throw new Error(body['error']);
}
if ('redirect' in body) {
window.location.href = body['redirect'];
return;
}
return;
}
async function handleSuccess(dispatch: Dispatch, duration?: number) {
async function handle() { async function handle() {
if (ownProps.redirectionUrl) {
try {
await SafelyRedirectBehavior(ownProps.redirectionUrl);
} catch (e) {
await fetchState(dispatch); await fetchState(dispatch);
} }
} else {
await fetchState(dispatch);
}
}
if (duration) { if (duration) {
setTimeout(handle, duration); setTimeout(handle, duration);
@ -88,14 +109,13 @@ const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps) => {
const isU2FSupported = await u2fApi.isSupported(); const isU2FSupported = await u2fApi.isSupported();
if (isU2FSupported) { if (isU2FSupported) {
await dispatch(setSecurityKeySupported(true)); await dispatch(setSecurityKeySupported(true));
await triggerSecurityKeySigning(dispatch); await triggerSecurityKeySigning(dispatch, ownProps.redirectionUrl);
await handleSuccess(dispatch, ownProps, 1000);
} }
}, },
onOneTimePasswordValidationRequested: async (token: string) => { onOneTimePasswordValidationRequested: async (token: string) => {
let err, res; let err, res;
dispatch(oneTimePasswordVerification()); dispatch(oneTimePasswordVerification());
[err, res] = await to(AutheliaService.verifyTotpToken(token)); [err, res] = await to(AutheliaService.verifyTotpToken(token, ownProps.redirectionUrl));
if (err) { if (err) {
dispatch(oneTimePasswordVerificationFailure(err.message)); dispatch(oneTimePasswordVerificationFailure(err.message));
throw err; throw err;
@ -105,13 +125,13 @@ const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps) => {
throw 'No response'; throw 'No response';
} }
const body = await res.json(); try {
if ('error' in body) { await redirectIfPossible(dispatch, res);
dispatch(oneTimePasswordVerificationFailure(body['error']));
throw body['error'];
}
dispatch(oneTimePasswordVerificationSuccess()); dispatch(oneTimePasswordVerificationSuccess());
await handleSuccess(dispatch, ownProps); await handleSuccess(dispatch);
} catch (err) {
dispatch(oneTimePasswordVerificationFailure(err.message));
}
}, },
} }
} }

View File

@ -75,24 +75,36 @@ export async function requestSigning() {
}); });
} }
export async function completeSecurityKeySigning(response: u2fApi.SignResponse) { export async function completeSecurityKeySigning(
return fetchSafe('/api/u2f/sign', { response: u2fApi.SignResponse, redirectionUrl: string | null) {
method: 'POST',
headers: { const headers: Record<string, string> = {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, }
if (redirectionUrl) {
headers['X-Target-Url'] = redirectionUrl;
}
return fetchSafe('/api/u2f/sign', {
method: 'POST',
headers: headers,
body: JSON.stringify(response), body: JSON.stringify(response),
}); });
} }
export async function verifyTotpToken(token: string) { export async function verifyTotpToken(
return fetchSafe('/api/totp', { token: string, redirectionUrl: string | null) {
method: 'POST',
headers: { const headers: Record<string, string> = {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, }
if (redirectionUrl) {
headers['X-Target-Url'] = redirectionUrl;
}
return fetchSafe('/api/totp', {
method: 'POST',
headers: headers,
body: JSON.stringify({token}), body: JSON.stringify({token}),
}) })
} }
@ -124,23 +136,3 @@ export async function resetPassword(newPassword: string) {
body: JSON.stringify({password: newPassword}) body: JSON.stringify({password: newPassword})
}); });
} }
export async function checkRedirection(url: string) {
const res = await fetch('/api/redirect', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({url})
})
if (res.status !== 200) {
throw new Error('Status code ' + res.status);
}
const text = await res.text();
if (text !== 'OK') {
throw new Error('Cannot redirect');
}
return;
}

View File

@ -40,7 +40,7 @@ class AuthenticationView extends Component<Props> {
} else if (this.props.stage === Stage.ALREADY_AUTHENTICATED) { } else if (this.props.stage === Stage.ALREADY_AUTHENTICATED) {
return <AlreadyAuthenticated return <AlreadyAuthenticated
username={this.props.remoteState.username} username={this.props.remoteState.username}
redirectionUrl={this.props.remoteState.default_redirection_url} />; redirectionUrl={this.props.redirectionUrl} />;
} }
return <FirstFactorForm redirectionUrl={this.props.redirectionUrl} />; return <FirstFactorForm redirectionUrl={this.props.redirectionUrl} />;
} }

View File

@ -130,8 +130,10 @@ access_control:
rules: rules:
# Rules applied to everyone # Rules applied to everyone
- domain: public.example.com - domain: public.example.com
policy: bypass
- domain: secure.example.com
policy: two_factor policy: two_factor
- domain: single_factor.example.com - domain: singlefactor.example.com
policy: one_factor policy: one_factor
# Rules applied to 'admin' group # Rules applied to 'admin' group

View File

@ -4,17 +4,20 @@ Authelia comes with a set of dedicated scripts doing a broad range of operations
building the distributed version of Authelia, building the Docker image, running suites, building the distributed version of Authelia, building the Docker image, running suites,
testing the code, etc... testing the code, etc...
You can access the scripts usage by running the following command: Those scripts becomes available after sourcing the bootstrap.sh script with
npm run scripts source bootstrap.sh
Then, you can access the scripts usage by running the following command:
authelia-scripts --help
For instance, you can build Authelia with: For instance, you can build Authelia with:
npm run scripts build authelia-scripts build
Or start the *basic* suite with: Or start the *basic* suite with:
npm run scripts suites start basic authelia-scripts suites start basic
You will find more information in the scripts usage helpers. You will find more information in the scripts usage helpers.

View File

@ -0,0 +1,54 @@
# Build and dev
**Authelia** is written in Typescript and built with [Authelia scripts](./docs/authelia-scripts.md).
In order to build and contribute to **Authelia**, you need to make sure Node with version >= 8 and < 10
and NPM is installed on your machine.
## Build
**Authelia** is made of two parts: the frontend and the backend.
The frontend is a [React](https://reactjs.org/) application written in Typescript and
the backend is an express application also written in Typescript.
The following command builds **Authelia** under dist/:
authelia-scripts build
And then you can also build the Docker image with:
authelia-scripts docker build
## Development
In order to ease development, Authelia uses the concept of [suites]. This is
a kind of virutal environment for **Authelia**, it allows you to run **Authelia** in a complete
environment, develop and test your patches. A hot-reload feature has been implemented so that
you can test your changes in realtime.
The next command will start the suite called [basic](./test/suites/basic/README.md):
authelia-scripts suites start basic
Then, edit the code and observe how **Authelia** is automatically updated.
### Unit tests
To run the unit tests written in Mocha, run:
authelia-scripts unittest
### Integration tests
Integration tests also run with Mocha and are based on Selenium. They generally
require a complete environment made of several components like redis, mongo and a LDAP
to run. That's why [suites] have been created. At this point, the *basic* suite should
already be running and you can run the tests related to this suite with the following
command:
authelia-scripts suites test
[suites]: ./suites.md

View File

@ -1,53 +0,0 @@
# Build
**Authelia** is written in Typescript and built with [Authelia scripts](docs/authelia-scripts.md).
In order to build **Authelia**, you need to make sure Node with version >= 8 and < 10 and NPM is
installed on your machine.
Then, run the following command to install the node modules:
npm install
And, this command to build **Authelia** under dist/:
npm run build
Then you can also build the Docker image with:
npm run docker build
## Details
### Build
**Authelia** is made of two parts: the frontend and the backend.
The frontend is a [React](https://reactjs.org/) application written in Typescript and
the backend is an express application also written in Typescript.
### Tests
There are two kind of tests: unit tests and integration tests.
### Unit tests
To run the unit tests, run:
npm run unittest
### Integration tests
Integration tests run with Mocha and are based on Selenium. They generally
require a complete environment made of several components like redis, mongo and a LDAP
to run.
In order to simplify the creation of such environments, Authelia comes with a concept of
[Suites] that basically act as virtual environments for running either
manual or integration tests.
Please read the documentation related to [Suites] in order to discover
how to run related tests.
[Suites]: ./suites.md

View File

@ -29,14 +29,13 @@ the root of the repo.
### Deploy With Docker ### Deploy With Docker
docker pull clems4ever/authelia
docker run -v /path/to/your/config.yml:/etc/authelia/config.yml clems4ever/authelia docker run -v /path/to/your/config.yml:/etc/authelia/config.yml clems4ever/authelia
## On top of Kubernetes ## On top of Kubernetes
<img src="/images/kube-logo.png" width="24" align="left"> <img src="/images/kube-logo.png" width="24" align="left">
**Authelia** can also be used on top of [Kubernetes] using **Authelia** can also be installed on top of [Kubernetes] using
[nginx ingress controller](https://github.com/kubernetes/ingress-nginx). [nginx ingress controller](https://github.com/kubernetes/ingress-nginx).
Please refer to the following [documentation](../example/kube/README.md) Please refer to the following [documentation](../example/kube/README.md)

View File

@ -3,61 +3,17 @@
**Authelia** can be tested in a matter of seconds with docker-compose based **Authelia** can be tested in a matter of seconds with docker-compose based
on the latest image available on [Dockerhub]. on the latest image available on [Dockerhub].
## Pre-requisites In order to deploy the latest release locally, run the following command and
follow the instructions of bootstrap.sh:
In order to test **Authelia**, we need to make sure that: source bootstrap.sh
- **Docker** and **docker-compose** are installed on your computer.
- Ports 8080 and 8085 are not already used on your machine.
- Some subdomains of **example.com** redirect to your test infrastructure.
### Docker & docker-compose Then, start the *dockerhub* [suite].
Make sure you have **docker** and **docker-compose** installed on your authelia-scripts suites start dockerhub
machine.
Here are the versions used for testing in Travis:
$ docker --version A [suite] is kind of a virtual environment for running Authelia.
Docker version 17.03.1-ce, build c6d412e If you want more details please read the related [documentation](./suites.md).
$ docker-compose --version
docker-compose version 1.14.0, build c7bdf9e
### Available port
Make sure you don't have anything listening on port 8080 and 8085.
The port 8080 will be our frontend load balancer serving both **Authelia**'s portal and the
applications we want to protect.
The port 8085 is serving a webmail used to receive emails sent by **Authelia**
to validate your identity when registering U2F or TOTP secrets or when
resetting your password.
### Subdomain aliases
In order to simulate the behavior of a DNS resolving some test subdomains of **example.com**
to your machine, we need to add the following lines to your **/etc/hosts**. It will alias the
subdomains so that nginx can redirect requests to the correct virtual host.
127.0.0.1 home.example.com
127.0.0.1 public.example.com
127.0.0.1 dev.example.com
127.0.0.1 admin.example.com
127.0.0.1 mx1.mail.example.com
127.0.0.1 mx2.mail.example.com
127.0.0.1 single_factor.example.com
127.0.0.1 login.example.com
## Deploy
To deploy **Authelia** using the latest image from [Dockerhub], run the
following command:
npm install commander
npm run scripts suites start dockerhub
A Suites is a virtual environment for running Authelia. If you want more details please
read the related [documentation](./suites.md).
## Test it! ## Test it!
@ -77,25 +33,52 @@ Below is what the login page looks like after you accepted all exceptions:
</p> </p>
You can use one of the users listed in [https://home.example.com:8080/](https://home.example.com:8080/). You can use one of the users listed in [https://home.example.com:8080/](https://home.example.com:8080/).
The rights granted to each user and group is also provided there. The rights granted to each user and group is also provided in the page as
a list of rules.
At some point, you'll be required to register your second factor, either At some point, you'll be required to register your second factor device.
U2F or TOTP. Since your security is **Authelia**'s priority, it will send Since your security is **Authelia**'s priority, it will send
an email to the email address of the user to confirm the user identity. an email to the email address of the user to confirm the user identity.
Since we're running a test environment, we provide a fake webmail called Since we're running a test environment, we provide a fake webmail called
*MailCatcher* from which you can checkout the email and confirm *MailCatcher* from which you can checkout the email and confirm
your identity. your identity.
The webmail is accessible from The webmail is accessible from
[http://localhost:8085](http://localhost:8085). [http://mail.example.com:8080](http://mail.example.com:8085).
**Note:** If you cannot deploy the fake webmail for any reason. You can
configure **Authelia** to use the filesystem notifier (option available
in [config.template.yml]) that will send the content of the email in a
file instead of sending an email. It is advised to not use this option
in production.
Enjoy! Enjoy!
## FAQ
### What version of Docker and docker-compose should I use?
Here are the versions used for testing in Travis:
$ docker --version
Docker version 17.03.1-ce, build c6d412e
$ docker-compose --version
docker-compose version 1.14.0, build c7bdf9e
### How am I supposed to access the subdomains of example.com?
Well, in order to test Authelia, we will fake your browser that example.com is
served by your machine. To do that, open */etc/hosts* and append the following
lines:
127.0.0.1 home.example.com
127.0.0.1 public.example.com
127.0.0.1 secure.example.com
127.0.0.1 dev.example.com
127.0.0.1 admin.example.com
127.0.0.1 mx1.mail.example.com
127.0.0.1 mx2.mail.example.com
127.0.0.1 singlefactor.example.com
127.0.0.1 login.example.com
### What should I do if I want to contribute?
You can refer to the dedicated documentation [here](./build-and-dev.md).
[config.template.yml]: ../config.template.yml [config.template.yml]: ../config.template.yml
[DockerHub]: https://hub.docker.com/r/clems4ever/authelia/ [DockerHub]: https://hub.docker.com/r/clems4ever/authelia/
[Build]: ./build.md [suite]: ./suites.md

View File

@ -1,18 +1,18 @@
# Suites # Suites
Authelia is a single component in interaction with many others. Consequently, testing the features Authelia is a single component in interaction with many others. Consequently, testing the features
is not as easy as we might think. Consequently, a suite is kind of a virtual environment for Authelia, is not as easy as we might think. In order to solve this problem, Authelia came up with the concept of
it allows to create an environment made of components such as nginx, redis or mongo in which Authelia can suite which is a kind of virtual environment for Authelia, it allows to create an environment made of
run and be tested. components such as nginx, redis or mongo in which Authelia can run and be tested.
This abstraction allows to prepare an environment for manual testing during development and also to This abstraction allows to prepare an environment for manual testing during development and also to
craft and run integration tests. craft and run integration tests.
## Start a suite. ## Start a suite.
Starting a suite called *simple* is done with the following command: Starting a suite called *basic* is done with the following command:
npm run scripts suites start simple authelia-scripts suites start basic
It will start the suite and block until you hit ctrl-c to stop the suite. It will start the suite and block until you hit ctrl-c to stop the suite.
@ -20,9 +20,9 @@ It will start the suite and block until you hit ctrl-c to stop the suite.
### Run tests of running suite ### Run tests of running suite
If you are already running a suite with the previous command, you can simply type: If a suite is already running, you can simply type:
npm run scripts test authelia-scripts suites test
and this will run the tests related to the running suite. and this will run the tests related to the running suite.
@ -31,18 +31,26 @@ and this will run the tests related to the running suite.
However, if no suite is running and you still want to test a particular suite like *complete*. However, if no suite is running and you still want to test a particular suite like *complete*.
You can do so with the next command: You can do so with the next command:
npm run scripts test complete authelia-scripts suites test complete
This command will run the tests for the *complete* suite using the built version of Authelia that This command will run the tests for the *complete* suite using the built version of Authelia that
should be located in *dist*. should be located in *dist*.
WARNING: Authelia must be built before running this command. WARNING: Authelia must be built with `authelia-scripts build` and possibly
`authelia-scripts docker build` before running this command.
### Run all tests of all suites ### Run all tests of all suites
Running all tests is as easy as making sure that there is no running suite and typing: Running all tests is easy. Make sure that no suite is already running and run:
npm run scripts test authelia-scripts suites test
### Run tests in headless mode
In order to run the tests without seeing the windows creating and vanishing, one
can run the tests in headless mode with:
authelia-scripts suites test --headless
## Create a suite ## Create a suite
@ -51,5 +59,5 @@ Creating a suite is as easy as creating a new directory with at least two files:
* **environment.ts** - It defines the setup and teardown phases when creating the environment. The *setup* * **environment.ts** - It defines the setup and teardown phases when creating the environment. The *setup*
phase is the phase when the required components will be spawned and Authelia will start while the *teardown* phase is the phase when the required components will be spawned and Authelia will start while the *teardown*
is executed when the suite is destroyed (ctrl-c hit by the user). is executed when the suite is destroyed (ctrl-c hit by the user) or the tests are finished.
* **test.ts** - It defines a set of tests to run in the virtual environment of the suite. * **test.ts** - It defines a set of tests to run against the suite.

View File

@ -0,0 +1,4 @@
FROM nginx:alpine
ADD ./html /usr/share/nginx/html
ADD ./nginx.conf /etc/nginx/nginx.conf

View File

@ -1,9 +1,6 @@
version: '2' version: '2'
services: services:
nginx-backend: nginx-backend:
image: nginx:alpine image: authelia-example-backend
volumes:
- ./example/compose/nginx/backend/html:/usr/share/nginx/html
- ./example/compose/nginx/backend/nginx.conf:/etc/nginx/nginx.conf
networks: networks:
- authelianet - authelianet

View File

@ -15,7 +15,13 @@
public.example.com <a href="https://public.example.com:8080/"> / index.html</a> public.example.com <a href="https://public.example.com:8080/"> / index.html</a>
</li> </li>
<li> <li>
secret.example.com secure.example.com <a href="https://secure.example.com:8080/"> / secret.html</a>
</li>
<li>
singlefactor.example.com <a href="https://singlefactor.example.com:8080/secret.html"> / secret.html</a>
</li>
<li>
dev.example.com
<ul> <ul>
<li>Groups <li>Groups
<ul> <ul>
@ -51,12 +57,9 @@
<li> <li>
mx2.main.example.com <a href="https://mx2.mail.example.com:8080/secret.html"> / secret.html</a> mx2.main.example.com <a href="https://mx2.mail.example.com:8080/secret.html"> / secret.html</a>
</li> </li>
<li>
single_factor.example.com <a href="https://single_factor.example.com:8080/secret.html"> / secret.html</a>
</li>
</ul> </ul>
You can also log off by visiting the following <a href="https://login.example.com:8080/logout?rd=https://home.example.com:8080/">link</a>. You can also log off by visiting the following <a href="https://login.example.com:8080/#/logout?rd=https://home.example.com:8080/">link</a>.
<h1>List of users</h1> <h1>List of users</h1>
Here is the list of credentials you can log in with to test access control.<br/> Here is the list of credentials you can log in with to test access control.<br/>
@ -74,61 +77,57 @@
<p></p>These rules are extracted from the configuration file <p></p>These rules are extracted from the configuration file
<a href="https://github.com/clems4ever/authelia/blob/master/config.template.yml">config.template.yml</a>.</p> <a href="https://github.com/clems4ever/authelia/blob/master/config.template.yml">config.template.yml</a>.</p>
<pre id="rules" style="border: 1px grey solid; padding: 20px; display: inline-block;"> <pre id="rules" style="border: 1px grey solid; padding: 20px; display: inline-block;">
# Default policy can either be `allow` or `deny`. default_policy: deny
# It is the policy applied to any resource if it has not been overriden
# in the `any`, `groups` or `users` category.
default_policy: deny rules:
# Rules applied to everyone
# The rules that apply to anyone.
# The value is a list of rules.
any:
- domain: public.example.com - domain: public.example.com
policy: allow policy: bypass
- domain: secure.example.com
policy: two_factor
- domain: singlefactor.example.com
policy: one_factor
# Group-based rules. The key is a group name and the value # Rules applied to 'admin' group
# is a list of rules.
groups:
admin:
# All resources in all domains
- domain: '*.example.com'
policy: allow
# Except mx2.mail.example.com (it restricts the first rule)
- domain: 'mx2.mail.example.com' - domain: 'mx2.mail.example.com'
subject: 'group:admin'
policy: deny policy: deny
dev: - domain: '*.example.com'
subject: 'group:admin'
policy: two_factor
# Rules applied to 'dev' group
- domain: dev.example.com - domain: dev.example.com
policy: allow
resources: resources:
- '^/groups/dev/.*$' - '^/groups/dev/.*$'
subject: 'group:dev'
policy: two_factor
# User-based rules. The key is a user name and the value # Rules applied to user 'john'
# is a list of rules.
users:
john:
- domain: dev.example.com - domain: dev.example.com
policy: allow
resources: resources:
- '^/users/john/.*$' - '^/users/john/.*$'
harry: subject: 'user:john'
policy: two_factor
# Rules applied to user 'harry'
- domain: dev.example.com - domain: dev.example.com
policy: allow
resources: resources:
- '^/users/harry/.*$' - '^/users/harry/.*$'
bob: subject: 'user:harry'
policy: two_factor
# Rules applied to user 'bob'
- domain: '*.mail.example.com' - domain: '*.mail.example.com'
policy: allow subject: 'user:bob'
policy: two_factor
- domain: 'dev.example.com' - domain: 'dev.example.com'
policy: allow
resources: resources:
- '^/users/bob/.*$' - '^/users/bob/.*$'
- domain: 'dev.example.com' subject: 'user:bob'
policy: allow policy: two_factor
resources: </pre>
- '^/users/harry/.*$'</pre>
</body> </body>
</html> </html>

View File

@ -4,7 +4,6 @@
<link rel="icon" href="/icon.png" type="image/png" /> <link rel="icon" href="/icon.png" type="image/png" />
</head> </head>
<body> <body>
<h1>Secret</h1>
This is a very important secret!<br/> This is a very important secret!<br/>
Go back to <a href="https://home.example.com:8080/">home page</a>. Go back to <a href="https://home.example.com:8080/">home page</a>.
</body> </body>

View File

@ -18,6 +18,12 @@ http {
server_name public.example.com; server_name public.example.com;
} }
server {
listen 80;
root /usr/share/nginx/html/secure;
server_name secure.example.com;
}
server { server {
listen 80; listen 80;
root /usr/share/nginx/html/admin; root /usr/share/nginx/html/admin;
@ -38,8 +44,8 @@ http {
server { server {
listen 80; listen 80;
root /usr/share/nginx/html/single_factor; root /usr/share/nginx/html/singlefactor;
server_name single_factor.example.com; server_name singlefactor.example.com;
} }
} }

View File

@ -1,12 +0,0 @@
version: '2'
services:
nginx-portal:
image: nginx:alpine
volumes:
- ./example/compose/nginx/minimal/nginx.conf:/etc/nginx/nginx.conf
- ./example/compose/nginx/minimal/html:/usr/share/nginx/html
- ./example/compose/nginx/minimal/ssl:/etc/ssl
ports:
- "8080:443"
networks:
- authelianet

View File

@ -1,32 +0,0 @@
<!DOCTYPE>
<html>
<head>
<title>Home page</title>
<link rel="icon" href="/icon.png" type="image/png" />
</head>
<body>
<h1>Access the secret</h1>
<span style="font-size: 1.2em; color: red">You need to log in to access the secret!</span><br/><br/> Try to access it using
the following links to test Authelia.<br/>
<ul>
<li>
admin.example.com <a href="https://admin.example.com:8080/secret.html"> / secret.html</a>
</li>
</ul>
You can also log off by visiting the following <a href="https://login.example.com:8080/logout?rd=https://home.example.com:8080/">link</a>.
<h1>List of users</h1>
Here is the list of credentials you can log in with.<br/>
<br/> Once first factor is passed, you will need to follow the links to register a secret for the second factor.<br/> Authelia
will send you a fictituous email stored in a <strong>local file</strong> called <strong>/tmp/authelia/notification.txt</strong>.<br/>
It will provide you with the link to complete the registration allowing you to authenticate with 2-factor.
<ul>
<li><strong>john / password</strong>: belongs to <em>admin</em> and <em>dev</em> groups.</li>
<li><strong>bob / password</strong>: belongs to <em>dev</em> group only.</li>
<li><strong>harry / password</strong>: does not belong to any group.</li>
</ul>
</body>
</html>

View File

@ -1,96 +0,0 @@
worker_processes 1;
events {
worker_connections 1024;
}
http {
server {
listen 443 ssl;
server_name login.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_endpoint http://authelia:8080;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN";
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_intercept_errors on;
proxy_pass $upstream_endpoint;
if ($request_method !~ ^(POST)$){
error_page 401 = /error/401;
error_page 403 = /error/403;
error_page 404 = /error/404;
}
}
}
server {
listen 443 ssl;
server_name home.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_endpoint http://nginx-backend;
root /usr/share/nginx/html/home;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN";
}
server {
listen 443 ssl;
server_name admin.example.com;
root /usr/share/nginx/html/admin;
resolver 127.0.0.11 ipv6=off;
set $upstream_verify http://authelia:8080/api/verify;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN";
location /auth_verify {
internal;
proxy_set_header Host $http_host;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_pass $upstream_verify;
}
location / {
auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect;
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
error_page 401 =302 https://login.example.com:8080?rd=$redirect;
error_page 403 = https://login.example.com:8080/error/403;
}
}
}

View File

@ -1,11 +0,0 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBhDCB7gIBADBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh
MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEB
AQUAA4GNADCBiQKBgQDNloThcTVDIU1uscEdGFIDndqcCwnmYF4bs6bpJ1BxkBhq
GUNYRu12hjiHLSA80ZwhNxZ4T5YD4+81Gvs9zMoSGd4jJRSBX6evPTXR8zkmcQI/
EtN7WgXOXhTx3JiIGlPOI3OGJR+rvfc9FiNx30FC1wpw3gt2ZC+NQeedDvdPKwID
AQABoAAwDQYJKoZIhvcNAQELBQADgYEAmCX60kspIw1Zfb79AQOarFW5Q2K2h5Vx
/cRbDyHlKtbmG77EtICccULyqf76B1gNRw5Zq3lSotSUcLzsWcdesXCFDC7k87Qf
mpQKPj6GdTYJvdWf8aDwt32tAqWuBIRoAbdx5WbFPPWVfDcm7zDJefBrhNUDH0Qd
vcnxjvPMmOM=
-----END CERTIFICATE REQUEST-----

View File

@ -106,10 +106,79 @@ http {
} }
} }
server {
listen 443 ssl;
server_name mail.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_endpoint http://smtp:1080;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN";
location / {
proxy_set_header Host $http_host;
proxy_pass $upstream_endpoint;
}
}
server { server {
listen 443 ssl; listen 443 ssl;
server_name public.example.com; server_name public.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_verify <%= authelia_backend %>/api/verify;
set $upstream_endpoint http://nginx-backend;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN";
location /auth_verify {
internal;
proxy_set_header Host $http_host;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_pass $upstream_verify;
}
location / {
auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect;
auth_request_set $user $upstream_http_remote_user;
proxy_set_header X-Forwarded-User $user;
auth_request_set $groups $upstream_http_remote_groups;
proxy_set_header Remote-Groups $groups;
proxy_set_header Host $http_host;
error_page 401 =302 https://login.example.com:8080/#/?rd=$redirect;
proxy_pass $upstream_endpoint;
}
}
server {
listen 443 ssl;
server_name admin.example.com
secure.example.com;
resolver 127.0.0.11 ipv6=off; resolver 127.0.0.11 ipv6=off;
set $upstream_verify <%= authelia_backend %>/api/verify; set $upstream_verify <%= authelia_backend %>/api/verify;
set $upstream_endpoint http://nginx-backend; set $upstream_endpoint http://nginx-backend;
@ -150,8 +219,7 @@ http {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
error_page 401 =302 https://login.example.com:8080?rd=$redirect; error_page 401 =302 https://login.example.com:8080/#/?rd=$redirect;
error_page 403 = https://login.example.com:8080/error/403;
proxy_pass $upstream_endpoint; proxy_pass $upstream_endpoint;
} }
@ -167,63 +235,12 @@ http {
auth_request_set $groups $upstream_http_remote_groups; auth_request_set $groups $upstream_http_remote_groups;
proxy_set_header Custom-Forwarded-Groups $groups; proxy_set_header Custom-Forwarded-Groups $groups;
error_page 401 =302 https://login.example.com:8080?rd=$redirect; error_page 401 =302 https://login.example.com:8080/#/?rd=$redirect;
error_page 403 = https://login.example.com:8080/error/403;
proxy_pass $upstream_headers; proxy_pass $upstream_headers;
} }
} }
server {
listen 443 ssl;
server_name admin.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_verify <%= authelia_backend %>/api/verify;
set $upstream_endpoint http://nginx-backend;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN";
location /auth_verify {
internal;
proxy_set_header Host $http_host;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_pass $upstream_verify;
}
location / {
auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect;
auth_request_set $user $upstream_http_remote_user;
proxy_set_header X-Forwarded-User $user;
auth_request_set $groups $upstream_http_remote_groups;
proxy_set_header Remote-Groups $groups;
proxy_set_header Host $http_host;
error_page 401 =302 https://login.example.com:8080?rd=$redirect;
error_page 403 = https://login.example.com:8080/error/403;
proxy_pass $upstream_endpoint;
}
}
server { server {
listen 443 ssl; listen 443 ssl;
server_name dev.example.com; server_name dev.example.com;
@ -267,8 +284,7 @@ http {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
error_page 401 =302 https://login.example.com:8080?rd=$redirect; error_page 401 =302 https://login.example.com:8080/#/?rd=$redirect;
error_page 403 = https://login.example.com:8080/error/403;
proxy_pass $upstream_endpoint; proxy_pass $upstream_endpoint;
} }
@ -317,8 +333,7 @@ http {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
error_page 401 =302 https://login.example.com:8080?rd=$redirect; error_page 401 =302 https://login.example.com:8080/#/?rd=$redirect;
error_page 403 = https://login.example.com:8080/error/403;
proxy_pass $upstream_endpoint; proxy_pass $upstream_endpoint;
} }
@ -326,7 +341,7 @@ http {
server { server {
listen 443 ssl; listen 443 ssl;
server_name single_factor.example.com; server_name singlefactor.example.com;
resolver 127.0.0.11 ipv6=off; resolver 127.0.0.11 ipv6=off;
set $upstream_verify <%= authelia_backend %>/api/verify; set $upstream_verify <%= authelia_backend %>/api/verify;
@ -371,8 +386,7 @@ http {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
error_page 401 =302 https://login.example.com:8080?rd=$redirect; error_page 401 =302 https://login.example.com:8080/#/?rd=$redirect;
error_page 403 = https://login.example.com:8080/error/403;
proxy_pass $upstream_endpoint; proxy_pass $upstream_endpoint;
} }
@ -388,8 +402,7 @@ http {
auth_request_set $groups $upstream_http_remote_groups; auth_request_set $groups $upstream_http_remote_groups;
proxy_set_header Custom-Forwarded-Groups $groups; proxy_set_header Custom-Forwarded-Groups $groups;
error_page 401 =302 https://login.example.com:8080?rd=$redirect; error_page 401 =302 https://login.example.com:8080/#/?rd=$redirect;
error_page 403 = https://login.example.com:8080/error/403;
proxy_pass $upstream_headers; proxy_pass $upstream_headers;
} }

View File

@ -4,6 +4,5 @@ services:
image: schickling/mailcatcher image: schickling/mailcatcher
ports: ports:
- "1025:1025" - "1025:1025"
- "8085:1080"
networks: networks:
- authelianet - authelianet

View File

@ -4,66 +4,20 @@ Authelia is now available on Kube in order to protect your most critical
applications using 2-factor authentication and Single Sign-On. applications using 2-factor authentication and Single Sign-On.
This example leverages [ingress-nginx](https://github.com/kubernetes/ingress-nginx) This example leverages [ingress-nginx](https://github.com/kubernetes/ingress-nginx)
v0.13.0 to delegate authentications and authorizations to Authelia within to delegate authentication and authorization to Authelia within the cluster.
the cluster.
## Getting started ## Getting started
In order to deploy Authelia on Kube, you must have a cluster at hand. If you You can either try to install **Authelia** on your running instance of Kubernetes
don't, please follow the next section otherwise skip it and go or deploy the dedicated [suite](/docs/suites.md) called *kubernetes*.
to the next.
### Set up a Kube cluster ### Set up a Kube cluster
Hopefully, spawning a development cluster from scratch has become very The simplest way to start a Kubernetes cluster is to deploy the *kubernetes* suite with
easy lately with the use of **minikube**. This project creates a VM on your
computer and start a Kube cluster inside it. It also configure a CLI called
kubectl so that you can deploy applications in the cluster right away.
Basically, you need to follow the instruction from the [repository](https://github.com/kubernetes/minikube). authelia-scripts suites start kubernetes
It should be a matter of downloading the binary and start the cluster with
two commands:
``` This will take a few seconds (or minutes) to deploy the cluster.
curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/
minikube start # you can use --vm-driver flag for selecting your hypervisor (virtualbox by default otherwise)
```
After few seconds, your cluster should be working and you should be able to
get access to the cluster by creating a proxy with
```
kubectl proxy
```
and visiting `http://localhost:8001/ui`
### Deploy Authelia
Once the cluster is ready and you can access it, run the following command to
deploy Authelia:
```
./bootstrap.sh
```
In order to visit the test applications that have been deployed to test
Authelia, edit your /etc/hosts and add the following lines replacing the IP
with the IP of your VM given by minikube:
```
192.168.39.26 login.kube.example.com
192.168.39.26 app1.kube.example.com
192.168.39.26 app2.kube.example.com
192.168.39.26 mail.kube.example.com
192.168.39.26 home.kube.example.com
# The domain of the private docker registry holding dev version of Authelia
192.168.39.26 registry.kube.example.com
```
Once done, you can visit http://home.kube.example.com and follow the
instructions written in the page.
## How does it work? ## How does it work?
@ -81,32 +35,26 @@ The authentication is provided at the ingress level by an annotation called
`nginx.ingress.kubernetes.io/auth-url` that is filled with the URL of `nginx.ingress.kubernetes.io/auth-url` that is filled with the URL of
Authelia's verification endpoint. Authelia's verification endpoint.
The ingress controller also requires the URL to the The ingress controller also requires the URL to the
authentication portal so that the user can be redirected in case she is not authentication portal so that the user can be redirected if he is not
yet authenticated. yet authenticated. This annotation is as follows:
`nginx.ingress.kubernetes.io/auth-signin: "https://login.example.com:8080/#/"`
Those annotations can be seen in `apps/secure-ingress.yml` configuration. Those annotations can be seen in `apps/apps.yml` configuration.
### Production grade infrastructure ### Production grade infrastructure
What is great with using [ingress-nginx](https://github.com/kubernetes/ingress-nginx) What is great with using [ingress-nginx](https://github.com/kubernetes/ingress-nginx)
is that it is compatible with [kube-lego](https://github.com/jetstack/kube-lego) is that it is compatible with [kube-lego](https://github.com/jetstack/kube-lego)
which removes the usual pain of manual SSL certificate renewals. It uses which removes the usual pain of manually renewing SSL certificates. It uses
letsencrypt to issue and renew certificates every three month without any letsencrypt to issue and renew certificates every three month without any
manual intervention. manual intervention.
## What do I need know to deploy it in my cluster? ## What do I need to know to deploy it in my cluster?
Given your cluster is already made of an LDAP server, a Redis cluster, a Mongo Given your cluster already runs a LDAP server, a Redis, a Mongo database,
cluster and a SMTP server, you'll only need to install the ingress-controller a SMTP server and a nginx ingress-controller, you can deploy **Authelia**
and Authelia whose kubernetes deployment configurations are respectively in and update your ingress configurations. An example is provided
`ingress-controller` and `authelia` directories. A template configuration [here](./authelia).
is provided there, you just need to create the configmap to use it within
the cluster.
### I'm already using ingress-nginx
If you're already using ingress-nginx as your ingress controller, you only
need to install Authelia with its configuration and that's it!
## Questions ## Questions

View File

@ -1,33 +0,0 @@
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: test-app-home
namespace: authelia
labels:
app: test-app-home
spec:
replicas: 1
selector:
matchLabels:
app: test-app-home
template:
metadata:
labels:
app: test-app-home
spec:
containers:
- name: test-app-home
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: app-home-page
mountPath: /usr/share/nginx/html
volumes:
- name: app-home-page
configMap:
name: app-home-page
items:
- key: index.html
path: index.html

View File

@ -1,35 +0,0 @@
<html>
<head>
<title>Authelia Home</title>
</head>
<body>
<h1>Authelia on Kube</h1>
In this example, two applications have been deployed along with Authelia and a fake mailbox in order to confirm your secret registration to Authelia:
<ul>
<li><a href="https://app1.kube.example.com">https://app1.kube.example.com</a></li>
<li><a href="https://app2.kube.example.com">https://app2.kube.example.com</a></li>
<li><a href="http://mail.kube.example.com">http://mail.kube.example.com</a></li>
</ul>
<p>
Please note that <b>app1</b> is publicly available and <b>app2</b> is protected by Authelia.<br/>
<br/>
You can start by visiting <b>app1</b> and then try to access <b>app2</b>. Since <b>app2</b> is protected by Authelia, you will be redirected to Authelia's portal.<br/>
<br/>
If it's the first time you login in this cluster, you'll need to choose your authentication method and follow Authelia's instructions.<br/>
<br/>
Once done, you'll be able to authenticate with your selected second factor method.
</p>
<p>
Here is the list of available users in the LDAP
<ul>
<li><strong>john / password</strong>
<li><strong>bob / password</strong>
<li><strong>harry / password</strong>
</ul>
</p>
<p>
You can always log off by clicking <a href="https://login.kube.example.com/logout?rd=http://home.kube.example.com">here</a>
</p>
</body>
</html>

View File

@ -1,12 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: test-app-home-service
namespace: authelia
spec:
selector:
app: test-app-home
ports:
- protocol: TCP
port: 80

View File

@ -1,33 +0,0 @@
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: test-app1
namespace: authelia
labels:
app: test-app1
spec:
replicas: 1
selector:
matchLabels:
app: test-app1
template:
metadata:
labels:
app: test-app1
spec:
containers:
- name: test-app1
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: app1-page
mountPath: /usr/share/nginx/html
volumes:
- name: app1-page
configMap:
name: app1-page
items:
- key: index.html
path: index.html

View File

@ -1,9 +0,0 @@
<html>
<head>
<title>Application 1</title>
</head>
<body>
<h1>Application 1</h1>
<p><a href="http://home.kube.example.com">Go Home</a></p>
</body>
</html>

View File

@ -1,12 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: test-app1-service
namespace: authelia
spec:
selector:
app: test-app1
ports:
- protocol: TCP
port: 80

View File

@ -1,17 +0,0 @@
-----BEGIN CERTIFICATE-----
MIICvDCCAaQCCQD9zdaObelW0jANBgkqhkiG9w0BAQsFADAgMR4wHAYDVQQDDBVh
cHAxLmt1YmUuZXhhbXBsZS5jb20wHhcNMTgwMzA0MTUxMDUxWhcNMjgwMzAxMTUx
MDUxWjAgMR4wHAYDVQQDDBVhcHAxLmt1YmUuZXhhbXBsZS5jb20wggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+v2s9ABHFgxuYFyGkqlbNQ5OtzHAB79lM
sbAMJ9pnMTBA4FXdHgQuQ6Z4F233hMokVfnX9C2KxuLLOQMXJdoVQGytkbGGCzoz
Hz1qXBjDCwQtUGgLJ6zc3C8QKx90zmY0NmH55ttFCQKPHaaxgrS2YXsPzMlQKrgH
drRklfCpnRZWG9/M1YzXOKeT4VuwTsHyeI8tnco11WJLsZwRxc6TjEgNwwBnct8R
/cDhl5guDhqFPgMuBtzRlTVOeZS8NraHL5GRswoUeFJfS2VA3FTABwRWV3J6d5gl
oXpvMzCB01u2jVKLq75g91lGT6I+AVMgSOJG4Nezum7brS7jJjVFAgMBAAEwDQYJ
KoZIhvcNAQELBQADggEBAIFox2Ox/hJqmaSAyE0TqCaLf3UK78z9m/FYUzWoupGE
JAJ83wghVcj7XLKG4x6wN+v342JVa0mQ5X773kqNidHmSdWFPd/hGtx+0dQK3BVV
4CmQCNfA7BZDuxWPW0mcrkKun2aSCq0zjK0f/CzPXZxyPW18EmzSNGkmkXCesM5i
aevdE5PBKikGz4EfzieVTsImdHDfh2/azdRrmSh22x9/tpZy3mMkc5ERpgL2ll+m
HZuZ9FEBQHj92pk2aMBVdxPYpgzWAWbuRUs3vfTIhPlzHcI/ZpE60Ete+yY5lBw/
Gf+ph3HPB8gzSOH/hmcmerX9h6E3MFAncdC4hH3R0j8=
-----END CERTIFICATE-----

View File

@ -1,15 +0,0 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICZTCCAU0CAQAwIDEeMBwGA1UEAwwVYXBwMS5rdWJlLmV4YW1wbGUuY29tMIIB
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvr9rPQARxYMbmBchpKpWzUOT
rcxwAe/ZTLGwDCfaZzEwQOBV3R4ELkOmeBdt94TKJFX51/QtisbiyzkDFyXaFUBs
rZGxhgs6Mx89alwYwwsELVBoCyes3NwvECsfdM5mNDZh+ebbRQkCjx2msYK0tmF7
D8zJUCq4B3a0ZJXwqZ0WVhvfzNWM1zink+FbsE7B8niPLZ3KNdViS7GcEcXOk4xI
DcMAZ3LfEf3A4ZeYLg4ahT4DLgbc0ZU1TnmUvDa2hy+RkbMKFHhSX0tlQNxUwAcE
VldyeneYJaF6bzMwgdNbto1Si6u+YPdZRk+iPgFTIEjiRuDXs7pu260u4yY1RQID
AQABoAAwDQYJKoZIhvcNAQELBQADggEBALcwQAjNjyhMAbGNpFl6iYhzdhUjz02p
hTpc15T+S4PatbgeERYAIuSGxomPHfh+30udxGDCSy730V2urC0eGPjRpnJpGHNG
1Dau0sgi28TQPOqhBcZm0GNHMocc5iG+AxWAsxNwtSH3wRoeUGYXavcm9/tWvYbi
RIKmzXTQKsmY+qhBo3e/phUFuSU8GARYkfDSVqcTM7C3xswgXYcImrkbHAxvsC7+
3nr8ir2K9t2M3QxV7lTybNacuOF84wL93AZDvJ04HctXXgtt2rSI6vxxt18jyrxJ
yHX9ADrAa+jhPAM664SZs/+l0fBbil2UaMPOKXOCmYGuERpWh1HLHKo=
-----END CERTIFICATE REQUEST-----

View File

@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAvr9rPQARxYMbmBchpKpWzUOTrcxwAe/ZTLGwDCfaZzEwQOBV
3R4ELkOmeBdt94TKJFX51/QtisbiyzkDFyXaFUBsrZGxhgs6Mx89alwYwwsELVBo
Cyes3NwvECsfdM5mNDZh+ebbRQkCjx2msYK0tmF7D8zJUCq4B3a0ZJXwqZ0WVhvf
zNWM1zink+FbsE7B8niPLZ3KNdViS7GcEcXOk4xIDcMAZ3LfEf3A4ZeYLg4ahT4D
Lgbc0ZU1TnmUvDa2hy+RkbMKFHhSX0tlQNxUwAcEVldyeneYJaF6bzMwgdNbto1S
i6u+YPdZRk+iPgFTIEjiRuDXs7pu260u4yY1RQIDAQABAoIBAQCwn3ahCUtrZDdM
4T5ZxxCRCJ3aNI8SfBDuHyowV0a4fqd7qz5WfNDKNgIS+T7uDptOgf3SpVr2QasH
GkduS7JgM0NuhJWo1QSTCb5Imfajw7OecfGlQpuh9o/tnMCH3AZvGlwmlkk651jj
REVx4OGMbz8QJkPSY3v8DUKEUQKDSkRhZRhRGDZqbOSeERhOo2O3MqcBaVossYRw
+2Apth2XHg+7mgQjQF+8Z/DeXtLZgOB4VwQ0vkJmSu+FLhWDJqA+WH5JaOrmS3SN
lrPeIEwQMGHmwmIUAaQ7Akkow2xx4VuDRQUcUlitAM21x/MjBdTFbFx975svOiuE
vePNasYBAoGBAOK8e8x0OrqhgsVGWLvJjHnhzTePpemCCB9jRRnTcmMUxXR7moCh
WGgbyQ3+pU15WMxaIpVrGlV3LOwqSEXYspIpBQKv5+kSAaJf/Op3rtuzo57JLMYP
YokMu43ewtSF0CE/7h98vruAYCcDJZs+T7K0lHCBS9pcjhU2MS/ktW2lAoGBANdd
2a1dkEbZyD9NMGD+wWTkctrML3r1ZLy2gq1AX/oyKzdtD6T1nob435RVkyqPGLhr
tEHJuxLiaJlCk5Kd8V84ps0J9SlZTkLzf2ixb06f1YI9ye37UzBr13H59+N6w8Zu
24Gv12ht+WBecWKHgtF16LaCDPYORUMOa3CpgFchAoGBAN6M0Rr6jta3R0tpZBlW
mErd5vd9SQWtO1nLr3zM/f7g2XsfA6T0OXlepHbXFtu3mwBiDIYK7XssEezxB6V/
MK+kEaX0kTZFFVOS0gY2WWyOo7BsmEUDvtz0oXd8SlId0g+A17MSV4hlVnuUbCo3
/DRVaUoQrypzJIcPfTIcVDR9AoGAY3uNrqB2odO9xUfhnhxvtywzxc/l6tVp6CYi
bOc8rnT4M40kWd2/kbdqh7mT1mftUlsmE/GcgZemG41+X46nzYV8v1/nKGeBWDnk
U7cKpHX+iUADg/PBNK/MAHEoSaMOxh21Nc3FIg8Sz6owlAPmsNzXV17xn8NtyRDj
HlKd3yECgYBvB0bV1yXOvfoqGXAyadNJM9eKOuQwhrOJCXzCeMInhUtLpH35kC17
e8rOmoBhV1rLpxlMLV8giyJSRUPoN/TfUHfS2pWfKPobrp50vaUn1hYp6EEr85Qs
DdV2VilJsJN49sMg67GV3Zi4lnjVzYNFegw6y5Xi8l8ulYtzxJnemA==
-----END RSA PRIVATE KEY-----

View File

@ -1,33 +0,0 @@
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: test-app2
namespace: authelia
labels:
app: test-app2
spec:
replicas: 1
selector:
matchLabels:
app: test-app2
template:
metadata:
labels:
app: test-app2
spec:
containers:
- name: test-app2
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: app2-page
mountPath: /usr/share/nginx/html
volumes:
- name: app2-page
configMap:
name: app2-page
items:
- key: index.html
path: index.html

View File

@ -1,9 +0,0 @@
<html>
<head>
<title>Application 2</title>
</head>
<body>
<h1>Application 2</h2>
<p><a href="http://home.kube.example.com">Go Home</a></p>
</body>
</html>

View File

@ -1,12 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: test-app2-service
namespace: authelia
spec:
selector:
app: test-app2
ports:
- protocol: TCP
port: 80

View File

@ -1,17 +0,0 @@
-----BEGIN CERTIFICATE-----
MIICvDCCAaQCCQCE6h+pwV+OTTANBgkqhkiG9w0BAQsFADAgMR4wHAYDVQQDDBVh
cHAyLmt1YmUuZXhhbXBsZS5jb20wHhcNMTgwMzA0MTUwODUyWhcNMjgwMzAxMTUw
ODUyWjAgMR4wHAYDVQQDDBVhcHAyLmt1YmUuZXhhbXBsZS5jb20wggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDuVrXRL0i75y0QpbkFM7h1jIwbKOQYdkCR
tAp4aFjRAnHE1DtiXuexoyhs0hbwtxmFF0RpPjOVLvLl/AZt6i65dC5gl1czLmmH
nzjBzUF4jM5qJioP867vR2+SDnD6YWCQrwYG8bHeFKvppAlTY7g8YoTD/b3JNnV1
t+uaPyNKvu33pOP2vP/w/709WFVEwd/334RFF6fjAyYrOgYgvqcgyJ6E6T4cf/Vl
psgLZpJ7o4VoFHq1MdP6Ro+HzKwHUsDbH53U6wISe3dRBmHjTJH2sp1KuJ5gR1Ko
CiiVfq+CCPOxGKNngQe2EqDHmuVuXu30VFu82hznfkXdlhuzTGSfAgMBAAEwDQYJ
KoZIhvcNAQELBQADggEBAEWeTkGmuXnAPU8JGTa+O00kI9nFy10inbiU8O+XuwUL
Cj53CRffbzlsCDRDDxxoBuJ5EvWho5F7MR7A8ZRDfWqLTogvjpVp2YJ+jK/iTbqU
95tCVMByZufa4bHPWmngeYsSu0s0+qRYOeyiCbiFzlzFP3nLSS6aMPwrMUz7/Qp1
XUE1YfyqjKDkvFN4Wq1GKOUZEh4CJh3SuOE/FrRAiaAWnOH4LVDn5TFEbLyEqZLd
BrYEDopRsTpJck/ZEF43GZ0t8Y8CKffpWGV4PvSiUnl/7mKd+i+QsTx8CnnlR9y/
ZvwNRwvRw3uXwkgRinE8wwONAfwB2nIYKyuU9/ODAPs=
-----END CERTIFICATE-----

View File

@ -1,15 +0,0 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICZTCCAU0CAQAwIDEeMBwGA1UEAwwVYXBwMi5rdWJlLmV4YW1wbGUuY29tMIIB
IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7la10S9Iu+ctEKW5BTO4dYyM
GyjkGHZAkbQKeGhY0QJxxNQ7Yl7nsaMobNIW8LcZhRdEaT4zlS7y5fwGbeouuXQu
YJdXMy5ph584wc1BeIzOaiYqD/Ou70dvkg5w+mFgkK8GBvGx3hSr6aQJU2O4PGKE
w/29yTZ1dbfrmj8jSr7t96Tj9rz/8P+9PVhVRMHf99+ERRen4wMmKzoGIL6nIMie
hOk+HH/1ZabIC2aSe6OFaBR6tTHT+kaPh8ysB1LA2x+d1OsCEnt3UQZh40yR9rKd
SrieYEdSqAoolX6vggjzsRijZ4EHthKgx5rlbl7t9FRbvNoc535F3ZYbs0xknwID
AQABoAAwDQYJKoZIhvcNAQELBQADggEBANqbKTFSeOf9GRgrNuqRGYYdqSPaoXpu
iSKhJRABj4zMOCJlfDpeMQ8mGfmBUV+IHr+X8/nbMt+OMEf4u1+7Mmz4Zfvkt5gP
MBlYbauVxn/uIYp7aZgBUABC7SvLeITRz4rnQW5SvCNyuJAKQh84uF82g47S7Oaz
2dp6NO1nQ/N9SD6y0CyuIXf1KbSk4+lXa3+rGyqpF1aovpXCgvcA3tWrI/Lg2t5E
uPoiHegKGKyWUZeVh8eKY2ZBCl+uRmwLqTTdzj1HcoK5T1slg0X+K9Q1UsGy23Pw
RHFtGuel8msESgTnspzQF3T1uOscOOiQFG3xnoZtxH92gFT+pI7DoEY=
-----END CERTIFICATE REQUEST-----

View File

@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEA7la10S9Iu+ctEKW5BTO4dYyMGyjkGHZAkbQKeGhY0QJxxNQ7
Yl7nsaMobNIW8LcZhRdEaT4zlS7y5fwGbeouuXQuYJdXMy5ph584wc1BeIzOaiYq
D/Ou70dvkg5w+mFgkK8GBvGx3hSr6aQJU2O4PGKEw/29yTZ1dbfrmj8jSr7t96Tj
9rz/8P+9PVhVRMHf99+ERRen4wMmKzoGIL6nIMiehOk+HH/1ZabIC2aSe6OFaBR6
tTHT+kaPh8ysB1LA2x+d1OsCEnt3UQZh40yR9rKdSrieYEdSqAoolX6vggjzsRij
Z4EHthKgx5rlbl7t9FRbvNoc535F3ZYbs0xknwIDAQABAoIBAFcXgGDsMlvXYfRP
Woi4GZN6xEe4bYEy1O1pKNpO5wWZKxGNrBWKMIgM4tzA+HkFr2Ge2vTKMfc1rLS1
n3PSuzgxaDELnGWrdAyG9ip7Yo02hsbrIzupBCeTpwVsGYSkyLCWBFHNR/2q+Bbs
RiweqFgIeBNWSV+ZctqNVp6Kq87HuYfm/ka8ewVnPYhEMewuE0vqfVjc+fdPr/5I
4uQCCw0/ZTFFP++hkyNsH1ZCdUeT83xbgUwFA0M/3ckjMhzcP7lncq6Gvy8NpBhI
mle/Ev82yaDRl5VKoenpy2d8L+qvIjRbhZiZuTAU9AWRvOKrTMtzKSpyMgWAQZ64
69ResmECgYEA+2Ws4qBiYd3BKF5tc0fWbS0omThT60oLc6aLu5NoryogHHNwPOud
69nCDHSyYp+PqK1HLrOAVoTLmuwyys15juizewO809gYRgXlNZ9TKED8Znyg004o
EQVYxMevq2kfDOBJkMphLXAvFrRFQVB2Km8EfKkrI++1rhxKGJD+FzMCgYEA8rPU
G1v3/uemBxYO/bEmdjvsDfLI7FBYNmjgvBGJocSwb2sG/tRr0imZBPAuDlCKcZ1E
G3rZOJ0VsLzxX5RCVYFKRQvUtbaNO+uDJjChxQ/fQQdbIoaIMjyAVLI7t3Ei4+nY
7eHYjcHeMX54drO8YqQ6OoWZ4x8DI8KNcxBnzOUCgYA+uMRkmn1RS4For/6At5ih
DpZFfA878fJffVr5hrKkmU7/qjGDkYmKEX9fmjHzdznhbLIIzdIkQ+eElI+rl45P
gHFfLLSM6ipMNiZUtZaKwYP3kfqSHbrTXFEkb2m9y3Fqxf60uDl8m7Oz53Ar9oY0
2hP1gkN4KNNcSESYUnyCjwKBgAR9+ZYMDLoGFZeZ++sMJVcY4tSbQsbE8e0H4ej5
Nh/tYQqe44FB80Dvjip+O4v+R6G0tHcBvhWDKsyboqgPOW8Vtocyodw/JbwPLt09
FzFrislMVo58CPdNEV7/8YUCrg+j22UDwhtVlEQ8QASKbRkySvWcVW3TvB4kUrPn
gNRVAoGAI99QzqSRBSLeDgxxRgTM7l5wzyVEmJfmMqQsIbHvYy6zG9iYjFI6UZ8I
U3C/Sdnq7wCWa70b3L8A1iN2fYVwvkEH5WGbEp5B2Cye50avegbPi1FgGw2VSIuS
ysAkWaVJXsb2oQNtjidRuEflhy4nr7eybwa6cgarh7ci3JB+tQ8=
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,130 @@
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: test-app
namespace: authelia
labels:
k8s-app: test-app
spec:
replicas: 1
template:
metadata:
labels:
k8s-app: test-app
spec:
containers:
- name: test-app
imagePullPolicy: Never
image: authelia-example-backend
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: test-app-service
namespace: authelia
labels:
k8s-app: test-app
spec:
selector:
k8s-app: test-app
ports:
- port: 80
name: http
- port: 443
name: https
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: insecure-ingress
namespace: authelia
annotations:
kubernetes.io/ingress.class: "nginx"
kubernetes.io/ingress.allow-http: "false"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
tls:
- secretName: test-app-tls
hosts:
- home.example.com
rules:
- host: home.example.com
http:
paths:
- path: /
backend:
serviceName: test-app-service
servicePort: 80
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: secure-ingress
namespace: authelia
annotations:
kubernetes.io/ingress.class: "nginx"
kubernetes.io/ingress.allow-http: "false"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/auth-url: "http://authelia-service.authelia.svc.cluster.local/api/verify"
nginx.ingress.kubernetes.io/auth-signin: "https://login.example.com:8080/#/"
spec:
tls:
- secretName: test-app-tls
hosts:
- public.example.com
- admin.example.com
- dev.example.com
- mx1.mail.example.com
- mx2.mail.example.com
- singlefactor.example.com
rules:
- host: public.example.com
http:
paths:
- path: /
backend:
serviceName: test-app-service
servicePort: 80
- host: admin.example.com
http:
paths:
- path: /
backend:
serviceName: test-app-service
servicePort: 80
- host: dev.example.com
http:
paths:
- path: /
backend:
serviceName: test-app-service
servicePort: 80
- host: mx1.mail.example.com
http:
paths:
- path: /
backend:
serviceName: test-app-service
servicePort: 80
- host: mx2.mail.example.com
http:
paths:
- path: /
backend:
serviceName: test-app-service
servicePort: 80
- host: singlefactor.example.com
http:
paths:
- path: /
backend:
serviceName: test-app-service
servicePort: 80

View File

@ -1,28 +0,0 @@
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: insecure-ingress
namespace: authelia
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
tls:
- secretName: app1-tls
hosts:
- app1.kube.example.com
rules:
- host: app1.kube.example.com
http:
paths:
- path: /
backend:
serviceName: test-app1-service
servicePort: 80
- host: home.kube.example.com
http:
paths:
- path: /
backend:
serviceName: test-app-home-service
servicePort: 80

View File

@ -1,23 +0,0 @@
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: secure-ingress
namespace: authelia
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/auth-url: "http://authelia-service.authelia.svc.cluster.local/api/verify"
nginx.ingress.kubernetes.io/auth-signin: "https://login.kube.example.com"
spec:
tls:
- secretName: app2-tls
hosts:
- app2.kube.example.com
rules:
- host: app2.kube.example.com
http:
paths:
- path: /
backend:
serviceName: test-app2-service
servicePort: 80

View File

@ -20,7 +20,7 @@ logs_level: debug
# #
# Note: this parameter is optional. If not provided, user won't # Note: this parameter is optional. If not provided, user won't
# be redirected upon successful authentication. # be redirected upon successful authentication.
default_redirection_url: https://login.kube.example.com default_redirection_url: https://home.example.com:8080
# The authentication backend to use for verifying user passwords # The authentication backend to use for verifying user passwords
# and retrieve information such as email address and groups # and retrieve information such as email address and groups
@ -79,76 +79,60 @@ authentication_backend:
## file: ## file:
## path: ./users_database.yml ## path: ./users_database.yml
# Authentication methods
#
# Authentication methods can be defined per subdomain.
# There are currently two available methods: "single_factor" and "two_factor"
#
# Note: by default a domain uses "two_factor" method.
#
# Note: 'per_subdomain_methods' is a dictionary where keys must be subdomains and
# values must be one of the two possible methods.
#
# Note: 'per_subdomain_methods' is optional.
#
# Note: authentication_methods is optional. If it is not set all sub-domains
# are protected by two factors.
authentication_methods:
default_method: two_factor
# per_subdomain_methods:
# single_factor.example.com: single_factor
# Access Control # Access Control
# #
# Access control is a set of rules you can use to restrict user access to certain # For more details about the configuration see config.template.yml at the root of the repo.
# resources.
# Any (apply to anyone), per-user or per-group rules can be defined.
#
# If 'access_control' is not defined, ACL rules are disabled and the `allow` default
# policy is applied, i.e., access is allowed to anyone. Otherwise restrictions follow
# the rules defined.
#
# Note: One can use the wildcard * to match any subdomain.
# It must stand at the beginning of the pattern. (example: *.mydomain.com)
#
# Note: You must put the pattern in simple quotes when using the wildcard for the YAML
# to be syntaxically correct.
#
# Definition: A `rule` is an object with the following keys: `domain`, `policy`
# and `resources`.
# - `domain` defines which domain or set of domains the rule applies to.
# - `policy` is the policy to apply to resources. It must be either `allow` or `deny`.
# - `resources` is a list of regular expressions that matches a set of resources to
# apply the policy to.
#
# Note: Rules follow an order of priority defined as follows:
# In each category (`any`, `groups`, `users`), the latest rules have the highest
# priority. In other words, it means that if a given resource matches two rules in the
# same category, the latest one overrides the first one.
# Each category has also its own priority. That is, `users` has the highest priority, then
# `groups` and `any` has the lowest priority. It means if two rules in different categories
# match a given resource, the one in the category with the highest priority overrides the
# other one.
#
access_control: access_control:
# Default policy can either be `allow` or `deny`.
# It is the policy applied to any resource if it has not been overriden
# in the `any`, `groups` or `users` category.
default_policy: deny default_policy: deny
# The rules that apply to anyone. rules:
# The value is a list of rules. # Rules applied to everyone
any: - domain: public.example.com
policy: bypass
- domain: secure.example.com
policy: two_factor
- domain: singlefactor.example.com
policy: one_factor
# Rules applied to 'admin' group
- domain: 'mx2.mail.example.com'
subject: 'group:admin'
policy: deny
- domain: '*.example.com' - domain: '*.example.com'
policy: allow subject: 'group:admin'
policy: two_factor
# Group-based rules. The key is a group name and the value # Rules applied to 'dev' group
# is a list of rules. - domain: dev.example.com
groups: {} resources:
- '^/groups/dev/.*$'
subject: 'group:dev'
policy: two_factor
# User-based rules. The key is a user name and the value # Rules applied to user 'john'
# is a list of rules. - domain: dev.example.com
users: {} resources:
- '^/users/john/.*$'
subject: 'user:john'
policy: two_factor
# Rules applied to user 'harry'
- domain: dev.example.com
resources:
- '^/users/harry/.*$'
subject: 'user:harry'
policy: two_factor
# Rules applied to user 'bob'
- domain: '*.mail.example.com'
subject: 'user:bob'
policy: two_factor
- domain: 'dev.example.com'
resources:
- '^/users/bob/.*$'
subject: 'user:bob'
policy: two_factor
# Configuration of session cookies # Configuration of session cookies

View File

@ -18,8 +18,7 @@ spec:
spec: spec:
containers: containers:
- name: authelia - name: authelia
image: localhost:5000/authelia:latest image: authelia:dist
imagePullPolicy: Always
ports: ports:
- containerPort: 80 - containerPort: 80
volumeMounts: volumeMounts:

View File

@ -10,10 +10,10 @@ spec:
tls: tls:
- secretName: authelia-tls - secretName: authelia-tls
hosts: hosts:
- login.kube.example.com - login.example.com
rules: rules:
rules: rules:
- host: login.kube.example.com - host: login.example.com
http: http:
paths: paths:
- path: / - path: /

View File

@ -0,0 +1,8 @@
#!/bin/bash
start_authelia() {
kubectl create configmap authelia-config --namespace=authelia --from-file=authelia/configs/config.yml
kubectl apply -f authelia
}
start_authelia

30
example/kube/bootstrap.sh 100644 → 100755
View File

@ -1,30 +1,19 @@
#!/bin/bash #!/bin/bash
start_apps() { start_apps() {
# Create the test application pages
kubectl create configmap app1-page --namespace=authelia --from-file=apps/app1/index.html
kubectl create configmap app2-page --namespace=authelia --from-file=apps/app2/index.html
kubectl create configmap app-home-page --namespace=authelia --from-file=apps/app-home/index.html
# Create TLS certificate and key for HTTPS termination # Create TLS certificate and key for HTTPS termination
kubectl create secret generic app1-tls --namespace=authelia --from-file=apps/app1/ssl/tls.key --from-file=apps/app1/ssl/tls.crt kubectl create secret generic test-app-tls --namespace=authelia --from-file=apps/ssl/tls.key --from-file=apps/ssl/tls.crt
kubectl create secret generic app2-tls --namespace=authelia --from-file=apps/app2/ssl/tls.key --from-file=apps/app2/ssl/tls.crt
kubectl create secret generic authelia-tls --namespace=authelia --from-file=authelia/ssl/tls.key --from-file=authelia/ssl/tls.crt
# Spawn the applications # Spawn the applications
kubectl apply -f apps kubectl apply -f apps
kubectl apply -f apps/app1
kubectl apply -f apps/app2
kubectl apply -f apps/app-home
} }
start_ingress_controller() { start_ingress_controller() {
kubectl apply -f ingress-controller kubectl apply -f ingress-controller
} }
start_authelia() { start_dashboard() {
kubectl create configmap authelia-config --namespace=authelia --from-file=authelia/configs/config.yml kubectl apply -f dashboard
kubectl apply -f authelia
} }
# Spawn Redis and Mongo as backend for Authelia # Spawn Redis and Mongo as backend for Authelia
@ -34,28 +23,23 @@ start_storage() {
} }
# Create a fake mailbox to catch emails sent by Authelia # Create a fake mailbox to catch emails sent by Authelia
start_mailcatcher() { start_mail() {
kubectl apply -f mailcatcher kubectl apply -f mail
} }
start_ldap() { start_ldap() {
kubectl apply -f ldap kubectl apply -f ldap
} }
start_docker_registry() {
kubectl apply -f docker-registry
}
# Create the Authelia namespace in the cluster # Create the Authelia namespace in the cluster
create_namespace() { create_namespace() {
kubectl apply -f namespace.yml kubectl apply -f namespace.yml
} }
create_namespace create_namespace
start_docker_registry start_dashboard
start_storage start_storage
start_ldap start_ldap
start_mailcatcher start_mail
start_ingress_controller start_ingress_controller
start_authelia
start_apps start_apps

View File

@ -1,9 +0,0 @@
#!/bin/bash
build_and_push_authelia() {
cd ../../
docker build -t registry.kube.example.com:80/authelia .
docker push registry.kube.example.com:80/authelia
}
build_and_push_authelia

View File

@ -0,0 +1,179 @@
# Copyright 2017 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ------------------- Dashboard Secret ------------------- #
apiVersion: v1
kind: Secret
metadata:
labels:
k8s-app: kubernetes-dashboard
name: kubernetes-dashboard-certs
namespace: kube-system
type: Opaque
---
# ------------------- Dashboard Service Account ------------------- #
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
k8s-app: kubernetes-dashboard
name: kubernetes-dashboard
namespace: kube-system
---
# ------------------- Dashboard Role & Role Binding ------------------- #
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: kubernetes-dashboard-minimal
namespace: kube-system
rules:
# Allow Dashboard to create 'kubernetes-dashboard-key-holder' secret.
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create"]
# Allow Dashboard to create 'kubernetes-dashboard-settings' config map.
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["create"]
# Allow Dashboard to get, update and delete Dashboard exclusive secrets.
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["kubernetes-dashboard-key-holder", "kubernetes-dashboard-certs"]
verbs: ["get", "update", "delete"]
# Allow Dashboard to get and update 'kubernetes-dashboard-settings' config map.
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["kubernetes-dashboard-settings"]
verbs: ["get", "update"]
# Allow Dashboard to get metrics from heapster.
- apiGroups: [""]
resources: ["services"]
resourceNames: ["heapster"]
verbs: ["proxy"]
- apiGroups: [""]
resources: ["services/proxy"]
resourceNames: ["heapster", "http:heapster:", "https:heapster:"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: kubernetes-dashboard-minimal
namespace: kube-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: kubernetes-dashboard-minimal
subjects:
- kind: ServiceAccount
name: kubernetes-dashboard
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: kubernetes-dashboard
labels:
k8s-app: kubernetes-dashboard
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: kubernetes-dashboard
namespace: kube-system
---
# ------------------- Dashboard Deployment ------------------- #
kind: Deployment
apiVersion: apps/v1
metadata:
labels:
k8s-app: kubernetes-dashboard
name: kubernetes-dashboard
namespace: kube-system
spec:
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
k8s-app: kubernetes-dashboard
template:
metadata:
labels:
k8s-app: kubernetes-dashboard
spec:
containers:
- name: kubernetes-dashboard
image: k8s.gcr.io/kubernetes-dashboard-amd64:v1.10.1
ports:
- containerPort: 8443
protocol: TCP
args:
- --auto-generate-certificates
- --enable-skip-login
# Uncomment the following line to manually specify Kubernetes API server Host
# If not specified, Dashboard will attempt to auto discover the API server and connect
# to it. Uncomment only if the default does not work.
# - --apiserver-host=http://my-address:port
volumeMounts:
- name: kubernetes-dashboard-certs
mountPath: /certs
# Create on-disk volume to store exec logs
- mountPath: /tmp
name: tmp-volume
livenessProbe:
httpGet:
scheme: HTTPS
path: /
port: 8443
initialDelaySeconds: 30
timeoutSeconds: 30
volumes:
- name: kubernetes-dashboard-certs
secret:
secretName: kubernetes-dashboard-certs
- name: tmp-volume
emptyDir: {}
serviceAccountName: kubernetes-dashboard
# Comment the following tolerations if Dashboard must not be deployed on master
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
---
# ------------------- Dashboard Service ------------------- #
kind: Service
apiVersion: v1
metadata:
labels:
k8s-app: kubernetes-dashboard
name: kubernetes-dashboard
namespace: kube-system
spec:
ports:
- port: 443
targetPort: 8443
selector:
k8s-app: kubernetes-dashboard

View File

@ -1,35 +0,0 @@
---
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
name: kube-registry-proxy
namespace: kube-system
labels:
k8s-app: kube-registry-proxy
kubernetes.io/cluster-service: "true"
version: v0.4
spec:
template:
metadata:
labels:
k8s-app: kube-registry-proxy
kubernetes.io/name: "kube-registry-proxy"
kubernetes.io/cluster-service: "true"
version: v0.4
spec:
containers:
- name: kube-registry-proxy
image: gcr.io/google_containers/kube-registry-proxy:0.4
resources:
limits:
cpu: 100m
memory: 50Mi
env:
- name: REGISTRY_HOST
value: kube-registry.kube-system.svc.cluster.local
- name: REGISTRY_PORT
value: "5000"
ports:
- name: registry
containerPort: 80
hostPort: 5000

View File

@ -1,18 +0,0 @@
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: registry-ingress
namespace: kube-system
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/proxy-body-size: 100m # Avoid 413 Request entity too large
spec:
rules:
- host: registry.kube.example.com
http:
paths:
- path: /
backend:
serviceName: kube-registry
servicePort: 5000

View File

@ -1,44 +0,0 @@
---
apiVersion: v1
kind: ReplicationController
metadata:
name: kube-registry-v0
namespace: kube-system
labels:
k8s-app: kube-registry-upstream
version: v0
kubernetes.io/cluster-service: "true"
spec:
replicas: 1
selector:
k8s-app: kube-registry-upstream
version: v0
template:
metadata:
labels:
k8s-app: kube-registry-upstream
version: v0
kubernetes.io/cluster-service: "true"
spec:
containers:
- name: registry
image: registry:2
resources:
limits:
cpu: 100m
memory: 100Mi
env:
- name: REGISTRY_HTTP_ADDR
value: :5000
- name: REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY
value: /var/lib/registry
volumeMounts:
- name: image-store
mountPath: /var/lib/registry
ports:
- containerPort: 5000
name: registry
protocol: TCP
volumes:
- name: image-store
emptyDir: {}

View File

@ -1,17 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: kube-registry
namespace: kube-system
labels:
k8s-app: kube-registry-upstream
kubernetes.io/cluster-service: "true"
kubernetes.io/name: "KubeRegistry"
spec:
selector:
k8s-app: kube-registry-upstream
ports:
- name: registry
port: 5000
protocol: TCP

View File

@ -1,48 +0,0 @@
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: default-http-backend
labels:
app: default-http-backend
namespace: authelia
spec:
replicas: 1
template:
metadata:
labels:
app: default-http-backend
spec:
terminationGracePeriodSeconds: 60
containers:
- name: default-http-backend
image: gcr.io/google_containers/defaultbackend:1.4
livenessProbe:
httpGet:
path: /healthz
port: 8080
scheme: HTTP
initialDelaySeconds: 30
timeoutSeconds: 5
ports:
- containerPort: 8080
resources:
limits:
cpu: 10m
memory: 20Mi
requests:
cpu: 10m
memory: 20Mi
---
apiVersion: v1
kind: Service
metadata:
name: default-http-backend
namespace: authelia
labels:
app: default-http-backend
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: default-http-backend

View File

@ -2,26 +2,27 @@
apiVersion: extensions/v1beta1 apiVersion: extensions/v1beta1
kind: Deployment kind: Deployment
metadata: metadata:
name: nginx-ingress-controller-external name: nginx-ingress-controller
namespace: authelia namespace: authelia
labels: labels:
k8s-app: nginx-ingress-controller-external k8s-app: nginx-ingress-controller
spec: spec:
replicas: 1 replicas: 1
revisionHistoryLimit: 0 revisionHistoryLimit: 0
template: template:
metadata: metadata:
labels: labels:
k8s-app: nginx-ingress-controller-external k8s-app: nginx-ingress-controller
name: nginx-ingress-controller-external name: nginx-ingress-controller
annotations: annotations:
prometheus.io/port: '10254' prometheus.io/port: '10254'
prometheus.io/scrape: 'true' prometheus.io/scrape: 'true'
spec: spec:
terminationGracePeriodSeconds: 60 terminationGracePeriodSeconds: 60
serviceAccountName: nginx-ingress-controller-serviceaccount
containers: containers:
- image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.13.0 - image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.23.0
name: nginx-ingress-controller-external name: nginx-ingress-controller
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- containerPort: 80 - containerPort: 80
@ -38,5 +39,4 @@ spec:
args: args:
- /nginx-ingress-controller - /nginx-ingress-controller
- --ingress-class=nginx - --ingress-class=nginx
- --election-id=ingress-controller-leader-external - --election-id=ingress-controller-leader
- --default-backend-service=$(POD_NAMESPACE)/default-http-backend

View File

@ -0,0 +1,141 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: nginx-ingress-controller-serviceaccount
namespace: authelia
labels:
k8s-app: nginx-ingress-controller
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: nginx-ingress-controller-clusterrole
labels:
k8s-app: nginx-ingress-controller
rules:
- apiGroups:
- ""
resources:
- configmaps
- endpoints
- nodes
- pods
- secrets
verbs:
- list
- watch
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- apiGroups:
- ""
resources:
- services
verbs:
- get
- list
- watch
- apiGroups:
- "extensions"
resources:
- ingresses
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- "extensions"
resources:
- ingresses/status
verbs:
- update
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
name: nginx-ingress-controller-role
namespace: authelia
labels:
k8s-app: nginx-ingress-controller
rules:
- apiGroups:
- ""
resources:
- configmaps
- pods
- secrets
- namespaces
verbs:
- get
- apiGroups:
- ""
resources:
- configmaps
resourceNames:
# Defaults to "<election-id>-<ingress-class>"
# Here: "<ingress-controller-leader>-<nginx>"
# This has to be adapted if you change either parameter
# when launching the nginx-ingress-controller.
- "ingress-controller-leader-nginx"
verbs:
- get
- update
- apiGroups:
- ""
resources:
- configmaps
verbs:
- create
- apiGroups:
- ""
resources:
- endpoints
verbs:
- get
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: nginx-ingress-controller-role-nisa-binding
namespace: authelia
labels:
k8s-app: nginx-ingress-controller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: nginx-ingress-controller-role
subjects:
- kind: ServiceAccount
name: nginx-ingress-controller-serviceaccount
namespace: authelia
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: nginx-ingress-controller-clusterrole-nisa-binding
labels:
k8s-app: nginx-ingress-controller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: nginx-ingress-controller-clusterrole
subjects:
- kind: ServiceAccount
name: nginx-ingress-controller-serviceaccount
namespace: authelia
---

View File

@ -2,17 +2,16 @@
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: nginx-ingress-controller-external-service name: nginx-ingress-controller-service
namespace: authelia namespace: authelia
labels: labels:
k8s-app: nginx-ingress-controller-external k8s-app: nginx-ingress-controller
spec: spec:
selector: selector:
k8s-app: nginx-ingress-controller-external k8s-app: nginx-ingress-controller
type: NodePort
ports: ports:
- port: 80 - port: 80
name: http name: http
- port: 443 - port: 443
name: https name: https
externalIPs:
- 192.168.39.26

View File

@ -7,8 +7,12 @@ metadata:
annotations: annotations:
kubernetes.io/ingress.class: "nginx" kubernetes.io/ingress.class: "nginx"
spec: spec:
tls:
- secretName: mail-tls
hosts:
- mail.example.com
rules: rules:
- host: mail.kube.example.com - host: mail.example.com
http: http:
paths: paths:
- path: / - path: /

View File

@ -0,0 +1,22 @@
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: test-app1
namespace: kube-public
labels:
app: test-app1
spec:
replicas: 1
selector:
matchLabels:
app: test-app1
template:
metadata:
labels:
app: test-app1
spec:
containers:
- name: test-app1
image: clems4ever/authelia:kube
imagePullPolicy: Never

47
package-lock.json generated
View File

@ -260,14 +260,14 @@
} }
}, },
"@types/connect-redis": { "@types/connect-redis": {
"version": "0.0.7", "version": "0.0.9",
"resolved": "https://registry.npmjs.org/@types/connect-redis/-/connect-redis-0.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/connect-redis/-/connect-redis-0.0.9.tgz",
"integrity": "sha512-ui1DPnJxqgBhqPj0XTVyPkzffEX9DIGkb2nT2mzZ0OlsKn/u9BvRvKmjpi4Vydf2uw3z/D4UmMH4KMkilySqvw==", "integrity": "sha512-LdAbC9EnQkHLMgeV0EKufPbqaRKfsAxgdWaYnDtQOKuJV2U4JpFE2EU4yKJ3G2FjVvNXyzO6rcaBBjPBztGTzg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/express": "4.11.1", "@types/express": "4.11.1",
"@types/express-session": "1.15.8", "@types/express-session": "1.15.8",
"@types/redis": "2.8.6" "@types/redis": "2.8.11"
} }
}, },
"@types/ejs": { "@types/ejs": {
@ -495,12 +495,11 @@
"dev": true "dev": true
}, },
"@types/redis": { "@types/redis": {
"version": "2.8.6", "version": "2.8.11",
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.6.tgz", "resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.11.tgz",
"integrity": "sha512-kaSI4XQwCfJtPiuyCXvLxCaw2N0fMZesdob3Jh01W20vNFct+3lfvJ/4yCJxbSopXOBOzpg+pGxkW6uWZrPZHA==", "integrity": "sha512-i0AqDzjX0FlO+npqkhl1etZw1fGnJc0IeHUHKAf/V7Drk+rDJI634GE9Lwh8aj/2ahuW6jHdWX4ZnyNofWXKrw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/events": "1.2.0",
"@types/node": "10.0.3" "@types/node": "10.0.3"
} }
}, },
@ -569,7 +568,8 @@
"@types/url-parse": { "@types/url-parse": {
"version": "1.4.2", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/@types/url-parse/-/url-parse-1.4.2.tgz", "resolved": "https://registry.npmjs.org/@types/url-parse/-/url-parse-1.4.2.tgz",
"integrity": "sha512-Z25Ef2iI0lkiBAoe8qATRHQWy+BWFWuWasD6HB5prsWx2QjLmB/ngXv8v9dePw2jDwdSvET4/6J5HxmeqhslmQ==" "integrity": "sha512-Z25Ef2iI0lkiBAoe8qATRHQWy+BWFWuWasD6HB5prsWx2QjLmB/ngXv8v9dePw2jDwdSvET4/6J5HxmeqhslmQ==",
"dev": true
}, },
"@types/winston": { "@types/winston": {
"version": "2.3.9", "version": "2.3.9",
@ -1745,21 +1745,26 @@
} }
}, },
"connect-redis": { "connect-redis": {
"version": "3.3.3", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-3.3.3.tgz", "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-3.4.1.tgz",
"integrity": "sha512-rpWsW2uk1uOe/ccY/JvW+RiLrhZm7auIx8z4yR+KXemFTIhJyD58jXiJbI0E/fZCnybawpdSqOZ+6/ah6aBeyg==", "integrity": "sha512-oXNcpLg/PJ6G4gbhyGwrQK9mUQTKYa2aEnOH9kWIxbNUjIFPqUmzz75RdLp5JTPSjrBVcz+9ll4sSxfvlW0ZLA==",
"requires": { "requires": {
"debug": "3.1.0", "debug": "4.1.1",
"redis": "2.8.0" "redis": "2.8.0"
}, },
"dependencies": { "dependencies": {
"debug": { "debug": {
"version": "3.1.0", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"requires": { "requires": {
"ms": "2.0.0" "ms": "2.1.1"
} }
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
} }
} }
}, },
@ -7489,14 +7494,14 @@
"integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==", "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==",
"requires": { "requires": {
"double-ended-queue": "2.1.0-0", "double-ended-queue": "2.1.0-0",
"redis-commands": "1.3.5", "redis-commands": "1.4.0",
"redis-parser": "2.6.0" "redis-parser": "2.6.0"
} }
}, },
"redis-commands": { "redis-commands": {
"version": "1.3.5", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.5.tgz", "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.4.0.tgz",
"integrity": "sha512-foGF8u6MXGFF++1TZVC6icGXuMYPftKXt1FBT2vrfU9ZATNtZJ8duRC5d1lEfE8hyVe3jhelHGB91oB7I6qLsA==" "integrity": "sha512-cu8EF+MtkwI4DLIT0x9P8qNTLFhQD4jLfxLR0cCNkeGzs87FN6879JOJwNQR/1zD7aSYNbU0hgsV9zGY71Itvw=="
}, },
"redis-parser": { "redis-parser": {
"version": "2.6.0", "version": "2.6.0",

View File

@ -31,11 +31,10 @@
"title": "Authelia API documentation" "title": "Authelia API documentation"
}, },
"dependencies": { "dependencies": {
"@types/url-parse": "^1.4.2",
"ajv": "^6.3.0", "ajv": "^6.3.0",
"bluebird": "^3.5.0", "bluebird": "^3.5.0",
"body-parser": "^1.15.2", "body-parser": "^1.15.2",
"connect-redis": "^3.3.0", "connect-redis": "^3.4.1",
"crypt3": "^1.0.0", "crypt3": "^1.0.0",
"ejs": "^2.5.5", "ejs": "^2.5.5",
"express": "^4.14.0", "express": "^4.14.0",
@ -51,7 +50,6 @@
"object-path": "^0.11.3", "object-path": "^0.11.3",
"randomatic": "^3.1.0", "randomatic": "^3.1.0",
"randomstring": "^1.1.5", "randomstring": "^1.1.5",
"redis": "^2.8.0",
"speakeasy": "^2.0.0", "speakeasy": "^2.0.0",
"u2f": "^0.1.2", "u2f": "^0.1.2",
"u2f-api": "^1.0.7", "u2f-api": "^1.0.7",
@ -64,7 +62,7 @@
"@types/body-parser": "^1.16.3", "@types/body-parser": "^1.16.3",
"@types/chokidar": "^1.7.5", "@types/chokidar": "^1.7.5",
"@types/commander": "^2.12.2", "@types/commander": "^2.12.2",
"@types/connect-redis": "0.0.7", "@types/connect-redis": "0.0.9",
"@types/ejs": "^2.3.33", "@types/ejs": "^2.3.33",
"@types/express": "^4.0.35", "@types/express": "^4.0.35",
"@types/express-session": "1.15.8", "@types/express-session": "1.15.8",
@ -81,13 +79,13 @@
"@types/object-path": "^0.9.28", "@types/object-path": "^0.9.28",
"@types/query-string": "^5.1.0", "@types/query-string": "^5.1.0",
"@types/randomstring": "^1.1.5", "@types/randomstring": "^1.1.5",
"@types/redis": "^2.6.0",
"@types/request": "^2.0.5", "@types/request": "^2.0.5",
"@types/request-promise": "^4.1.38", "@types/request-promise": "^4.1.38",
"@types/selenium-webdriver": "^3.0.4", "@types/selenium-webdriver": "^3.0.4",
"@types/sinon": "^4.3.0", "@types/sinon": "^4.3.0",
"@types/speakeasy": "^2.0.2", "@types/speakeasy": "^2.0.2",
"@types/tmp": "0.0.33", "@types/tmp": "0.0.33",
"@types/url-parse": "^1.4.2",
"@types/winston": "^2.3.2", "@types/winston": "^2.3.2",
"@types/yamljs": "^0.2.30", "@types/yamljs": "^0.2.30",
"apidoc": "^0.17.6", "apidoc": "^0.17.6",

View File

@ -5,6 +5,7 @@ var program = require('commander');
program program
.version('0.0.1') .version('0.0.1')
.command('bootstrap', 'Prepare some containers for development.')
.command('suites', 'Run Authelia in specific environments.') .command('suites', 'Run Authelia in specific environments.')
.command('serve', 'Run Authelia with a customized configuration.') .command('serve', 'Run Authelia with a customized configuration.')
.command('build', 'Build production version of Authelia from source.') .command('build', 'Build production version of Authelia from source.')

View File

@ -0,0 +1,24 @@
#!/usr/bin/env node
var { exec } = require('./utils/exec');
var fs = require('fs');
async function main() {
console.log('Build authelia-example-backend docker image.')
await exec('docker build -t authelia-example-backend example/compose/nginx/backend');
if (!fs.existsSync('/tmp/kind')) {
console.log('Install Kind for spawning a Kubernetes cluster.');
await exec('wget https://github.com/clems4ever/kind/releases/download/0.1.0-cmic1/kind-linux-amd64 -O /tmp/kind && chmod +x /tmp/kind');
}
if (!fs.existsSync('/tmp/kubectl')) {
console.log('Install Kubectl for interacting with Kubernetes.');
await exec('wget https://storage.googleapis.com/kubernetes-release/release/v1.13.0/bin/linux/amd64/kubectl -O /tmp/kubectl && chmod +x /tmp/kubectl');
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
})

View File

@ -1,6 +1,14 @@
#!/usr/bin/env node #!/usr/bin/env node
var { exec } = require('./utils/exec'); var { exec } = require('./utils/exec');
var { IMAGE_NAME } = require('./utils/docker'); var { IMAGE_NAME, DOCKERHUB_IMAGE_NAME } = require('./utils/docker');
exec(`docker build -t ${IMAGE_NAME} .`); async function main() {
await exec(`docker build -t ${IMAGE_NAME}:dist .`);
await exec(`docker tag ${IMAGE_NAME}:dist ${DOCKERHUB_IMAGE_NAME}`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
})

View File

@ -15,8 +15,9 @@ function login_to_dockerhub {
function deploy_on_dockerhub { function deploy_on_dockerhub {
TAG=$1 TAG=$1
IMAGE_NAME=clems4ever/authelia IMAGE_NAME=authelia:dist
IMAGE_WITH_TAG=$IMAGE_NAME:$TAG DOCKERHUB_IMAGE_NAME=clems4ever/authelia
IMAGE_WITH_TAG=$DOCKERHUB_IMAGE_NAME:$TAG
echo "===========================================================" echo "==========================================================="
echo "Docker image $IMAGE_WITH_TAG will be deployed on Dockerhub." echo "Docker image $IMAGE_WITH_TAG will be deployed on Dockerhub."
@ -27,12 +28,10 @@ function deploy_on_dockerhub {
} }
if [ "$TRAVIS_BRANCH" == "master" ]; then if [ "$TRAVIS_BRANCH" == "master" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then
echo "Building on master branch"
login_to_dockerhub login_to_dockerhub
deploy_on_dockerhub master deploy_on_dockerhub master
elif [ "$TRAVIS_BRANCH" == "develop" ]; then
login_to_dockerhub
deploy_on_dockerhub develop
elif [ ! -z "$TRAVIS_TAG" ]; then elif [ ! -z "$TRAVIS_TAG" ]; then
login_to_dockerhub login_to_dockerhub
deploy_on_dockerhub $TRAVIS_TAG deploy_on_dockerhub $TRAVIS_TAG

View File

@ -2,7 +2,6 @@
var program = require('commander'); var program = require('commander');
var spawn = require('child_process').spawn; var spawn = require('child_process').spawn;
var fs = require('fs');
let config; let config;

View File

@ -53,7 +53,8 @@ async function main() {
}); });
} }
main().catch((err) => { main()
.catch((err) => {
console.error(err); console.error(err);
process.exit(1) process.exit(1)
}); });

View File

@ -3,6 +3,7 @@
var program = require('commander'); var program = require('commander');
var spawn = require('child_process').spawn; var spawn = require('child_process').spawn;
var fs = require('fs'); var fs = require('fs');
var ListSuites = require('./utils/ListSuites');
let suite; let suite;
@ -10,52 +11,108 @@ program
.description('Run the tests for the current suite or the provided one.') .description('Run the tests for the current suite or the provided one.')
.arguments('[suite]') .arguments('[suite]')
.option('--headless', 'Run in headless mode.') .option('--headless', 'Run in headless mode.')
.option('--forbid-only', 'Forbid only and pending filters.')
.action((suiteArg) => suite = suiteArg) .action((suiteArg) => suite = suiteArg)
.parse(process.argv); .parse(process.argv);
let args = []; async function runTests(suite, withEnv) {
return new Promise((resolve, reject) => {
let mochaArgs = ['--exit', '--colors', '--require', 'ts-node/register', 'test/suites/' + suite + '/test.ts']
if (program.forbidOnly) {
mochaArgs = ['--forbid-only', '--forbid-pending'] + mochaArgs;
}
const mochaCommand = './node_modules/.bin/mocha ' + mochaArgs.join(' ');
let testProcess;
if (!withEnv) {
testProcess = spawn(mochaCommand, {
shell: true,
env: {
...process.env,
TS_NODE_PROJECT: 'test/tsconfig.json',
HEADLESS: (program.headless) ? 'y' : 'n'
}
});
} else {
testProcess = spawn('./node_modules/.bin/ts-node',
['-P', 'test/tsconfig.json', '--', './scripts/run-environment.ts', suite, mochaCommand], {
env: {
...process.env,
TS_NODE_PROJECT: 'test/tsconfig.json',
HEADLESS: (program.headless) ? 'y' : 'n'
}
});
}
testProcess.stdout.pipe(process.stdout);
testProcess.stderr.pipe(process.stderr);
testProcess.on('exit', function(statusCode) {
if (statusCode == 0) resolve();
reject(new Error('Tests exited with status ' + statusCode));
});
});
}
async function runAllTests() {
const suites = ListSuites();
let failure = false;
for(var s in suites) {
try {
console.log('Running suite %s', suites[s]);
await runTests(suites[s], true);
} catch (e) {
console.error(e);
failure = true;
}
}
if (failure) {
throw new Error('Some tests failed.');
}
}
const ENVIRONMENT_FILENAME = '.suite'; // This file is created by the start command. const ENVIRONMENT_FILENAME = '.suite'; // This file is created by the start command.
const suiteFileExists = fs.existsSync(ENVIRONMENT_FILENAME);
if (suite) { if (suite) {
if (fs.existsSync(ENVIRONMENT_FILENAME) && suite != fs.readFileSync(ENVIRONMENT_FILENAME)) { if (suiteFileExists && suite != fs.readFileSync(ENVIRONMENT_FILENAME)) {
console.error('You cannot test a suite while another one is running. If you want to test the current suite, ' + console.error('You cannot test a suite while another one is running. If you want to test the current suite, ' +
'do not provide the suite argument. Otherwise, stop the current suite and run the command again.'); 'do not provide the suite argument. Otherwise, stop the current suite and run the command again.');
process.exit(1); process.exit(1);
} }
console.log('Suite %s provided. Running test related to this suite against built version of Authelia.', suite); console.log('Suite %s provided. Running test related to this suite against built version of Authelia.', suite);
args.push('test/suites/' + suite + '/test.ts'); runTests(suite, !suiteFileExists)
.catch((err) => {
console.error(err);
process.exit(1);
});
} }
else if (fs.existsSync(ENVIRONMENT_FILENAME)) { else if (suiteFileExists) {
suite = fs.readFileSync(ENVIRONMENT_FILENAME); suite = fs.readFileSync(ENVIRONMENT_FILENAME);
console.log('Suite %s detected from dev env. Running test related to this suite.', suite); console.log('Suite %s detected from dev env. Running test related to this suite.', suite);
args.push('test/suites/' + suite + '/test.ts'); runTests(suite, false)
.catch((err) => {
console.error(err);
process.exit(1);
});
} }
else { else {
console.log('No suite provided therefore all suites will be tested.'); console.log('No suite provided therefore all suites will be tested.');
args.push('test/suites/**/test.ts'); runAllTests(suite)
.catch((err) => {
console.error(err);
process.exit(1);
});
} }
mocha = spawn('./node_modules/.bin/mocha', ['--forbid-only', '--forbid-pending', '--exit', '--colors', '--require', 'ts-node/register', ...args], { // Sometime SIGINT is received twice, we make sure with this variable that we cleanup
env: { // only once.
...process.env, var alreadyCleaningUp = false;
TS_NODE_PROJECT: 'test/tsconfig.json',
HEADLESS: (program.headless) ? 'y' : 'n', // Properly cleanup server and client if ctrl-c is hit
} process.on('SIGINT', function() {
if (alreadyCleaningUp) return;
alreadyCleaningUp = true;
console.log('Cleanup procedure is running...');
}); });
mocha.stdout.pipe(process.stdout);
mocha.stderr.pipe(process.stderr);
mocha.on('exit', function(statusCode) {
if (statusCode != 0) {
console.error("The tests failed... Mocha exited with status code " + statusCode);
fs.readFile('/tmp/authelia-server.log', function(err, data) {
if (err) {
console.error(err);
return;
}
console.log(data.toString('utf-8'));
});
}
process.exit(statusCode);
})

View File

@ -2,17 +2,18 @@
set -e set -e
export PATH=./scripts:$PATH source bootstrap.sh
docker --version docker --version
docker-compose --version docker-compose --version
echo "node `node -v`" echo "node `node -v`"
echo "npm `npm -v`" echo "npm `npm -v`"
authelia-scripts bootstrap
docker-compose -f docker-compose.yml \ docker-compose -f docker-compose.yml \
-f example/compose/mongo/docker-compose.yml \ -f example/compose/mongo/docker-compose.yml \
-f example/compose/redis/docker-compose.yml \ -f example/compose/redis/docker-compose.yml \
-f example/compose/nginx/backend/docker-compose.yml \
-f example/compose/nginx/portal/docker-compose.yml \ -f example/compose/nginx/portal/docker-compose.yml \
-f example/compose/smtp/docker-compose.yml \ -f example/compose/smtp/docker-compose.yml \
-f example/compose/httpbin/docker-compose.yml \ -f example/compose/httpbin/docker-compose.yml \

View File

@ -1,7 +1,9 @@
import { exec } from './utils/exec';
const userSuite = process.argv[2]; const userSuite = process.argv[2];
const command = process.argv[3]; // The command to run once the env is up.
var { setup, teardown } = require(`../test/suites/${userSuite}/environment`); var { setup, setup_timeout, teardown, teardown_timeout } = require(`../test/suites/${userSuite}/environment`);
function sleep(ms: number) { function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
@ -9,32 +11,64 @@ function sleep(ms: number) {
let teardownInProgress = false; let teardownInProgress = false;
process.on('SIGINT', function() { async function block() {
if (teardownInProgress) return;
teardownInProgress = true;
console.log('Tearing down environment...');
return teardown()
.then(() => {
process.exit(0)
})
.catch((err: Error) => {
console.error(err);
process.exit(1);
});
});
function main() {
console.log('Setting up environment...');
return setup()
.then(async () => {
while (true) { while (true) {
await sleep(10000); await sleep(10000);
} }
})
.catch((err: Error) => {
console.error(err);
process.exit(1);
});
} }
main(); async function blockOrRun(cmd: string | null) {
if (cmd) {
await exec(cmd);
} else {
await block();
}
}
process.on('SIGINT', function() {
if (teardownInProgress) return;
teardownInProgress = true;
stop()
.then(() => process.exit(0))
.catch(() => process.exit(1));
});
async function stop() {
const timer = setTimeout(() => {
console.error('Teardown timed out...');
process.exit(1);
}, teardown_timeout);
console.log('>>> Tearing down environment <<<');
try {
await teardown();
clearTimeout(timer);
} catch (err) {
console.error(err);
throw err;
}
}
async function start() {
const timer = setTimeout(() => {
console.error('Setup timed out...');
teardown().finally(() => process.exit(1));
}, setup_timeout);
console.log('>>> Setting up environment <<<');
try {
await setup();
clearTimeout(timer);
await blockOrRun(command);
if (!teardownInProgress) {
await stop();
process.exit(0);
}
}
catch (err) {
console.error(err);
await stop();
process.exit(1);
}
}
start();

View File

@ -1,5 +1,6 @@
module.exports = { module.exports = {
IMAGE_NAME: 'clems4ever/authelia' IMAGE_NAME: 'authelia',
DOCKERHUB_IMAGE_NAME: 'clems4ever/authelia'
} }

View File

@ -2,15 +2,12 @@ var spawn = require('child_process').spawn;
function exec(cmd) { function exec(cmd) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const command = spawn(cmd, { const command = spawn(cmd, {shell: true, env: process.env});
shell: true
});
command.stdout.pipe(process.stdout); command.stdout.pipe(process.stdout);
command.stderr.pipe(process.stderr); command.stderr.pipe(process.stderr);
command.on('exit', function(statusCode) { command.on('exit', function(statusCode) {
if (statusCode != 0) { if (statusCode != 0) {
reject(new Error('Exited with status ' + statusCode)); reject(new Error('Command \'' + cmd + '\' has exited with status ' + statusCode + '.'));
return; return;
} }
resolve(); resolve();

View File

@ -112,7 +112,7 @@ export function post_start_validation(handler: IdentityValidable,
}) })
.then((token: string) => { .then((token: string) => {
const host = req.get("Host"); const host = req.get("Host");
const link_url = util.format("https://%s%s?token=%s", host, const link_url = util.format("https://%s/#%s?token=%s", host,
handler.destinationPath(), token); handler.destinationPath(), token);
vars.logger.info(req, "Notification sent to user \"%s\"", vars.logger.info(req, "Notification sent to user \"%s\"",
identity.userid); identity.userid);

View File

@ -49,8 +49,7 @@ export default class Server {
return ServerVariablesInitializer.initialize( return ServerVariablesInitializer.initialize(
config, this.globalLogger, this.requestLogger, deps) config, this.globalLogger, this.requestLogger, deps)
.then(function (vars: ServerVariables) { .then(function (vars: ServerVariables) {
Configurator.configure(config, app, vars, deps); return Configurator.configure(config, app, vars, deps);
return BluebirdPromise.resolve();
}); });
} }

View File

@ -45,11 +45,9 @@ function MatchSubject(subject: Subject) {
} }
export class Authorizer implements IAuthorizer { export class Authorizer implements IAuthorizer {
private logger: Winston;
private readonly configuration: ACLConfiguration; private readonly configuration: ACLConfiguration;
constructor(configuration: ACLConfiguration, logger_: Winston) { constructor(configuration: ACLConfiguration, logger_: Winston) {
this.logger = logger_;
this.configuration = configuration; this.configuration = configuration;
} }

View File

@ -3,7 +3,6 @@ import { Configuration } from "./schema/Configuration";
import { GlobalDependencies } from "../../../types/Dependencies"; import { GlobalDependencies } from "../../../types/Dependencies";
import ExpressSession = require("express-session"); import ExpressSession = require("express-session");
import ConnectRedis = require("connect-redis");
import Sinon = require("sinon"); import Sinon = require("sinon");
import Assert = require("assert"); import Assert = require("assert");
@ -128,22 +127,15 @@ describe("configuration/SessionConfigurationBuilder", function () {
password: "authelia_pass" password: "authelia_pass"
}; };
const RedisStoreMock = Sinon.spy(); const RedisStoreMock = Sinon.spy();
const redisClient = Sinon.mock().returns({ on: Sinon.spy() }); deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock);
const createClientStub = Sinon.stub();
deps.ConnectRedis = Sinon.stub().returns(RedisStoreMock) as any; SessionConfigurationBuilder.build(configuration, deps);
deps.Redis = {
createClient: createClientStub
} as any;
createClientStub.returns(redisClient); Assert(RedisStoreMock.calledWith({
const options = SessionConfigurationBuilder.build(configuration, deps);
Assert(createClientStub.calledWith({
host: "redis.example.com", host: "redis.example.com",
port: 6379, port: 6379,
password: "authelia_pass" pass: "authelia_pass",
logErrors: true,
})); }));
}); });
}); });

View File

@ -1,12 +1,8 @@
import ExpressSession = require("express-session"); import ExpressSession = require("express-session");
import Redis = require("redis");
import { Configuration } from "./schema/Configuration"; import { Configuration } from "./schema/Configuration";
import { GlobalDependencies } from "../../../types/Dependencies"; import { GlobalDependencies } from "../../../types/Dependencies";
import { RedisStoreOptions } from "connect-redis";
export class SessionConfigurationBuilder { export class SessionConfigurationBuilder {
static build(configuration: Configuration, deps: GlobalDependencies): ExpressSession.SessionOptions { static build(configuration: Configuration, deps: GlobalDependencies): ExpressSession.SessionOptions {
const sessionOptions: ExpressSession.SessionOptions = { const sessionOptions: ExpressSession.SessionOptions = {
name: configuration.session.name, name: configuration.session.name,
@ -22,30 +18,13 @@ export class SessionConfigurationBuilder {
}; };
if (configuration.session.redis) { if (configuration.session.redis) {
let redisOptions;
const options: Redis.ClientOpts = {
host: configuration.session.redis.host,
port: configuration.session.redis.port
};
if (configuration.session.redis.password) {
options["password"] = configuration.session.redis.password;
}
const client = deps.Redis.createClient(options);
client.on("error", function (err: Error) {
console.error("Redis error:", err);
});
redisOptions = {
client: client,
logErrors: true
};
if (redisOptions) {
const RedisStore = deps.ConnectRedis(deps.session); const RedisStore = deps.ConnectRedis(deps.session);
sessionOptions.store = new RedisStore(redisOptions); sessionOptions.store = new RedisStore({
} host: configuration.session.redis.host,
port: configuration.session.redis.port,
pass: configuration.session.redis.password,
logErrors: true
});
} }
return sessionOptions; return sessionOptions;
} }

View File

@ -1,5 +1,3 @@
import Sinon = require("sinon");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import Assert = require("assert"); import Assert = require("assert");
import FirstFactorPost = require("./post"); import FirstFactorPost = require("./post");

View File

@ -14,6 +14,8 @@ import { URLDecomposer } from "../..//utils/URLDecomposer";
import { Object } from "../../../lib/authorization/Object"; import { Object } from "../../../lib/authorization/Object";
import { Subject } from "../../../lib/authorization/Subject"; import { Subject } from "../../../lib/authorization/Subject";
import AuthenticationError from "../../../lib/authentication/AuthenticationError"; import AuthenticationError from "../../../lib/authentication/AuthenticationError";
import IsRedirectionSafe from "../../../lib/utils/IsRedirectionSafe";
import * as URLParse from "url-parse";
export default function (vars: ServerVariables) { export default function (vars: ServerVariables) {
return function (req: express.Request, res: express.Response) return function (req: express.Request, res: express.Response)
@ -61,7 +63,7 @@ export default function (vars: ServerVariables) {
vars.regulator.mark(username, true); vars.regulator.mark(username, true);
}) })
.then(function() { .then(function() {
const targetUrl = ObjectPath.get(req, "headers.x-target-url", undefined); const targetUrl = ObjectPath.get<Express.Request, string>(req, "headers.x-target-url", undefined);
if (!targetUrl) { if (!targetUrl) {
res.status(204); res.status(204);
@ -83,10 +85,13 @@ export default function (vars: ServerVariables) {
const authorizationLevel = vars.authorizer.authorization(resObject, subject); const authorizationLevel = vars.authorizer.authorization(resObject, subject);
if (authorizationLevel <= AuthorizationLevel.ONE_FACTOR) { if (authorizationLevel <= AuthorizationLevel.ONE_FACTOR) {
res.json({ if (IsRedirectionSafe(vars, authSession, new URLParse(targetUrl))) {
redirect: targetUrl res.json({redirect: targetUrl});
});
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
} else {
res.json({error: "You're authenticated but cannot be automatically redirected to an unsafe URL."});
return BluebirdPromise.resolve();
}
} }
} }

View File

@ -1,31 +0,0 @@
import * as Express from "express";
import { ServerVariables } from "../../ServerVariables";
import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler";
import { Level } from "../../authentication/Level";
import * as URLParse from "url-parse";
export default function (vars: ServerVariables) {
return function (req: Express.Request, res: Express.Response) {
if (!req.body.url) {
res.status(400);
vars.logger.error(req, "Provide url for verification to be done.");
return;
}
const authSession = AuthenticationSessionHandler.get(req, vars.logger);
const url = new URLParse(req.body.url);
const urlInDomain = url.hostname.endsWith(vars.config.session.domain);
vars.logger.debug(req, "Check domain %s is in url %s.", vars.config.session.domain, url.hostname);
const sufficientPermissions = authSession.authentication_level >= Level.TWO_FACTOR;
vars.logger.debug(req, "Check that protocol %s is HTTPS.", url.protocol);
const protocolIsHttps = url.protocol === "https:";
if (sufficientPermissions && urlInDomain && protocolIsHttps) {
res.send("OK");
return;
}
res.send("NOK");
};
}

View File

@ -1,24 +1,36 @@
import express = require("express"); import express = require("express");
import * as URLParse from "url-parse";
import * as ObjectPath from "object-path";
import { ServerVariables } from "../../ServerVariables"; import { ServerVariables } from "../../ServerVariables";
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import ErrorReplies = require("../../ErrorReplies"); import ErrorReplies = require("../../ErrorReplies");
import UserMessages = require("../../../../../shared/UserMessages"); import UserMessages = require("../../../../../shared/UserMessages");
import { RedirectionMessage } from "../../../../../shared/RedirectionMessage"; import IsRedirectionSafe from "../../../lib/utils/IsRedirectionSafe";
import { AuthenticationSessionHandler } from "../../../lib/AuthenticationSessionHandler";
export default function (vars: ServerVariables) { export default function (vars: ServerVariables) {
return function (req: express.Request, res: express.Response) return function (req: express.Request, res: express.Response)
: BluebirdPromise<void> { : BluebirdPromise<void> {
return new BluebirdPromise<void>(function (resolve, reject) { return new BluebirdPromise<void>(function (resolve, reject) {
let redirectUrl: string = "/"; let redirectUrl: string = ObjectPath.get<Express.Request, string>(
if (vars.config.default_redirection_url) { req, "headers.x-target-url", undefined);
if (!redirectUrl && vars.config.default_redirection_url) {
redirectUrl = vars.config.default_redirection_url; redirectUrl = vars.config.default_redirection_url;
} }
vars.logger.debug(req, "Request redirection to \"%s\".", redirectUrl);
res.json({ const authSession = AuthenticationSessionHandler.get(req, vars.logger);
redirect: redirectUrl if ((redirectUrl && !IsRedirectionSafe(vars, authSession, new URLParse(redirectUrl)))
} as RedirectionMessage); || !redirectUrl) {
res.status(204);
res.send();
return resolve();
}
res.json({redirect: redirectUrl});
return resolve(); return resolve();
}) })
.catch(ErrorReplies.replyWithError200(req, res, vars.logger, .catch(ErrorReplies.replyWithError200(req, res, vars.logger,

View File

@ -4,7 +4,7 @@ import BluebirdPromise = require("bluebird");
import express = require("express"); import express = require("express");
import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument";
import U2f = require("u2f"); import U2f = require("u2f");
import redirect from "../../redirect"; import Redirect from "../../redirect";
import ErrorReplies = require("../../../../ErrorReplies"); import ErrorReplies = require("../../../../ErrorReplies");
import { ServerVariables } from "../../../../ServerVariables"; import { ServerVariables } from "../../../../ServerVariables";
import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler"; import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler";
@ -41,7 +41,7 @@ export default function (vars: ServerVariables) {
return BluebirdPromise.reject(new Error("Error while signing")); return BluebirdPromise.reject(new Error("Error while signing"));
vars.logger.info(req, "Successful authentication"); vars.logger.info(req, "Successful authentication");
authSession.authentication_level = Level.TWO_FACTOR; authSession.authentication_level = Level.TWO_FACTOR;
redirect(vars)(req, res); Redirect(vars)(req, res);
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}) })
.catch(ErrorReplies.replyWithError200(req, res, vars.logger, .catch(ErrorReplies.replyWithError200(req, res, vars.logger,

View File

@ -0,0 +1,14 @@
import { ServerVariables } from "../ServerVariables";
import { Level } from "../authentication/Level";
import * as URLParse from "url-parse";
import { AuthenticationSession } from "AuthenticationSession";
export default function IsRedirectionSafe(
vars: ServerVariables,
authSession: AuthenticationSession,
url: URLParse): boolean {
const urlInDomain = url.hostname.endsWith(vars.config.session.domain);
const protocolIsHttps = url.protocol === "https:";
return urlInDomain && protocolIsHttps;
}

View File

@ -1,33 +0,0 @@
import Assert = require("assert");
import Sinon = require("sinon");
import { SafeRedirector } from "./SafeRedirection";
describe("web_server/middlewares/SafeRedirection", () => {
describe("Url is in protected domain", () => {
before(() => {
this.redirector = new SafeRedirector("example.com");
this.res = {redirect: Sinon.stub()};
});
it("should redirect to provided url", () => {
this.redirector.redirectOrElse(this.res,
"https://mysubdomain.example.com:8080/abc",
"https://authelia.example.com");
Assert(this.res.redirect.calledWith("https://mysubdomain.example.com:8080/abc"));
});
it("should redirect to default url when wrong domain", () => {
this.redirector.redirectOrElse(this.res,
"https://mysubdomain.domain.rtf:8080/abc",
"https://authelia.example.com");
Assert(this.res.redirect.calledWith("https://authelia.example.com"));
});
it("should redirect to default url when not terminating by domain", () => {
this.redirector.redirectOrElse(this.res,
"https://mysubdomain.example.com.rtf:8080/abc",
"https://authelia.example.com");
Assert(this.res.redirect.calledWith("https://authelia.example.com"));
});
});
});

View File

@ -1,21 +0,0 @@
import Express = require("express");
import { BelongToDomain } from "../../../../shared/BelongToDomain";
export class SafeRedirector {
private domain: string;
constructor(domain: string) {
this.domain = domain;
}
redirectOrElse(
res: Express.Response,
url: string,
defaultUrl: string): void {
if (BelongToDomain(url, this.domain)) {
res.redirect(url);
}
res.redirect(defaultUrl);
}
}

View File

@ -34,6 +34,12 @@ export class Configurator {
app.disable(X_POWERED_BY); app.disable(X_POWERED_BY);
app.enable(TRUST_PROXY); app.enable(TRUST_PROXY);
app.use(Helmet()); app.use(Helmet());
app.use(function (req, res, next) {
if (!req.session) {
return next(new Error("No session available."));
}
next();
});
RestApi.setup(app, vars); RestApi.setup(app, vars);
} }

View File

@ -3,7 +3,6 @@ import Express = require("express");
import FirstFactorPost = require("../routes/firstfactor/post"); import FirstFactorPost = require("../routes/firstfactor/post");
import LogoutPost from "../routes/logout/post"; import LogoutPost from "../routes/logout/post";
import StateGet from "../routes/state/get"; import StateGet from "../routes/state/get";
import RedirectPost from "../routes/redirect/post";
import VerifyGet = require("../routes/verify/get"); import VerifyGet = require("../routes/verify/get");
import TOTPSignGet = require("../routes/secondfactor/totp/sign/post"); import TOTPSignGet = require("../routes/secondfactor/totp/sign/post");
@ -87,7 +86,6 @@ function setupResetPassword(app: Express.Application, vars: ServerVariables) {
export class RestApi { export class RestApi {
static setup(app: Express.Application, vars: ServerVariables): void { static setup(app: Express.Application, vars: ServerVariables): void {
app.get(Endpoints.STATE_GET, StateGet(vars)); app.get(Endpoints.STATE_GET, StateGet(vars));
app.post(Endpoints.REDIRECT_POST, RedirectPost(vars));
app.post(Endpoints.LOGOUT_POST, LogoutPost(vars)); app.post(Endpoints.LOGOUT_POST, LogoutPost(vars));

View File

@ -25,8 +25,8 @@ describe.only("shared/DomainExtractor", function () {
const domain1 = DomainExtractor.fromUrl("https://login.example.com:8080/?rd=https://public.example.com:8080/"); const domain1 = DomainExtractor.fromUrl("https://login.example.com:8080/?rd=https://public.example.com:8080/");
Assert.equal(domain1, "login.example.com"); Assert.equal(domain1, "login.example.com");
const domain2 = DomainExtractor.fromUrl("https://single_factor.example.com:8080/secret.html"); const domain2 = DomainExtractor.fromUrl("https://singlefactor.example.com:8080/secret.html");
Assert.equal(domain2, "single_factor.example.com"); Assert.equal(domain2, "singlefactor.example.com");
}); });
}); });
}); });

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